Skip to content

Implement trailing headers support on SocketsHttpHandler #28547

@caesar-chen

Description

@caesar-chen

gRPC (Google RPC) protocol uses HTTP trailers to send/receive metadata. On the client side, SocketsHttpHandler needs to implement support for trailing headers to receive metadata.

Client:
GET /index.html
TE: trailers (Indicates that the client is willing to accept trailer fields in a chunked transfer coding)

Server:
HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked
Trailer: Expires (Indicates what will be the additional fields)

7\r\n 
Network\r\n 
0\r\n 
Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n (Here is the trailing header)
\r\n

API Proposal

To support trailing headers come with the response message, we need a new Property in HttpResponseMessage class.

public HttpResponseHeaders TrailingHeaders
{
    get
    {
        if (_trailingHeaders == null)
        {
              _trailingHeaders = new HttpResponseHeaders();
        }
        return _trailingHeaders;
    }
}

Usage

Trailers are part of the response body (chunked message), the information stored in TrailingHeaders field will be ready for developer after the underneath response stream is read to the EOF.

By default, the HttpCompletionOption for response is ResponseContentRead, which indicates HttpClient operations (Get/Send) will be considered completed after reading the entire response message including the content. Therefore, we will always have the trailing headers information ready in this case.

However, if developer wants to get response as soon as it's available (HttpCompletionOption.ResponseHeadersRead), he needs to issue an explicit read on the response stream to the EOF to get the trailing headers. If the TrailingHeaders is not ready, we will return an empty collection.

Sample Code

// Response contents:

// Headers.
// Content-Type: text/plain
// Transfer-Encoding: chunked
// Trailer: MyCoolTrailerHeader, Hello

// Body.
// Microsoft
// gRPC

// Trailing Headers.
// MyCoolTrailerHeader: info
// Hello: World

HttpClientHandler handler = new SocketsHttpHandler();
var client = new HttpClient(handler);

// CASE 1: Default case.
Task<HttpResponseMessage> getResponseTask = client.GetAsync(url);
using (HttpResponseMessage response = await getResponseTask)
{
    // Headers will always be available for a response message.
    Assert.Contains("text/plain", response.Headers.GetValues("Content-Type"));
    Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding"));
    Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer"));
    Assert.Contains("Hello", response.Headers.GetValues("Trailer"));

    // The HttpClient.GetAsync() by default passed in HttpCompletionOption.ResponseContentRead.
    // `TrailingHeaders` info will be ready when response is returned.
    Assert.Contains("info", response.TrailingHeaders.GetValues("MyCoolTrailerHeader"));
    Assert.Contains("World", response.TrailingHeaders.GetValues("Hello"));
    
    // Trailers should not be part of the content data.
    string data = await response.Content.ReadAsStringAsync();
    Assert.Contains("Microsoft", data);
    Assert.DoesNotContain("MyCoolTrailerHeader", data);
    Assert.DoesNotContain("amazingtrailer", data);
}

// CASE 2: If HttpCompletionOption.ResponseHeadersRead is specified.
Task<HttpResponseMessage> getResponseTask = client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
using (HttpResponseMessage response = await getResponseTask)
{
    // Headers will always be available for a response message.
    Assert.Contains("text/plain", response.Headers.GetValues("Content-Type"));
    Assert.Contains("chunked", response.Headers.GetValues("Transfer-Encoding"));
    Assert.Contains("MyCoolTrailerHeader", response.Headers.GetValues("Trailer"));
    Assert.Contains("Hello", response.Headers.GetValues("Trailer"));

    // Nothing in `TrailingHeaders` since we haven't read the body yet.
    Assert.Equals(string.Empty, response.TrailingHeaders.toString());
    
    // Read the body content: ReadAsStringAsync().
    // Trailers should not be part of the content data.
    string data = await response.Content.ReadAsStringAsync();
    Assert.Contains("Microsoft", data);
    Assert.DoesNotContain("MyCoolTrailerHeader", data);
    Assert.DoesNotContain("amazingtrailer", data);

    // Now `TrailingHeaders` info is ready.
    Assert.Contains("info", response.TrailingHeaders.GetValues("MyCoolTrailerHeader"));
    Assert.Contains("World", response.TrailingHeaders.GetValues("Hello"));
}

HTTP2

The TE header on the request can only contains trailers value. (RFC 7540)

The only exception to this is the TE header field, which MAY be
present in an HTTP/2 request; when it is, it MUST NOT contain any
value other than "trailers".

No additional API is needed for HTTP2.

cc: @dotnet/ncl @geoffkizer @stephentoub

Metadata

Metadata

Assignees

Labels

api-approvedAPI was approved in API review, it can be implemented

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions