runtime: HttpResponseMessage does not contain Content-Type and Content-Length headers in DelegatingHandler

Is there an existing issue for this?

  • I have searched the existing issues

Describe the bug

When inside DelegatingHandler, which is a HTTP request/response interceptor implementation for C#/.NET, (maybe somewhere else as well), HttpResponseMessage.Headers does not contain Content-Length and Content-Type, although the actual underlying HTTP response contains them. They might have been case-insensitive (that’s allowed by HTTP RFC) in name but they are not detected at all.

Edit: might be related: https://developercommunity.visualstudio.com/t/Getting-Content-Length-when-using-HttpCl/1187153?space=21&q=git+rebase+--continue

The following must be used in order to get these two headers - the response body (“content”) must be read (unexpected and unwanted):

await response.Content.LoadIntoBufferAsync();
var bodySizeBytes = response.Content.Headers.ContentLength;

Why actual content must be seemingly read to know content length header value which was created to know content length without reading the content? (the header is not mandatory (HTTP RFC ), but it is “removed” from the response in C#).

Expected Behavior

HttpResponseMessage.Headers contains Content-Length and Content-Type headers when they are sent in HTTP response so that HttpResponseMessage.Content does not have to be read into buffer to be able to get Content-Length header value (response body/content size in bytes).

Steps To Reproduce

Do a HttpClient GET call with using the following MessageHandler:

/// <summary>
/// Must be registered as transient
/// </summary>
public class HttpRequestInfoInterceptor : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = await base.SendAsync(request, cancellationToken);

        //TEST
        var responseHasContent = response.Content is not null;
        Trace.WriteLine($"Response has content: {responseHasContent}");
        Trace.WriteLine("--------");
        Trace.WriteLine("Response headers:");
        var headers = response.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        foreach(var header in headers)
        {
            Trace.WriteLine($"{header.Key}: {header.Value}");
        }

        Trace.WriteLine("--------");

        await response.Content.LoadIntoBufferAsync(); //needs to called else the following line fails
        var contentLength = response.Content.Headers.ContentLength;
        Debug.WriteLine("Response headers ("from content"):");
        var contentHeaders = response.Content.Headers.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
        foreach (var header in contentHeaders)
        {
            Trace.WriteLine($"{header.Key}: {header.Value}");
        }

        return response;
    }
}

Set up in DI like this:

public static IServiceCollection AddApiClient<T>(this IServiceCollection services)
    where T : class //and where T has HttpClient constructor parameter
{
    services.AddTransient<HttpRequestInfoInterceptor>();

    //.AddHttpClient -> NuGet package Microsoft.Extensions.Http
    services.AddHttpClient<T>(httpClient =>
    {
        httpClient.BaseAddress = new Uri("https://www.example.com/");
    }).AddHttpMessageHandler<HttpRequestInfoInterceptor>();
    return services;
}

Output:

Response has content: True
--------
Response headers:
Date: System.String[]
Server: System.String[]
X-Powered-By: System.String[]
X-XSS-Protection: System.String[]
X-Frame-Options: System.String[]
Set-Cookie: System.String[]
Upgrade: System.String[]
Connection: System.String[]
Vary: System.String[]
Transfer-Encoding: System.String[]
--------
Response headers ("from content"):
Content-Type: System.String[]
Content-Length: System.String[]

When I call the endpoint manually via Swagger UI (which calls curl and displays the result in web UI) (note: from what I read on the internet, gzip might be an important piece here):

content-encoding: gzip 
content-length: 25
content-type: text/plain;charset=UTF-8  
date: Wed,06 Dec 2023 18:54:37 GMT  
expires: Thu,19 Nov 1981 08:52:00 GMT  
pragma: no-cache  
server: Apache  
vary: Accept-Encoding  
x-firefox-spdy: h2  
x-frame-options: SAMEORIGIN  
x-powered-by: CENSORED  
x-xss-protection: 1; mode=block 

Exceptions (if any)

No response

.NET Version

8.0.100

Anything else?

No response

About this issue

  • Original URL
  • State: closed
  • Created 7 months ago
  • Comments: 16 (8 by maintainers)

Most upvoted comments

Yes and it’s of course not expected that response headers would be correct after decompression or any other modification of the received response from the network.

How would a handler like the ProgressMessageHandler you mentioned report progress percentage if the amount of data read from the content didn’t match the advertised content length?

It is a valid assumption for users to make that if a ContentLength is set, that number is correct and reflects exactly the amount of data that can be read from the HttpContent. Not removing the header when making modifications would mean breaking that assumption.

Potentially exposing the original header info in some way is what #42789 is tracking. Closing this as a duplicate of that. Feel free to reopen the issue if it turns out that you’re not using automatic decompression after all.

For that the only current workaround is not to use automatic decompression - see https://github.com/dotnet/runtime/issues/42789#issuecomment-699628305