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
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
using System.Net;
using System.Text;
using DebugProbe.AspNetCore.Handlers;
using DebugProbe.AspNetCore.Models;
using DebugProbe.AspNetCore.Options;

namespace DebugProbe.AspNetCore.Tests.Handlers;

public class DebugProbeHttpClientHandlerBodyTests
{
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// <summary>
/// Builds a handler wired to the given DebugEntry and options, with a stub
/// inner handler that echoes <paramref name="responseContent"/> back.
/// </summary>
private static (HttpClient client, DebugEntry entry) BuildClient(
DebugProbeOptions options,
HttpContent? responseContent = null)
{
var entry = new DebugEntry();
var context = new DefaultHttpContext();
context.Items["DebugProbeEntry"] = entry;

var handler = new DebugProbeHttpClientHandler(
new HttpContextAccessor { HttpContext = context },
options)
{
InnerHandler = new StubHandler(_ =>
new HttpResponseMessage(HttpStatusCode.OK)
{
Content = responseContent
})
};

return (new HttpClient(handler), entry);
}

// ---------------------------------------------------------------------------
// Test cases
// ---------------------------------------------------------------------------

[Fact]
public async Task Body_under_limit_is_captured_without_truncation_marker()
{
const int limitKb = 1; // 1 KB limit
var bodyText = new string('A', 100); // 100 bytes — well under 1 KB

var options = new DebugProbeOptions { MaxBodyCaptureSizeKb = limitKb };
var (client, entry) = BuildClient(options,
responseContent: new StringContent(bodyText, Encoding.UTF8, "text/plain"));

await client.GetAsync("https://example.test/");

var outgoing = Assert.Single(entry.OutgoingRequests);
Assert.DoesNotContain("[truncated]", outgoing.ResponseBody);
Assert.Contains(bodyText, outgoing.ResponseBody);
}

[Fact]
public async Task Body_exactly_at_limit_is_captured_without_truncation_marker()
{
const int limitKb = 1;
var bodyText = new string('B', 1024); // exactly 1 KB

var options = new DebugProbeOptions { MaxBodyCaptureSizeKb = limitKb };
var (client, entry) = BuildClient(options,
responseContent: new StringContent(bodyText, Encoding.UTF8, "text/plain"));

await client.GetAsync("https://example.test/");

var outgoing = Assert.Single(entry.OutgoingRequests);
Assert.DoesNotContain("[truncated]", outgoing.ResponseBody);
Assert.Contains(bodyText, outgoing.ResponseBody);
}

[Fact]
public async Task Body_over_limit_is_truncated_and_marker_is_appended()
{
const int limitKb = 1;
var bodyText = new string('C', 2048); // 2 KB — double the limit

var options = new DebugProbeOptions { MaxBodyCaptureSizeKb = limitKb };
var (client, entry) = BuildClient(options,
responseContent: new StringContent(bodyText, Encoding.UTF8, "text/plain"));

await client.GetAsync("https://example.test/");

var outgoing = Assert.Single(entry.OutgoingRequests);
Assert.EndsWith("[truncated]", outgoing.ResponseBody);
// Captured prefix must be exactly the limit (1024 'C' chars)
Assert.StartsWith(new string('C', 1024), outgoing.ResponseBody);
}

[Fact]
public async Task Null_content_returns_empty_string()
{
var options = new DebugProbeOptions();
var (client, entry) = BuildClient(options, responseContent: null);

await client.GetAsync("https://example.test/");

var outgoing = Assert.Single(entry.OutgoingRequests);
Assert.Equal(string.Empty, outgoing.ResponseBody);
}

[Fact]
public async Task Non_text_content_returns_empty_string()
{
var binaryContent = new ByteArrayContent([0x89, 0x50, 0x4E, 0x47]); // PNG header bytes
binaryContent.Headers.ContentType =
new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");

var options = new DebugProbeOptions();
var (client, entry) = BuildClient(options, responseContent: binaryContent);

await client.GetAsync("https://example.test/");

var outgoing = Assert.Single(entry.OutgoingRequests);
Assert.Equal(string.Empty, outgoing.ResponseBody);
}

// ---------------------------------------------------------------------------
// Stub inner handler
// ---------------------------------------------------------------------------

private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> send) : HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
=> Task.FromResult(send(request));
}
}
31 changes: 26 additions & 5 deletions DebugProbe.AspNetCore/Handlers/DebugProbeHttpClientHandler.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Diagnostics;
using System.Diagnostics;
using DebugProbe.AspNetCore.Internal.Utils;
using DebugProbe.AspNetCore.Models;
using DebugProbe.AspNetCore.Options;
Expand Down Expand Up @@ -108,10 +108,31 @@ private async Task<string> CaptureBodyAsync(HttpContent? content)
return string.Empty;
}

var body = await content.ReadAsStringAsync();
if (_options.MaxBodyCaptureSizeBytes <= 0)
return string.Empty;

var limit = _options.MaxBodyCaptureSizeBytes;
var buffer = new byte[limit + 1];
var totalRead = 0;

var stream = await content.ReadAsStreamAsync();

int bytesRead;
while (totalRead < buffer.Length &&
(bytesRead = await stream.ReadAsync(buffer.AsMemory(totalRead, buffer.Length - totalRead))) > 0)
{
totalRead += bytesRead;
}

var truncated = totalRead > limit;
var encoding = System.Text.Encoding.UTF8;
var body = encoding.GetString(buffer, 0, Math.Min(totalRead, limit));

if (truncated)
{
body += "[truncated]";
}

return JsonUtils.Format(RedactionUtils.RedactJsonFields(
HttpContentUtils.Trim(body, _options.MaxBodyCaptureSizeBytes),
_options));
return JsonUtils.Format(RedactionUtils.RedactJsonFields(body, _options));
}
}
Loading