Http trailer support for HTTP/1.x#35337
Conversation
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/Headers/HttpResponseHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
|
I think perhaps we're overcomplicating this. The simplest approach here seems to be: Make TrailingHeaders work just like Headers, i.e. lazily instantiated, no setter. This approach avoids handler lock-in, and has no additional overhead if you don't use trailers (the 99% case). What it doesn't address is: So, my suggestion is, let's just do the simple thing here. |
|
Sounds workable. |
What happens if someone accesses the collection before then? Presumably it would get a collection that could potentially be filled in/ mutated concurrently, so we'd need to be very clear that you couldn't actually access the collection until the stream ended, or else race conditions. Other than that I'm fine with it. My primary concern is not negatively impacting the 99.9% where there are no trailers and the consumer doesn't care about trailers, and in such cases I would be concerned about allocating an additional collection. We just need to be sure then we don't accidentally force it into existence unnecessarily. |
This seems like a recipe for disaster. We should not give out a collection but then hope that someone doesn't modify it while our HTTP stack is trying to add elements to it. If the headers aren't ready, we need to give out something immutable or NULL etc. |
|
HttpResponseMessage and its associated objects are not thread safe and should not be used in a concurrent manner. I'm pretty sure there are plenty of ways you could get into trouble today if you do this. For example, if you try to read on the response stream from multiple threads concurrently, very bad things could happen. |
This specific type of thing should already be protected. Most of our network streams public API surface for Read/Write protect against starting a new Read/Write while one is in progress. They usually will throw an InvalidOperationException in those cases. |
|
To clarify a bit:
We should ensure that the collection is never mutated concurrently. Rather, it is mutated at a very specific point, which is when the caller reads the response stream and there is no more data, so read returns 0 bytes to indicate EOF. I believe we've already agreed this is when trailers should be made available, independent of how they are actually surfaced. If we do this, then it's fine to retrieve and access the collection either before or after receiving EOF. You just won't see any actual trailing headers until after you receive the EOF. For HTTP/1.1, implementing this as described above is straightforward. For HTTP2, it's a little trickier because we can receive the trailing HEADERS frame concurrently to however the user is actually accessing the HttpResponseMessage object. That means we will probably need to store the received trailing headers on the Http2Stream initially, and only add them to the actual TraiingHeaders collection when we are returning EOF. But we need to do something like this anyway to implement the correct behavior for when trailing headers become available. |
As far as I'm aware, we don't do this for request/response streams in SocketsHttpHandler. But regardless, I don't think HttpResponseMessage is intended to support concurrent access and I don't think we need to or want to provide guarantees here. |
We will always return the However, a) Current pattern: use the setter. I can make it internal/private for now, and we can discuss later if we want to make setter public for all related properties. b) Add an additional I vote for a) for simplicity and additional protection for concurrency. Thoughts? |
7efb96a to
443177f
Compare
|
I think @geoffkizer had it nailed down pretty well. Make the headers lazy initialized without a setter like all the others and only populate them during the final Stream.ReadAsync. That way you're doing it on the application's thread and know the application isn't (shouldn't) be touching any of the other objects at the same time. |
Oh, sorry I missed that part. Sounds workable, let me fix in next commit. |
I'm not sure what that means, since the code could be: var headers = resp.TrailingHeaders;
var read = stream.ReadAsync(...);
foreach (var header in headers) { ... }
await read;so we'll need to be very clear about what the guarantees are. I suggest we explicitly say that consuming code should never access the collection property until EOF. |
|
We should also make sure that we don't inadvertently add to the collection while consuming trailing headers while draining a connection that's being returned back to the pool. Such trailers should be ignored. |
I don't think we need to be that strict (though it doesn't hurt to say that). The code you posted should work fine in this approach. |
|
I will need this PR in for HTTP/2 work, PTAL, thanks. /cc: @dotnet/ncl |
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
| if (descriptor.HeaderType == HttpHeaderType.Content) | ||
| if (isFromTrailer) | ||
| { | ||
| response.TrailingHeaders.TryAddWithoutValidation(descriptor, headerValue); |
There was a problem hiding this comment.
Do we need to do the same descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor dance that's done in the else block below?
There was a problem hiding this comment.
It is not clear to me since the headers go to their own collection. I made that change so it is consistent with "normal" headers.
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ChunkedEncodingReadStream.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Show resolved
Hide resolved
stephentoub
left a comment
There was a problem hiding this comment.
Generally LGTM, but left a handful of comments/questions.
…into trailer_header
|
your feedback should be covered @stephentoub. Please take another look when you get chance. |
src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs
Show resolved
Hide resolved
src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.TrailingHeaders.cs
Outdated
Show resolved
Hide resolved
* http trailer for http/1.1 * address feedback * address feedback * pr feedback * stop blocking context * fix framework test failiure * address feedback * seperate tests * address some feedback * more feedback from review * correct ReadAsync * feedback from review * handle short ReadAsync() * more feedback Commit migrated from dotnet/corefx@ac9c746
I decided to separate HTTP/1.x and HTTP/2 trailing header support in 2 different PRs. For HTTP/1.x, trailing headers
are at the end of the chunked response message, while for HTTP/2, HEADERS frame starting the trailing header block
is sent after DATA frames have been sent (not part of the DATA frames). Thus it's hard to apply the new
GenericLoopbackServerto test for both cases.
Pending API review #34912, so I mark this PR as no merge.
/cc: @dotnet/ncl @geoffkizer @stephentoub @Tratcher