Viorel Buliga
About me
← All articles
.NETHttpClientIHttpClientFactoryDiagnosticsBackend

"The Stream Was Already Consumed": AddExtendedHttpClientLogging Breaks in Debug Builds

July 14, 2026·5 min read
Quick answer: Adding AddExtendedHttpClientLogging with LogBody = true made every HTTP response unreadable in my services - the logging handler consumes the content stream, and my code then throws "The stream was already consumed." It only happens in Debug builds, and only from Microsoft.Extensions.Http.Diagnostics 9.1 upwards - 9.0 is fine. Switching from GetAsync to SendAsync with HttpCompletionOption.ResponseHeadersRead restores the Release behaviour in Debug.

I run a microservices solution on .NET 10 where services talk to each other synchronously over HttpClient. I wanted structured request and response logging across all of them, so I added the extended HTTP client logging from Microsoft.Extensions.Http.Diagnostics. Everything looked fine - until I ran it locally.

An HTTP response body stream consumed by the logging handler before the caller can read it

The tech stack

Worth being precise here, because the combination matters - swap any one of these and you may not see it.

ComponentVersion / detail
Runtime.NET 10
ArchitectureMicroservices, synchronous service-to-service HTTP calls
HTTP clientHttpClient resolved through IHttpClientFactory
Logging packageMicrosoft.Extensions.Http.Diagnostics 9.1+ (reproduced on 10.x too). Not on 9.0.
RegistrationAddExtendedHttpClientLogging with LogBody = true, plus AddRedaction(), which it requires
Call styleGetAsync / PostAsJsonAsync - both default to HttpCompletionOption.ResponseContentRead
Build configurationDebug - Release does not reproduce it

The exception

As soon as the caller reads the response body, in Debug:

System.InvalidOperationException:
  The stream was already consumed. It cannot be read again.

It does not come from my deserialisation code. It comes from reading HttpResponseMessage.Content a second time - the first read having been done by the diagnostics handler to log the body.

The configuration and the code that triggers it

The handler only reads the body if you ask it to. LogBody is the switch; the rest bound how much it reads and which content types it treats as text.

// Required by AddExtendedHttpClientLogging - it will not
// resolve its dependencies without the redaction services.
builder.Services.AddRedaction();

builder.Services.AddHttpClient("ReproClient")
    .AddExtendedHttpClientLogging(options =>
    {
        options.LogBody = true;                    // <- reads the body
        options.BodySizeLimit = 1024;              // max bytes read for logging
        options.BodyReadTimeout = TimeSpan.FromSeconds(5);
        options.RequestBodyContentTypes.Add("application/json");
        options.ResponseBodyContentTypes.Add("application/json");
    });

And the calling code, which is about as ordinary as it gets:

// GetAsync defaults to HttpCompletionOption.ResponseContentRead
using var response = await client.GetAsync($"{ApiUrl}/events");
response.EnsureSuccessStatusCode();

