Skip to content

Commit b58b29a

Browse files
feat(http-client): add configurable outgoing request capture
1 parent 6505c0c commit b58b29a

7 files changed

Lines changed: 80 additions & 9 deletions

File tree

DebugProbe.AspNetCore.Tests/Configuration/DebugProbeOptionsTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public void Defaults_work_correctly()
1616
Assert.Equal(32, options.MaxBodyCaptureSizeKb);
1717
Assert.Null(options.AllowLocalCompareTargets);
1818
Assert.False(options.AllowUiInProduction);
19+
Assert.True(options.CaptureOutgoingHttpClientRequests);
1920
Assert.Empty(options.IgnorePaths);
2021
Assert.Equal(["Authorization", "Cookie", "Set-Cookie"], options.RedactedHeaders);
2122
Assert.Empty(options.RedactedQueryParameters);
@@ -34,6 +35,7 @@ public void Custom_options_are_registered_and_used()
3435
options.MaxBodyCaptureSizeKb = 4;
3536
options.AllowLocalCompareTargets = true;
3637
options.IgnorePaths = ["/health"];
38+
options.CaptureOutgoingHttpClientRequests = false;
3739
options.RedactedHeaders = ["X-Api-Key"];
3840
options.RedactedQueryParameters = ["token"];
3941
options.RedactedJsonFields = ["password"];
@@ -48,6 +50,7 @@ public void Custom_options_are_registered_and_used()
4850
Assert.Equal(4, options.MaxBodyCaptureSizeKb);
4951
Assert.True(options.AllowLocalCompareTargets);
5052
Assert.Equal(["/health"], options.IgnorePaths);
53+
Assert.False(options.CaptureOutgoingHttpClientRequests);
5154
Assert.Equal(["X-Api-Key"], options.RedactedHeaders);
5255
Assert.Equal(["token"], options.RedactedQueryParameters);
5356
Assert.Equal(["password"], options.RedactedJsonFields);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Net;
2+
using DebugProbe.AspNetCore.Tests.Infrastructure;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.Extensions.DependencyInjection;
5+
6+
namespace DebugProbe.AspNetCore.Tests.Handlers;
7+
8+
public class OutgoingHttpClientCaptureOptionsTests
9+
{
10+
[Fact]
11+
public async Task Disabled_outgoing_capture_does_not_store_outgoing_traces()
12+
{
13+
await using var app = await DebugProbeTestApp.CreateAsync(
14+
endpoints =>
15+
{
16+
endpoints.MapGet("/proxy", async (IHttpClientFactory httpClientFactory) =>
17+
{
18+
var client = httpClientFactory.CreateClient("outgoing");
19+
var body = await client.GetStringAsync("https://api.example.test/ping");
20+
21+
return Results.Text(body);
22+
});
23+
},
24+
configureOptions: options => options.CaptureOutgoingHttpClientRequests = false,
25+
configureServices: services =>
26+
{
27+
services.AddHttpClient("outgoing")
28+
.ConfigurePrimaryHttpMessageHandler(() =>
29+
new StubHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
30+
{
31+
Content = new StringContent("pong")
32+
}));
33+
});
34+
35+
var response = await app.Client.GetAsync("/proxy");
36+
37+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
38+
Assert.Equal("pong", await response.Content.ReadAsStringAsync());
39+
40+
var entry = app.SingleEntry;
41+
Assert.Equal("GET", entry.Method);
42+
Assert.Equal("/proxy", entry.Path);
43+
Assert.Empty(entry.OutgoingRequests);
44+
}
45+
46+
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> send) : HttpMessageHandler
47+
{
48+
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
49+
{
50+
return Task.FromResult(send(request));
51+
}
52+
}
53+
}

