xamarin / xamarin-macios

.NET for iOS, Mac Catalyst, macOS, and tvOS provide open-source bindings of the Apple SDKs for use with .NET managed languages such as C#
Other
2.42k stars 507 forks source link

NSUrlSession - ContentEncoding headers returned but content is uncompressed #20661

Closed projectgoav closed 1 month ago

projectgoav commented 1 month ago

Steps to Reproduce

  1. Make an HTTP request using NSUrlSessionHandler to a server that supports gzip/deflate compression
  2. Examine the ContentEncoding headers & the response stream on return

Expected Behavior

ContentEncoding headers should be blank as the content has been already decompressed.

Actual Behavior

ContentEncoding headers still contain gzip/deflate entries. We use this to decide if we need to wrap the content in a gzip/deflate stream which ultimately fails as the content is already uncompressed.

I was sure that back in the Xamarin.iOS/Xamarin.Mac days the ContentEncoding headers were stripped as the content has been uncompressed already. We've recently updated our app to .NET8.0-iOS and this no longer seems to be the case.

rolfbjarne commented 1 month ago
  • Assuming it has changed, is this a regression?

It could also be a bug fix, but I find it strange that it changed, since the underlying code is the same, and hasn't changed much.

Could you provide test projects (both a Xamarin.iOS project and a .NET project) that shows the difference?

  • If it hasn't changed, is there a reliable way to detect if the content is already decompressed other than wrapping in a gzip/deflate stream, attempting to read and detecting the resulting exception?

Why can't you assume it's always decompressed?

In fact, it seems it's not possible to get the compressed gzip data with NSUrlSession: https://stackoverflow.com/q/41044239/183422

projectgoav commented 1 month ago

Why can't you assume it's always decompressed?

Using the following pseudo-ish code:

HttpResponseMessage response = MakeHttpRequest(/*...*/);
Stream responseStream = response.Content.ReadAsStreamAsync().Result; 

if (response.Content.Headers.ContentEncoding.Contains("gzip"))
{
    return new GZipStream(responseStream, CompressionMode.Decompress);
}
else if (response.Content.Headers.ContentEncoding.Contains("deflate"))
{
    return new DeflateStream(responseStream, CompressionMode.Decompress);
}
else
{
    return responseStream;
}

On Android, if I enable AutomaticDecompression on our handler, the ContentEncoding headers are not populated. Disabling this property returns gzip/deflate in the ContentEncoding header.

On iOS, they remain populated.

I'm sure this hasn't always been the case as this particular but of code has been running on our apps for a long time. It was assumed that the ContentEncoding would be stripped by clients who handle automatic compression otherwise you need to handle it yourself like the code above.

rolfbjarne commented 1 month ago

OK, could you provide test projects (both a Xamarin.iOS project and a .NET project) that shows the difference? That way we can be sure we're testing the same thing.

projectgoav commented 1 month ago

I've tested this on .NET6.0 & .NET7.0 iOS and behaviour is consistent. On each, iOS always returns the ContentEncoding headers populated with the content decompressed. I'm quite happy that this hasn't changed recently and is likely a bug in our code and/or a misunderstanding of how the HTTP system works under .NET.

I sadly don't have the time or space on a Mac machine to get setup with the pre .NET for iOS toolchain, a supported version of XCode and Visual Studio for Mac so I wont be able to get a test project for you in a reasonable timeframe.

I do think it's odd that Android & iOS behave differently under the same circumstances. However, given a correctly configured HttpMessageHandler on both platforms you can achieve the consistent behaviour and so I'm happy to leave this closed.