-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
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