DebugProbe.AspNetCore.Tests/Infrastructure/DebugProbeTestApp.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ private DebugProbeTestApp(IHost host)
3232
public static async Task<DebugProbeTestApp> CreateAsync(
3333
Action<IEndpointRouteBuilder> mapEndpoints,
3434
Action<DebugProbeOptions>? configureOptions = null,
35-
Action<IApplicationBuilder>? configureAfterDebugProbe = null)
35+
Action<IApplicationBuilder>? configureAfterDebugProbe = null,
36+
Action<IServiceCollection>? configureServices = null)
3637
{
3738
var host = await new HostBuilder()
3839
.ConfigureWebHost(webHost =>
@@ -42,6 +43,7 @@ public static async Task<DebugProbeTestApp> CreateAsync(
4243
{
4344
services.AddRouting();
4445
services.AddDebugProbe(configureOptions);
46+
configureServices?.Invoke(services);
4547
});
4648
webHost.Configure(app =>
4749
{

DebugProbe.AspNetCore/Extensions/DebugProbeExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,18 @@ public static IServiceCollection AddDebugProbe(this IServiceCollection services,
4343

4444
services.AddHttpClient();
4545

46-
services.AddTransient<DebugProbeHttpClientHandler>();
47-
48-
services.ConfigureAll<HttpClientFactoryOptions>(options =>
46+
if (options.CaptureOutgoingHttpClientRequests)
4947
{
50-
options.HttpMessageHandlerBuilderActions.Add(builder =>
48+
services.AddTransient<DebugProbeHttpClientHandler>();
49+
50+
services.ConfigureAll<HttpClientFactoryOptions>(httpClientOptions =>
5151
{
52-
builder.AdditionalHandlers.Add(builder.Services.GetRequiredService<DebugProbeHttpClientHandler>());
52+
httpClientOptions.HttpMessageHandlerBuilderActions.Add(builder =>
53+
{
54+
builder.AdditionalHandlers.Add(builder.Services.GetRequiredService<DebugProbeHttpClientHandler>());
55+
});
5356
});
54-
});
57+
}
5558

5659
return services;
5760
}

DebugProbe.AspNetCore/Options/DebugProbeOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ public class DebugProbeOptions
2929
/// </summary>
3030
public bool AllowUiInProduction { get; set; }
3131

32+
/// <summary>
33+
/// Captures outgoing requests made through IHttpClientFactory.
34+
/// Defaults to true.
35+
/// </summary>
36+
public bool CaptureOutgoingHttpClientRequests { get; set; } = true;
37+
3238
/// <summary>
3339
/// Additional request paths to ignore.
3440
/// </summary>

DebugProbe.AspNetCore/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ builder.Services.AddDebugProbe(options =>
4848

4949
options.AllowUiInProduction = false;
5050

51+
options.CaptureOutgoingHttpClientRequests = true;
52+
5153
options.IgnorePaths =
5254
[
5355
"/api/auth/login",
@@ -89,7 +91,7 @@ app.UseDebugProbe();
8991
- Configurable body capture limits
9092
- Ignored path configuration for noisy or sensitive endpoints
9193
- Configurable redaction for sensitive headers, query parameters, and JSON fields
92-
- Outgoing `HttpClient` request tracing
94+
- Optional outgoing `HttpClient` request tracing
9395

9496
## Trace Compare
9597

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ builder.Services.AddDebugProbe(options =>
4848

4949
options.AllowUiInProduction = false;
5050

51+
options.CaptureOutgoingHttpClientRequests = true;
52+
5153
options.IgnorePaths =
5254
[
5355
"/api/auth/login",
@@ -89,7 +91,7 @@ app.UseDebugProbe();
8991
- Configurable body capture limits
9092
- Ignored path configuration for noisy or sensitive endpoints
9193
- Configurable redaction for sensitive headers, query parameters, and JSON fields
92-
- Outgoing `HttpClient` request tracing
94+
- Optional outgoing `HttpClient` request tracing
9395

9496
## Trace Compare
9597

0 commit comments

Comments
 (0)