diff --git a/DevProxy.Integration.Tests/FakeOrigin.cs b/DevProxy.Integration.Tests/FakeOrigin.cs index c6bbbc3e..034d6201 100644 --- a/DevProxy.Integration.Tests/FakeOrigin.cs +++ b/DevProxy.Integration.Tests/FakeOrigin.cs @@ -24,6 +24,7 @@ namespace DevProxy.Integration.Tests; /// POST /echo → 200 echoes request body (X-Echo-Length header) /// GET /status/{code} → {code} "status {code}" /// GET /headers → 200, reflects X-Probe request header in body +/// GET /via → 200, reflects the Via request header values in body /// GET /sse → 200 text/event-stream, 5 events flushed ~50ms apart /// GET /big/{n} → 200, n bytes of 'A' (large finite body) /// GET /json → 200 application/json, a 2-element array @@ -94,6 +95,9 @@ public static async Task StartAsync() return Results.Text($"probe={probe}"); }); + app.MapGet("/via", (HttpContext ctx) => + Results.Text(string.Join(", ", ctx.Request.Headers.Via.ToArray()))); + app.MapGet("/sse", async (HttpContext ctx) => { ctx.Response.Headers.ContentType = "text/event-stream"; diff --git a/DevProxy.Integration.Tests/PlainHttpIntegrationTests.cs b/DevProxy.Integration.Tests/PlainHttpIntegrationTests.cs index fa8d7ffd..a9536147 100644 --- a/DevProxy.Integration.Tests/PlainHttpIntegrationTests.cs +++ b/DevProxy.Integration.Tests/PlainHttpIntegrationTests.cs @@ -4,6 +4,7 @@ using System.Net; using System.Text; +using DevProxy.Abstractions.Utils; using Xunit; namespace DevProxy.Integration.Tests; @@ -130,4 +131,34 @@ public async Task Get_RequestHeader_ReachesOrigin() Assert.Equal("probe=abc123", body); } + + [Fact] + public async Task Get_PassedThrough_AddsViaHeader() + { + await using var origin = await FakeOrigin.StartAsync(); + await using var proxy = await KestrelProxyHarness.StartAsync(origin.Host); + using var client = proxy.CreateHttpClient(); + + using var response = await client.GetAsync( + new Uri($"http://{origin.Host}/via")); + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal($"1.1 dev-proxy/{ProxyUtils.ProductVersion}", body); + } + + [Fact] + public async Task Get_PassedThrough_AppendsToExistingViaHeader() + { + await using var origin = await FakeOrigin.StartAsync(); + await using var proxy = await KestrelProxyHarness.StartAsync(origin.Host); + using var client = proxy.CreateHttpClient(); + + using var request = new HttpRequestMessage( + HttpMethod.Get, new Uri($"http://{origin.Host}/via")); + _ = request.Headers.TryAddWithoutValidation("Via", "1.0 upstream-proxy"); + using var response = await client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + + Assert.Equal($"1.0 upstream-proxy, 1.1 dev-proxy/{ProxyUtils.ProductVersion}", body); + } } diff --git a/DevProxy.Proxy.Kestrel/Internal/PluginPipeline.cs b/DevProxy.Proxy.Kestrel/Internal/PluginPipeline.cs index 0e610179..71ebedfd 100644 --- a/DevProxy.Proxy.Kestrel/Internal/PluginPipeline.cs +++ b/DevProxy.Proxy.Kestrel/Internal/PluginPipeline.cs @@ -6,6 +6,7 @@ using System.Globalization; using DevProxy.Abstractions.Plugins; using DevProxy.Abstractions.Proxy; +using DevProxy.Abstractions.Utils; using DevProxy.Proxy.Kestrel.Http; using Microsoft.Extensions.Logging; @@ -125,10 +126,16 @@ public async Task RunRequestAsync(CanonicalProxySession session, C } _logger.LogRequest("Passed through", MessageType.PassedThrough, loggingContext); + AddProxyHeader(request); } return RequestPhase.Watched; } + // RFC 9110 §7.6.3: a proxy MUST send a Via header in each message it forwards. + // Appended as a new field so any existing Via value is preserved. + private static void AddProxyHeader(Abstractions.Proxy.Http.IHttpRequest request) => + request.Headers.Add("Via", $"{request.HttpVersion.ToString(2)} dev-proxy/{ProxyUtils.ProductVersion}"); + public Task RunResponseAsync(CanonicalProxySession session, CancellationToken ct) => RunResponseCoreAsync(session, betweenPhases: null, ct);