var raw = await response.Content.ReadAsStreamAsync();   // throws in Debug
var events = await JsonSerializer.DeserializeAsync<List<Event>>(
    raw, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

Debug fails, Release works

This is the detail that cost me the most time, and the one worth remembering even if you never touch this package. The exact same code, same configuration, same package version: Debug throws, Release does not.

It behaves like a heisenbug. It disappears when you publish, so you start doubting the diagnosis; it comes back the moment you run locally, so you cannot ignore it. And if you write a small reproduction and happen to run it in Release, you conclude the problem is somewhere else entirely and go hunting for exotic causes.

The lesson that generalises: when a failure will not reproduce in a clean project, compare the build configuration before you start blaming the handler pipeline. Debug and Release differ in JIT optimisation and therefore in timing - enough to change the behaviour of anything timing-sensitive, and this handler has a timing knob in BodyReadTimeout. I have not proven that is the mechanism, but it is the first thing I would test.

Narrowing it to the package version

Testing across versions put the boundary in a clear place: the failure appears from Microsoft.Extensions.Http.Diagnostics 9.1 upwards, including the 10.x releases. On 9.0 the same code, in the same Debug build, works.

So pinning the package to 9.0 is a valid fallback if you would rather not change your calling code. It just means giving up whatever else those versions brought.

The fix

What restored Debug to behaving like Release was to stop asking HttpClient to read the whole response before handing it back. GetAsync and PostAsJsonAsync use ResponseContentRead - "complete after reading the entire response including the content". Dropping to SendAsync with ResponseHeadersRead - "complete as soon as a response is available and headers are read; the content is not read yet" - leaves the stream readable for both the handler and my code.

using var request =
    new HttpRequestMessage(HttpMethod.Get, $"{ApiUrl}/events");

using var response = await client.SendAsync(
    request, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode();

var raw = await response.Content.ReadAsStreamAsync();   // works
var events = await JsonSerializer.DeserializeAsync<List<Event>>(
    raw, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

Note what did not change: the logging configuration is identical in both versions. Only the way HttpClient is invoked differs.

One caveat that comes with ResponseHeadersRead. The docs warn that with this option, HttpClient.Timeout "applies only up to where the headers end and the content starts". Reading the content is no longer covered by that timeout, so guard the body read yourself - a CancellationToken from a CancellationTokenSource with its own deadline.

Reproducing it

The reproduction needs nothing exotic - no OAuth handler, no retry policy, no unusual payload. Two projects are enough:

  • ReproApi - an ASP.NET Core Web API exposing a single /events endpoint that returns JSON.
  • ReproClient - a console app calling it through IHttpClientFactory, with the same package version and the same AddExtendedHttpClientLogging configuration.

The client has a single ApplyFix flag at the top. Leave it false to take the GetAsync path and watch it throw; set it to true to take the SendAsync path and watch the same call succeed. Then run the whole thing in Release and the failing path stops failing.

// ReproClient/Program.cs
//   false -> BUG: client.GetAsync(url)   (ResponseContentRead, the default)
//   true  -> FIX: client.SendAsync(request, ResponseHeadersRead)
const bool ApplyFix = false;

The full reproduction is on GitHub if you want to run it yourself: viorelbuligadev/StreamConsumedRepro.

If you hit this too, it is worth reporting to dotnet/extensions, which owns the package - with the version, the logging configuration, and above all the fact that it is Debug-only.

Frequently asked questions

What causes "The stream was already consumed" with extended HTTP logging?

With LogBody enabled, the diagnostics handler reads the response content stream in order to log it. When the caller then reads HttpResponseMessage.Content again, the stream has already been consumed and the second read throws. It shows up with the default ResponseContentRead completion option used by GetAsync and PostAsJsonAsync.

Why does it only happen in Debug and not in Release?

That is the observed behaviour: the same code throws in a Debug build and succeeds in a Release one. I have not proven the mechanism. Debug and Release differ in JIT optimisation and therefore in timing, which is enough to affect anything timing-sensitive - and this handler has a timing knob in BodyReadTimeout. That is a hypothesis to test, not a conclusion. The practical takeaway: if a failure will not reproduce in a clean project, check the build configuration first.

Which package versions are affected?

Microsoft.Extensions.Http.Diagnostics from 9.1 upwards, including 10.x, on .NET 10. Version 9.0 does not reproduce it, so pinning to 9.0 is a valid fallback if you would rather not change your calling code.

Does switching to ResponseHeadersRead have downsides?

Yes. With ResponseHeadersRead, HttpClient.Timeout only covers reading the headers, not the body. You must enforce a timeout on the content read yourself, typically with a CancellationToken carrying its own deadline. Otherwise a server that returns headers promptly but stalls on the content can hang the read.

Can I just turn off body logging instead?

Setting LogBody to false does avoid the problem, since nothing reads the body for logging. It is the right fix if you do not need response bodies in your logs. If you do need them, the SendAsync plus ResponseHeadersRead change keeps both the logging and your own read working.

Official resources

  • LoggingOptions class - Microsoft Learn
  • HttpCompletionOption enum - Microsoft Learn
  • IHttpClientFactory with .NET - Microsoft Learn
  • dotnet/extensions - the repository that owns this package
Share on LinkedIn