dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.94k stars 4.64k forks source link

HttpClient async calls not throwing TaskCanceledException on Android when token canceled #99568

Open mdemler opened 6 months ago

mdemler commented 6 months ago

Description

When running on Windows, TaskCanceledException is being thrown by async HttpClient calls when the timeout elapses. When running the same code under Android, receiving a WebException instead.

Reproduction Steps

  1. Create a new Maui app project using VS2022 and .NET 8.
  2. Paste the below provided code anywhere within the program that executes.
  3. Run the program on Windows.
  4. Notice a TaskCanceledException is caught.
  5. Run the program on Android.
  6. Notice that the general Exception handler triggers.

` using var client = new HttpClient();

        using var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        try
        {
            var response = await client.GetAsync("https://httpbin.org/delay/10", cancellationTokenSource.Token);
        }
        catch (TaskCanceledException)
        {
            // This is what we expect
        }
        catch (Exception exception)
        {
            // This is where we get on Android
        }

`

Expected behavior

Behavior is identical between Windows and Android. TaskCanceledException is thrown for both.

Actual behavior

WebException is thrown when a timeout is reached.

{System.Net.WebException: Socket closed ---> Java.Net.SocketException: Socket closed at Java.Interop.JniEnvironment.InstanceMethods.CallIntMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/obj/Release/net7.0/JniEnvironment.g.cs:line 20203 at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeVirtualInt32Method(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters) in /Users/runner/work/1/s/xamarin-android/external/Java.Interop/src/Java.Interop/Java.Interop/JniPeerMembers.JniInstanceMethods_Invoke.cs:line 511 at Java.Net.HttpURLConnection.get_ResponseCode() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Java.Net.HttpURLConnection.cs:line 521 at Xamarin.Android.Net.AndroidMessageHandler.<>c__DisplayClass136_0.<DoProcessRequest>b__2() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs:line 625 at System.Threading.Tasks.Task1[[System.Net.HttpStatusCode, System.Net.Primitives, Version=8.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a]].InnerInvoke() at System.Threading.Tasks.Task.<>c.<.cctor>b__281_0(Object obj) at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) --- End of stack trace from previous location --- at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state) at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot, Thread threadPoolThread) --- End of stack trace from previous location --- at Xamarin.Android.Net.AndroidMessageHandler.DoProcessRequest(HttpRequestMessage request, URL javaUrl, HttpURLConnection httpConnection, CancellationToken cancellationToken, RequestRedirectionState redirectState) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs:line 625 at Xamarin.Android.Net.AndroidMessageHandler.DoSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs:line 456 --- End of managed Java.Net.SocketException stack trace --- java.net.SocketException: Socket closed at java.net.SocketInputStream.read(SocketInputStream.java:188) at java.net.SocketInputStream.read(SocketInputStream.java:143) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readFromSocket(ConscryptEngineSocket.java:945) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.processDataFromSocket(ConscryptEngineSocket.java:909) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readUntilDataAvailable(ConscryptEngineSocket.java:824) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.read(ConscryptEngineSocket.java:797) at com.android.okhttp.okio.Okio$2.read(Okio.java:138) at com.android.okhttp.okio.AsyncTimeout$2.read(AsyncTimeout.java:213) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:307) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:301) at com.android.okhttp.okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:197) at com.android.okhttp.internal.http.Http1xStream.readResponse(Http1xStream.java:188) at com.android.okhttp.internal.http.Http1xStream.readResponseHeaders(Http1xStream.java:129) at com.android.okhttp.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:750) at com.android.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:622) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:475) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542) at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106) at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30)

--- End of managed Java.Net.SocketException stack trace --- java.net.SocketException: Socket closed at java.net.SocketInputStream.read(SocketInputStream.java:188) at java.net.SocketInputStream.read(SocketInputStream.java:143) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readFromSocket(ConscryptEngineSocket.java:945) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.processDataFromSocket(ConscryptEngineSocket.java:909) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.readUntilDataAvailable(ConscryptEngineSocket.java:824) at com.android.org.conscrypt.ConscryptEngineSocket$SSLInputStream.read(ConscryptEngineSocket.java:797) at com.android.okhttp.okio.Okio$2.read(Okio.java:138) at com.android.okhttp.okio.AsyncTimeout$2.read(AsyncTimeout.java:213) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:307) at com.android.okhttp.okio.RealBufferedSource.indexOf(RealBufferedSource.java:301) at com.android.okhttp.okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.java:197) at com.android.okhttp.internal.http.Http1xStream.readResponse(Http1xStream.java:188) at com.android.okhttp.internal.http.Http1xStream.readResponseHeaders(Http1xStream.java:129) at com.android.okhttp.internal.http.HttpEngine.readNetworkResponse(HttpEngine.java:750) at com.android.okhttp.internal.http.HttpEngine.readResponse(HttpEngine.java:622) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:475) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411) at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542) at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106) at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30)

--- End of inner exception stack trace --- at Xamarin.Android.Net.AndroidMessageHandler.DoSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs:line 471 at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at MauiApp4.MainPage.RunTestAsync() in C:\Projects\Temp\MauiApp4\MauiApp4\MainPage.xaml.cs:line 41} `

Regression?

Not sure. This is new code.

Known Workarounds

Unknown. Defeats the ability to use Polly to retry on a timeout.

Configuration

.NET 8.0.200

Windows 11.

Device: SAMSUNG ‎A9+, Android 13. Same behavior on Emulator running Android 13.

Other information

No response

stephentoub commented 6 months ago

When running the same code under Android, receiving a WebException instead.

That's confusing to me, as WebException is part of System.Net.Requests, which HttpClient in .NET Core doesn't use at all; WebRequest is built on top of HttpClient, not the other way around.

Can you share the full stack trace from the exception you're receiving?

Is it possible you're actually running on the old mono (https://github.com/mono/mono) rather than on .NET 8?

stephentoub commented 6 months ago

Ah, this might be coming from the Mono.Android HttpClientHandler that gets substituted in on Android (and which is not part of this repo). @steveisok ?

steveisok commented 6 months ago

@simonrozsival, is that expected behavior from the Android handler?

mdemler commented 6 months ago

When running the same code under Android, receiving a WebException instead.

That's confusing to me, as WebException is part of System.Net.Requests, which HttpClient in .NET Core doesn't use at all; WebRequest is built on top of HttpClient, not the other way around.

Can you share the full stack trace from the exception you're receiving?

Is it possible you're actually running on the old mono (https://github.com/mono/mono) rather than on .NET 8?

Updated my posting for that. Sorry, should have included that to begin with.

steveisok commented 6 months ago

@mdemler if you put <UseNativeHttpHandler>false</UseNativeHttpHandler> in your project, that'll use SocketsHttpHandler and you should have similar behavior as you do on Windows.

mdemler commented 6 months ago

Thanks, @steveisok! I've done that, and that will work for us for what we're trying to handle now. It would be nice if this worked as expected even when using the native handler though. Thanks again.

simonrozsival commented 6 months ago

It's a known issue: https://github.com/xamarin/xamarin-android/issues/5761

@grendello do you know why we haven't changed the exceptions to HttpRequestException yet?

grendello commented 6 months ago

@simonrozsival IIRC it was backward compatibility with classic. Shouldn't be an issue anymore

divil5000 commented 2 months ago

The right time to change this behaviour was when going from Xamarin to Core. It's shameful that this wasn't done then. My other issue, linked above, was opened more than three years ago and nobody seems to care that impossible exceptions are being thrown from within the framework.

false might be a resolution or it might not. It's disingenuous to suggest that without also documenting exactly the benefits and drawbacks of moving away from Android's native HTTP stack.