using System.Diagnostics;
using System.IO;
using System.Net.Http.Headers;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
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);
}
}
{
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<HttpResponseMessage> FinishSendAsyncBuffered(
- Task<HttpResponseMessage> sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts)
+ Task<HttpResponseMessage> sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime)
{
HttpResponseMessage response = null;
try
catch (Exception e)
{
response?.Dispose();
+
+ if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime))
+ {
+ HandleSendAsyncTimeout(operationException);
+ throw CreateTimeoutException(operationException);
+ }
+
HandleFinishSendAsyncError(e, cts);
throw;
}
}
private async Task<HttpResponseMessage> FinishSendAsyncUnbuffered(
- Task<HttpResponseMessage> sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts)
+ Task<HttpResponseMessage> sendTask, HttpRequestMessage request, CancellationTokenSource cts, bool disposeCts, CancellationToken callerToken, long timeoutTime)
{
try
{
}
catch (Exception e)
{
+ if (e is OperationCanceledException operationException && TimeoutFired(callerToken, timeoutTime))
+ {
+ HandleSendAsyncTimeout(operationException);
+ throw CreateTimeoutException(operationException);
+ }
+
HandleFinishSendAsyncError(e, cts);
throw;
}
}
}
+ 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);
}
}
+ 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
}
}
- [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<HttpResponseMessage>(c))))
{
client.Timeout = TimeSpan.FromMilliseconds(1);
- Task<HttpResponseMessage>[] tasks = Enumerable.Range(0, 3).Select(_ => client.GetAsync(CreateFakeUri())).ToArray();
- Assert.All(tasks, task => Assert.Throws<TaskCanceledException>(() => task.GetAwaiter().GetResult()));
+ Task<HttpResponseMessage>[] tasks = Enumerable.Range(0, 3).Select(_ => client.GetAsync(CreateFakeUri(), completionOption)).ToArray();
+ Assert.All(tasks, task => {
+ OperationCanceledException e = Assert.ThrowsAny<OperationCanceledException>(() => 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<HttpResponseMessage>(c))))
+ {
+ client.Timeout = TimeSpan.FromMilliseconds(0.01);
+ CancellationTokenSource cts = new CancellationTokenSource();
+ CancellationToken token = cts.Token;
+ cts.Cancel();
+ Task<HttpResponseMessage> task = client.GetAsync(CreateFakeUri(), completionOption, token);
+ OperationCanceledException e = await Assert.ThrowsAnyAsync<OperationCanceledException>(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<HttpResponseMessage>(c))))
+ {
+ client.Timeout = TimeSpan.FromDays(1);
+ CancellationTokenSource cts = new CancellationTokenSource();
+ Task<HttpResponseMessage> task = client.GetAsync(CreateFakeUri(), completionOption, cts.Token);
+ cts.Cancel();
+ OperationCanceledException e = Assert.ThrowsAny<OperationCanceledException>(() => task.GetAwaiter().GetResult());
+ Assert.Null(e.InnerException);
}
}