Skip to content

HTTP Compression - Improve/fix support for user-specified Accept-Encoding encodings#47000

Merged
geoffkizer merged 8 commits intodotnet:masterfrom
EatonZ:patch-1
Jan 26, 2021
Merged

HTTP Compression - Improve/fix support for user-specified Accept-Encoding encodings#47000
geoffkizer merged 8 commits intodotnet:masterfrom
EatonZ:patch-1

Conversation

@EatonZ
Copy link
Contributor

@EatonZ EatonZ commented Jan 14, 2021

@geoffkizer
Copy link
Contributor

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.

@geoffkizer
Copy link
Contributor

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...).

@geoffkizer
Copy link
Contributor

Yeah, they are case-insensitive: https://tools.ietf.org/html/rfc2616#section-3.5

@geoffkizer
Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than s_gzipHeaderValue.Value you can just use const string Gzip defined above.

@stephentoub
Copy link
Member

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.

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.

@geoffkizer
Copy link
Contributor

I'd be a little surprised if it even compiles

It does not. https://github.com/dotnet/runtime/pull/47000/checks?check_run_id=1703936268

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 14, 2021

I will update to remove LINQ.

@ghost ghost added the area-System.Net.Http label Jan 14, 2021
@ghost
Copy link

ghost commented Jan 14, 2021

Tagging subscribers to this area: @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #46948

I replaced the Contains check with a FirstOrDefault implementation. DecompressionHandler will now check each AcceptEncoding value to ensure it does not exist before adding its own. StringComparison.OrdinalIgnoreCase is used for comparing in the event users specify mixed-case encodings.
I believe this is a sufficient fix. If there are any other areas of concern I may have missed, let me know,

cc @geoffkizer @karelz

Author: EatonZ
Assignees: -
Labels:

area-System.Net.Http

Milestone: -

@dnfadmin
Copy link

dnfadmin commented Jan 14, 2021

CLA assistant check
All CLA requirements met.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 14, 2021

Updated based on feedback.

{
foreach (var existingEncoding in acceptEncodingHeader)
{
if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return true should be on a separate line and enclosed by braces

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be static

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
{
foreach (var existingEncoding in acceptEncodingHeader)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use var; use the full type name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

{
foreach (var existingEncoding in acceptEncodingHeader)
{
if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase)) return true;
if (string.Equals(existingEncoding.Value, encoding, StringComparison.OrdinalIgnoreCase))
{
return true;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
{
foreach (var existingEncoding in acceptEncodingHeader)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
foreach (var existingEncoding in acceptEncodingHeader)
foreach (StringWithQualityHeaderValue existingEncoding in acceptEncodingHeader)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
private static bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

internal bool DeflateEnabled => (_decompressionMethods & DecompressionMethods.Deflate) != 0;
internal bool BrotliEnabled => (_decompressionMethods & DecompressionMethods.Brotli) != 0;

private bool EncodingExists(HttpHeaderValueCollection<StringWithQualityHeaderValue> acceptEncodingHeader, string encoding)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geoffkizer
Copy link
Contributor

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.

Co-authored-by: Stephen Toub <stoub@microsoft.com>
Copy link
Member

@stephentoub stephentoub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix. The product change LGTM. But tests would be good, as Geoff noted.

Co-authored-by: Stephen Toub <stoub@microsoft.com>
@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 15, 2021

I'll look at getting a test in soon.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 24, 2021

I need some pointers on getting these tests to run.
For example, if I run this in cmd:

cd src\libraries\Common\tests
dotnet build /t:Test /p:XunitMethodName=System.Net.Http.Functional.Tests.HttpClientHandler_Decompression_Test.GetAsync_SetAutomaticDecompression_AcceptEncodingHeaderSentWithNoDuplicates

It says: Discovered: Common.Tests (found 0 of 241 test cases)

Please let me know what I might be missing.

@stephentoub
Copy link
Member

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.

@stephentoub
Copy link
Member

Also, you need to be in the System.Net.Http functional tests directory to run the HTTP functional tests.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 24, 2021

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")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

@geoffkizer
Copy link
Contributor

The test is failing for WinHttpHandler. We should just check for IsWinHttpHandler (as some other tests do) and return without doing the test.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 25, 2021

@geoffkizer Ok, so just remove the "br" check?

@geoffkizer
Copy link
Contributor

No, just do if (IsWinHttpHandler) { return; } at the beginning of the test.

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.

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 25, 2021

@geoffkizer Yup - that is what I meant, just remove the "br" check from the if.

Done

@geoffkizer
Copy link
Contributor

Ah, I understand now, thanks.

Changes look good. If CI results look good I will merge.

Thanks for the contribution!

@geoffkizer
Copy link
Contributor

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 25, 2021

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.

@geoffkizer
Copy link
Contributor

Ask @karelz

@geoffkizer geoffkizer merged commit abcf07b into dotnet:master Jan 26, 2021
@karelz karelz added this to the 6.0.0 milestone Jan 26, 2021
@EatonZ
Copy link
Contributor Author

EatonZ commented Jan 26, 2021

@karelz I guess that's a no?😥

@karelz
Copy link
Member

karelz commented Jan 26, 2021

@EatonZ sorry I missed the question here. Let's move the discussion to the issue.
We would need to have good business case - how often does it hurt? How much does it hurt your service? (anything larger?) ... We can take details offline if needed.

@ghost ghost locked as resolved and limited conversation to collaborators Feb 25, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HTTP Compression - Improve/fix support for user-specified Accept-Encoding encodings

5 participants