From: Alexander Nikolaev <55398552+alnikola@users.noreply.github.com>
Date: Wed, 19 Feb 2020 11:31:05 +0000 (+0100)
Subject: HttpClient throws TimeoutException wrapped by TaskCancellationException when request...
X-Git-Tag: submit/tizen/20210909.063632~9632
X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=7818335aabed0e7acb843b0c4be1b86f807cbc3c;p=platform%2Fupstream%2Fdotnet%2Fruntime.git
HttpClient throws TimeoutException wrapped by TaskCancellationException when request times out (#2281)
Currently, HttpClient throws the same TaskCancellationException regardless of the request cancellation reason that can be caller's cancellation, all pending request cancellation or timeout. This makes it impossible to handle a request timeout in a way different from all other cases (e.g. special retry logic).
This PR adds a timeout detection logic into HttpClient. It watches for all TaskCancelledExceptions and catches the one caused by timeout. Then, it creates two new exceptions and build a hierarchy. The first is a TimeoutException having its InnerException set to the original TaskCancelledException. The second is a new TaskCancelledException having its InnerException set to that new TimeoutException, but preserving the original stack trace, message and cancellation token. Finally, this top-level TaskCancelledException gets thrown.
Fixes #21965
---
diff --git a/src/libraries/System.Net.Http/src/Resources/Strings.resx b/src/libraries/System.Net.Http/src/Resources/Strings.resx
index 94beb1f..456e14c 100644
--- a/src/libraries/System.Net.Http/src/Resources/Strings.resx
+++ b/src/libraries/System.Net.Http/src/Resources/Strings.resx
@@ -537,4 +537,7 @@
The HTTP/3 server attempted to reference a dynamic table index that does not exist.
-
+
+ The request was canceled due to the configured HttpClient.Timeout of {0} seconds elapsing.
+
+
\ No newline at end of file
diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
index 05288da..ba716da 100644
--- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
+++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpClient.cs
@@ -5,6 +5,7 @@
using System.Diagnostics;
using System.IO;
using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
@@ -491,12 +492,14 @@ namespace System.Net.Http
CancellationTokenSource cts;
bool disposeCts;
bool hasTimeout = _timeout != s_infiniteTimeout;
+ long timeoutTime = long.MaxValue;
if (hasTimeout || cancellationToken.CanBeCanceled)
{
disposeCts = true;
cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _pendingRequestsCts.Token);
if (hasTimeout)
{
+ timeoutTime = Environment.TickCount64 + (_timeout.Ticks / TimeSpan.TicksPerMillisecond);
cts.CancelAfter(_timeout);
}
}
@@ -512,19 +515,25 @@ namespace System.Net.Http
{
sendTask = base.SendAsync(request, cts.Token);
}
- catch
+ catch (Exception e)
{
HandleFinishSendAsyncCleanup(cts, disposeCts);
+
+ if (e is OperationCanceledException operationException && TimeoutFired(cancellationToken, timeoutTime))
+ {
+ throw CreateTimeoutException(operationException);
+ }
+
throw;
}
return completionOption == HttpCompletionOption.ResponseContentRead && !string.Equals(request.Method.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ?
- FinishSendAsyncBuffered(sendTask, request, cts, disposeCts) :
- FinishSendAsyncUnbuffered(sendTask, request, cts, disposeCts);
+ FinishSendAsyncBuffered(sendTask, request, cts, disposeCts, cancellationToken, timeoutTime) :
+ FinishSendAsyncUnbuffered(sendTask, request, cts, disposeCts, cancellationToken, timeoutTime);
}
private async Task FinishSendAsyncBuffered(
- Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts)
+ Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime)
{
HttpResponseMessage response = null;
try
@@ -548,6 +557,13 @@ namespace System.Net.Http
catch (Exception e)
{
response?.Dispose();
+
+ if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime))
+ {
+ HandleSendAsyncTimeout(operationException);
+ throw CreateTimeoutException(operationException);
+ }
+
HandleFinishSendAsyncError(e, cts);
throw;
}
@@ -558,7 +574,7 @@ namespace System.Net.Http
}
private async Task FinishSendAsyncUnbuffered(
- Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts)
+ Task sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime)
{
try
{
@@ -573,6 +589,12 @@ namespace System.Net.Http
}
catch (Exception e)
{
+ if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime))
+ {
+ HandleSendAsyncTimeout(operationException);
+ throw CreateTimeoutException(operationException);
+ }
+
HandleFinishSendAsyncError(e, cts);
throw;
}
@@ -582,6 +604,14 @@ namespace System.Net.Http
}
}
+ private bool TimeoutFired(CancellationToken callerToken, long timeoutTime) => !callerToken.IsCancellationRequested && Environment.TickCount64 >= timeoutTime;
+
+ private TaskCanceledException CreateTimeoutException(OperationCanceledException originalException)
+ {
+ return new TaskCanceledException(string.Format(SR.net_http_request_timedout, _timeout.TotalSeconds),
+ new TimeoutException(originalException.Message, originalException), originalException.CancellationToken);
+ }
+
private void HandleFinishSendAsyncError(Exception e, CancellationTokenSource cts)
{
if (NetEventSource.IsEnabled) NetEventSource.Error(this, e);
@@ -595,6 +625,15 @@ namespace System.Net.Http
}
}
+ private void HandleSendAsyncTimeout(OperationCanceledException e)
+ {
+ if (NetEventSource.IsEnabled)
+ {
+ NetEventSource.Error(this, e);
+ NetEventSource.Error(this, "Canceled due to timeout");
+ }
+ }
+
private void HandleFinishSendAsyncCleanup(CancellationTokenSource cts, bool disposeCts)
{
// Dispose of the CancellationTokenSource if it was created specially for this request
diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs
index 83efb46..ad642fc 100644
--- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs
+++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientTest.cs
@@ -593,14 +593,54 @@ namespace System.Net.Http.Functional.Tests
}
}
- [Fact]
- public void Timeout_TooShort_AllPendingOperationsCanceled()
+ [Theory]
+ [InlineData(HttpCompletionOption.ResponseContentRead)]
+ [InlineData(HttpCompletionOption.ResponseHeadersRead)]
+ public void Timeout_TooShort_AllPendingOperationsCanceled(HttpCompletionOption completionOption)
{
using (var client = new HttpClient(new CustomResponseHandler((r, c) => WhenCanceled(c))))
{
client.Timeout = TimeSpan.FromMilliseconds(1);
- Task[] tasks = Enumerable.Range(0, 3).Select(_ => client.GetAsync(CreateFakeUri())).ToArray();
- Assert.All(tasks, task => Assert.Throws(() => task.GetAwaiter().GetResult()));
+ Task[] tasks = Enumerable.Range(0, 3).Select(_ => client.GetAsync(CreateFakeUri(), completionOption)).ToArray();
+ Assert.All(tasks, task => {
+ OperationCanceledException e = Assert.ThrowsAny(() => task.GetAwaiter().GetResult());
+ TimeoutException timeoutException = (TimeoutException)e.InnerException;
+ Assert.NotNull(timeoutException);
+ Assert.NotNull(timeoutException.InnerException);
+ });
+ }
+ }
+
+ [Theory]
+ [InlineData(HttpCompletionOption.ResponseContentRead)]
+ [InlineData(HttpCompletionOption.ResponseHeadersRead)]
+ public async Task Timeout_CallerCanceledTokenAfterTimeout_TimeoutIsNotDetected(HttpCompletionOption completionOption)
+ {
+ using (var client = new HttpClient(new CustomResponseHandler((r, c) => WhenCanceled(c))))
+ {
+ client.Timeout = TimeSpan.FromMilliseconds(0.01);
+ CancellationTokenSource cts = new CancellationTokenSource();
+ CancellationToken token = cts.Token;
+ cts.Cancel();
+ Task task = client.GetAsync(CreateFakeUri(), completionOption, token);
+ OperationCanceledException e = await Assert.ThrowsAnyAsync(async () => await task);
+ Assert.Null(e.InnerException);
+ }
+ }
+
+ [Theory]
+ [InlineData(HttpCompletionOption.ResponseContentRead)]
+ [InlineData(HttpCompletionOption.ResponseHeadersRead)]
+ public void Timeout_CallerCanceledTokenBeforeTimeout_TimeoutIsNotDetected(HttpCompletionOption completionOption)
+ {
+ using (var client = new HttpClient(new CustomResponseHandler((r, c) => WhenCanceled(c))))
+ {
+ client.Timeout = TimeSpan.FromDays(1);
+ CancellationTokenSource cts = new CancellationTokenSource();
+ Task task = client.GetAsync(CreateFakeUri(), completionOption, cts.Token);
+ cts.Cancel();
+ OperationCanceledException e = Assert.ThrowsAny(() => task.GetAwaiter().GetResult());
+ Assert.Null(e.InnerException);
}
}