Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions DevProxy.Integration.Tests/FakeOrigin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -94,6 +95,9 @@ public static async Task<FakeOrigin> 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";
Expand Down
31 changes: 31 additions & 0 deletions DevProxy.Integration.Tests/PlainHttpIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Net;
using System.Text;
using DevProxy.Abstractions.Utils;
using Xunit;

namespace DevProxy.Integration.Tests;
Expand Down Expand Up @@ -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);
}
}
7 changes: 7 additions & 0 deletions DevProxy.Proxy.Kestrel/Internal/PluginPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -125,10 +126,16 @@ public async Task<RequestPhase> 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);

Expand Down
Loading