9

I have my web requests handled by this code;

Response = await Client.SendAsync(Message, HttpCompletionOption.ResponseHeadersRead, CToken);

That returns after the response headers are read and before the content is finished reading. When I call this line to get the content...

return await Response.Content.ReadAsStringAsync();

I want to be able to stop it after X seconds. But it doesn't accept a cancellation token.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
iguanaman
  • 930
  • 3
  • 13
  • 25
  • 1
    FYI, if you're dealing with timeouts on IDisposable you probably want to look at this: [Async network operations never finish](http://stackoverflow.com/a/21468138/885318) – i3arnon Sep 23 '14 at 06:09
  • Does this answer your question? [Asynchronously wait for Task to complete with timeout](https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout) – Edward Brey Jun 16 '21 at 20:27

3 Answers3

15

While you can rely on WithCancellation for reuse purposes, a simpler solution for a timeout (which doesn't throw OperationCanceledException) would be to create a timeout task with Task.Delay and wait for the first task to complete using Task.WhenAny:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();
}

Or, if you want to throw an exception in case there's a timeout instead of just returning the default value (i.e. null):

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (task == await Task.WhenAny(task, Task.Delay(timeout)))
    {
        return await task;
    }
    throw new TimeoutException();
}

And the usage would be:

var content = await Response.Content.ReadAsStringAsync().WithTimeout(TimeSpan.FromSeconds(1));
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 3
    Whilst NedStoyanov's answer is also great this one specifically provides code to handle a timeout rather than just allowing a cancellation. I feel this one answers the question better. – iguanaman Sep 24 '14 at 09:54
  • But is there any guarantee that the Delay task won't be scheduled before the task? There could be nothing wrong with the task but the task scheduler could still run the Delay first right (if all task capacity is filled it has to choose)? – David S. Feb 01 '17 at 10:43
  • @DavidS. `Task.Delay` isn't really scheduled as it doesn't really "run". It just starts a timer that pops after the timeout and completes the task. Also, the task you get doesn't need to be "started".. it's already hot, so there's no real worry here (unless in very extreme cases). – i3arnon Feb 01 '17 at 11:08
3

Have a look at How do I cancel non-cancelable async operations?. If you just want the await to finish while the request continues in the background you can use the author's WithCancellation extension method. Here it is reproduced from the article:

public static async Task<T> WithCancellation<T>( 
    this Task<T> task, CancellationToken cancellationToken) 
{ 
    var tcs = new TaskCompletionSource<bool>(); 
    using(cancellationToken.Register( 
                s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs)) 
        if (task != await Task.WhenAny(task, tcs.Task)) 
            throw new OperationCanceledException(cancellationToken); 
    return await task; 
}

It essentially combines the original task with a task that accepts a cancellation token and then awaits both tasks using Task.WhenAny. So when you cancel the CancellationToken the secodn task gets cancelled but the original one keeps going. As long as you don't care about that you can use this method.

You can use it like this:

return await Response.Content.ReadAsStringAsync().WithCancellation(token);

Update

You can also try to dispose of the Response as part of the cancellation.

token.Register(Reponse.Content.Dispose);
return await Response.Content.ReadAsStringAsync().WithCancellation(token);

Now as you cancel the token, the Content object will be disposed.

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • That's great thanks, there's no way to stop a ReadAsStringAsync in progress then? It's a great solution but ideally I'd like to abort the ReadAsStringAsync if at all possible. – iguanaman Sep 23 '14 at 02:17
  • You can use `Thread.Abort` as in this answer http://stackoverflow.com/a/21715122/1239433, but you may have to call a synchronous version of the function inside `Task.Run` in order to do it. – NeddySpaghetti Sep 23 '14 at 03:13
  • 2
    @iguanaman: Traditionally, Microsoft APIs will cancel their I/O operations if the underlying handle is closed. So, you could try disposing the `Response.Content` and/or the `Response` when the cancel token is signalled. – Stephen Cleary Sep 23 '14 at 11:11
-3

Since returns a task, you can Wait for the Task which essentially is equivalent to specifying a timeout:

// grab the task object
var reader = response.Content.ReadAsStringAsync();

// so you're telling the reader to finish in X milliseconds
var timedOut = reader.Wait(X);  

if (timedOut)
{
    // handle timeouts
}
else
{
    return reader.Result;
}
Mrchief
  • 75,126
  • 20
  • 142
  • 189