This is Xamarin's bug tracking system. For product support, please use the support links listed in your Xamarin Account.
Bug 44673 - AndroidClientHandler ignores Timeout value and CancellationToken
Summary: AndroidClientHandler ignores Timeout value and CancellationToken
Status: RESOLVED FIXED
Alias: None
Product: Android
Classification: Xamarin
Component: General (show other bugs)
Version: 7.0 (C8)
Hardware: PC Windows
: --- normal
Target Milestone: ---
Assignee: Jonathan Pryor
URL:
Depends on:
Blocks:
 
Reported: 2016-09-23 09:44 UTC by Leon
Modified: 2017-03-23 13:01 UTC (History)
6 users (show)

See Also:
Tags:
Is this bug a regression?: ---
Last known good build:


Attachments

Description Leon 2016-09-23 09:44:24 UTC
Currently I have a HttpClient with a timeout defined like this:

var httpClient = new HttpClient(new Xamarin.Android.Net.AndroidClientHandler());
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd(ClientInfo.ToUserAgentString());
httpClient.Timeout = TimeSpan.FromMilliseconds(10000);
ByteArrayContent content = new ByteArrayContent(postData);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
content.Headers.ContentEncoding.Add("utf-8");
HttpResponseMessage response = await httpClient.PostAsync(Server + CallPath, content);


While this code works fine when using the HttpClientHandler (it stops and generates a expected timeout exception after 10 seconds), whenever I'm using the AndroidClientHandler the request times out after about 60 seconds (as a WebException with a Java.Net.ConnectException inner exception), no matter the timeout value.

I tried migrating to a CancellationToken implementation as shown below:

CancellationTokenSource timeoutCts = new CancellationTokenSource();
timeoutCts.CancelAfter(10000);

var httpClient = new HttpClient(new Xamarin.Android.Net.AndroidClientHandler());
//... Setup headers and content
HttpResponseMessage response = await httpClient.PostAsync(Server + CallPath, content,timeoutCts.Token);

While this works when using HttpClientHandler (the PostAsync times out with an expected OperationCanceledException), the CancellationToken is ignored when using the AndroidClientHandler and as with the non-token implementation the PostAsync times out after about 60 seconds with the same WebException.
Comment 1 Sam 2016-10-05 12:47:58 UTC
I can confirm that the AndroidClientHandler ignores the TimeOut setting, although it doesn't seem to happen all the time.
Comment 2 David Petrík 2016-10-06 12:10:18 UTC
I looked into your code
https://github.com/xamarin/xamarin-android/blob/master/src/Mono.Android/Xamarin.Android.Net/AndroidClientHandler.cs

and i can confirm that CancelationToken is only declared in DoProcessRequest as input parameter and never used for cancelling long running requests.
Comment 3 Marek Habersack 2016-10-10 11:58:48 UTC
This is fixed in Xamarin.Android/master, commit 5055a209de3f782468e0b0f1e7526e32ec2bd3bd
Comment 4 David Petrík 2016-10-10 12:31:42 UTC
Hi Marek,
coul'd you check this code? I not study it deeply :-)

public AndroidHttpResponseMessage (URL javaUrl, HttpURLConnection httpConnection) {
			javaUrl = javaUrl;
			httpConnection = httpConnection;
		}

We added your code directly into our project and we received this warning

Warning	CS1717	Assignment made to same variable; did you mean to assign something else?	

Thank you for above fix

David
Comment 5 David Petrík 2016-10-12 11:41:34 UTC
Hi,
i created new request in Bugzilla for previous comment

https://bugzilla.xamarin.com/show_bug.cgi?id=45311

David
Comment 6 Leon 2016-11-29 10:40:29 UTC
The fix still has a few issues.
The handler will not cancel properly if it was cancelled during a await call in the DoProcessRequest. Which might be an issue on slow connections or in case of timeouts.

Example scenario:

1. App makes request and sends along cancellation token to the handler.

2. Eventually processing reaches following bit of code at line 248:
await httpConnection.ConnectAsync().ConfigureAwait(false);

3. Since it's a slow connection the user decides to cancel the request. App sets Cancellationtoken's IsCancellationRequested to true.

4. The Task doesn't actually cancel until the ConnectAsync finishes or until the request times out (for which the timeout value in HttpClient is still ignored).
In case of a timeout it will also throw a WebException instead of the expected TaskCanceledException, causing a issue in the expected app flow.
Comment 7 Leon 2016-11-29 13:32:13 UTC
Would something like 

await Task.WhenAny(httpConnection.ConnectAsync(),Task.Run(()=> { cancellationToken.WaitHandle.WaitOne(); })).ConfigureAwait(false);

work to solve this issue, or are there major caveats to this approach?
Comment 8 Sam 2016-11-29 13:36:18 UTC
This would help, but it is not a definite fix, because this is only the init part of the call:
await httpConnection.ConnectAsync ().ConfigureAwait (false);

The call itself happens on this line:
var statusCode = await Task.Run (() => (HttpStatusCode)httpConnection.ResponseCode).ConfigureAwait (false);
Comment 9 Leon 2016-11-29 16:07:28 UTC
Judging by network activity most of the call takes place in the CopyToAsync method. By using the ReadAsStreamAsync method the content can be exposed as a Stream. The CopyToAsync method for streams does support CancellationTokens, solving that part of the process as well:

 if (httpConnection.DoOutput) {
   using (var stream = await request.Content.ReadAsStreamAsync())
   {
       await stream.CopyToAsync(httpConnection.OutputStream, 4096, cancellationToken).ConfigureAwait(false);
   }
 }
Comment 10 David Petrík 2017-01-10 15:05:01 UTC
Hi, 
we tried your last implementaion from GitHub and we have also problem with this exception

System.ObjectDisposedExceptionThe CancellationTokenSource has been disposed.
System.Threading.CancellationTokenSource.get_WaitHandle()<1a384679528d44a8be7dd421c9e8c3ae>:0
System.Threading.CancellationToken.get_WaitHandle()<1a384679528d44a8be7dd421c9e8c3ae>:0
Xamarin.Android.Net.Fix.AndroidClientHandler.<>c__DisplayClass32_0.<DoProcessRequest>b__0()<89e4b6c2417f4da0a8614b6ae2fcaeb4>:0
System.Threading.Tasks.Task.InnerInvoke()<1a384679528d44a8be7dd421c9e8c3ae>:0
System.Threading.Tasks.Task.Execute()<1a384679528d44a8be7dd421c9e8c3ae>:0

This never happened before
Comment 11 Jonathan Pryor 2017-03-23 13:01:04 UTC
I believe that this was fixed in:

https://github.com/xamarin/xamarin-android/commit/0c3597869bc4493895e755bda8a26f778e4fe9e0

The fix is part of the Xamarin.Android 7.2 release, currently in the beta channel.

Note You need to log in before you can comment on or make changes to this bug.