HTTP Compression - Improve/fix support for user-specified Accept-Encoding encodings#47000
HTTP Compression - Improve/fix support for user-specified Accept-Encoding encodings#47000geoffkizer merged 8 commits intodotnet:masterfrom EatonZ:patch-1
Conversation
|
If you're using LINQ methods, then Any seems like a better choice than FirstOrDefault. That said, LINQ is going to allocate IEnumerable instances, so we generally try to avoid it to minimize allocation. You could just add a helper method that does a foreach and then call it in each case. |
|
BTW are the encoding names case-insensitive? Previously we were doing a case-sensitive check (I assume that's what the Contains call is doing...). |
|
Yeah, they are case-insensitive: https://tools.ietf.org/html/rfc2616#section-3.5 |
|
We need tests for this. |
| internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) | ||
| { | ||
| if (GZipEnabled && !request.Headers.AcceptEncoding.Contains(s_gzipHeaderValue)) | ||
| if (GZipEnabled && request.Headers.AcceptEncoding.FirstOrDefault(encoding => string.Compare(encoding.Value, s_gzipHeaderValue.Value, StringComparison.OrdinalIgnoreCase) == 0) == null) |
There was a problem hiding this comment.
I think string.Equal is better than string,Compare, since only care about equality
| internal override async ValueTask<HttpResponseMessage> SendAsync(HttpRequestMessage request, bool async, CancellationToken cancellationToken) | ||
| { | ||
| if (GZipEnabled && !request.Headers.AcceptEncoding.Contains(s_gzipHeaderValue)) | ||
| if (GZipEnabled && request.Headers.AcceptEncoding.FirstOrDefault(encoding => string.Compare(encoding.Value, s_gzipHeaderValue.Value, StringComparison.OrdinalIgnoreCase) == 0) == null) |
There was a problem hiding this comment.
Rather than s_gzipHeaderValue.Value you can just use const string Gzip defined above.
Yeah, we should not be using LINQ here. I'd be a little surprised if it even compiles; we try to avoid even referencing System.Linq.dll in assemblies like this. |
It does not. https://github.com/dotnet/runtime/pull/47000/checks?check_run_id=1703936268 |
|
I will update to remove LINQ. |
|
Tagging subscribers to this area: @dotnet/ncl Issue DetailsFixes #46948 I replaced the
|
|
Updated based on feedback. |
| { | ||
| foreach (var existingEncoding in acceptEncodingHeader) | ||
| { | ||
| if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true; |
There was a problem hiding this comment.
return true should be on a separate line and enclosed by braces
| internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0; | ||
| internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0; | ||
|
|
||
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) |
|
|
||
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) | ||
| { | ||
| foreach (var existingEncoding in acceptEncodingHeader) |
There was a problem hiding this comment.
Don't use var; use the full type name
| { | ||
| foreach (var existingEncoding in acceptEncodingHeader) | ||
| { | ||
| if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true; |
There was a problem hiding this comment.
Nit:
| if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true; | |
| if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return true; | |
| } |
|
|
||
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) | ||
| { | ||
| foreach (var existingEncoding in acceptEncodingHeader) |
There was a problem hiding this comment.
Nit:
| foreach (var existingEncoding in acceptEncodingHeader) | |
| foreach (StringWithQualityHeaderValue existingEncoding in acceptEncodingHeader) |
| internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0; | ||
| internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0; | ||
|
|
||
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) |
There was a problem hiding this comment.
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) | |
| private static bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) |
| internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0; | ||
| internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0; | ||
|
|
||
| private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding) |
There was a problem hiding this comment.
I don't know what, if anything, can be done about it in this PR, but it looks like this is going to allocate more than we do today, e.g. an enumerator for the header collection. It's also going to force a StringWithQualityHeaderValue to be materialized for every entry in the collection, though I don't know if that was happening already.
There was a problem hiding this comment.
Would it be more expensive than the existing Contains check?
One thing I thought about was changing it to check all encodings in 1 loop instead of 3 loops. I didn't want to stray too far from the existing implementation, though.
There was a problem hiding this comment.
it looks like this is going to allocate more than we do today, e.g. an enumerator for the header collection
Presumably we could fix this in HttpHeaderValueCollection<T> by adding a struct enumerator, right?
Also, the common case here is that this header isn't set at all, so we could at least optimize for the case where there are no header values and return a singleton empty enumerator in that case.
It's also going to force a StringWithQualityHeaderValue to be materialized for every entry in the collection, though I don't know if that was happening already.
Probably happening already. Definitely happening if you set the header value by constructing a StringWithQualityHeaderValue. But likely also happening anyway, even if you set the value as a string, because we always force these values to get parsed eventually before send them (currently, that is).
There was a problem hiding this comment.
Also, the common case here is that this header isn't set at all, so we could at least optimize for the case where there are no header values and return a singleton empty enumerator in that case.
Ok, if that is indeed the common case, I'll go ahead and do that in a PR in parallel to this one. I don't want to block this PR on adding new API.
|
Existing tests are here: https://github.com/dotnet/runtime/blob/master/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Decompression.cs#L264 We should be able to add some similar cases to cover this new behavior. |
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs
Outdated
Show resolved
Hide resolved
Co-authored-by: Stephen Toub <stoub@microsoft.com>
stephentoub
left a comment
There was a problem hiding this comment.
Thanks for the fix. The product change LGTM. But tests would be good, as Geoff noted.
src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/DecompressionHandler.cs
Show resolved
Hide resolved
|
I'll look at getting a test in soon. |
|
I need some pointers on getting these tests to run. It says: Please let me know what I might be missing. |
|
The class you're specifying is an abstract base class. If you want to run a specific test and so give xunit a test name, it needs to include the actual class name, e.g. SocketsHttpHandler_HttpClientHandler_Decompression_Tests. |
|
Also, you need to be in the System.Net.Http functional tests directory to run the HTTP functional tests. |
|
Thank you. Test added. |
| #if NETCOREAPP | ||
| [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, "gzip; q=1.0, deflate; q=1.0, br; q=1.0")] | ||
| #endif | ||
| [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate, "gzip; q=1.0, deflate; q=1.0")] |
There was a problem hiding this comment.
We should add a case here where the decompression handler will add at least one new header value, but not overwrite an existing one (or ones).
e.g. something like [InlineData(DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli, "deflate; q=1.0")]
and check that the result is "deflate; q=1.0, gzip, brotli".
|
The test is failing for WinHttpHandler. We should just check for IsWinHttpHandler (as some other tests do) and return without doing the test. |
|
@geoffkizer Ok, so just remove the "br" check? |
|
No, just do If you check the test output, you'll see that WinHttpHandler is not handling these cases correctly. It either needs to be fixed (as we've done with SocketsHttpHandler in this PR) or the tests need to be disabled for WinHttpHandler. I don't think it's crucial to fix WinHttpHandler, and it may not be straightforward to do, so let's just do the latter. |
|
@geoffkizer Yup - that is what I meant, just remove the "br" check from the Done |
|
Ah, I understand now, thanks. Changes look good. If CI results look good I will merge. Thanks for the contribution! |
|
/azp run runtime-libraries-coreclr outerloop |
|
Azure Pipelines successfully started running 1 pipeline(s). |
|
Happy to help! Could this be considered for .NET 5.0.3? .NET 6 is rather far away, and there is no workaround to stop duplicates from being added. |
|
Ask @karelz |
|
@karelz I guess that's a no?😥 |
|
@EatonZ sorry I missed the question here. Let's move the discussion to the issue. |
Fixes #46948
cc @geoffkizer @karelz