22

It seems that GetResponseAsync does not accept cancellationToken in Async/Await. So the question is how can I cancel the below procedure, provided I need to collect Cookies from response:

 using (HttpWebResponse response = (HttpWebResponse) await request.GetResponseAsync())
 {
    cookies.Add(response.Cookies);
 }

An alternative code to achieve the above is also welcome.

Jakob Möllås
  • 4,239
  • 3
  • 33
  • 61
Jim
  • 2,760
  • 8
  • 42
  • 66
  • 1
    Have you tried calling `Abort()`? – svick Oct 06 '13 at 18:35
  • I've seen it in http://stackoverflow.com/questions/8635723/cancel-an-async-webrequest but not sure how to implement it correctly. I am passing the CancellationToken ct to the method. – Jim Oct 06 '13 at 18:46
  • Why don't you use `HttpClient`? – Paulo Morgado Oct 07 '13 at 07:26
  • Well it seems http://stackoverflow.com/questions/12373738/how-do-i-set-a-cookie-on-httpclients-httprequestmessage that it requires more code to write... Anyway why would you prefer HttpClient instead? – Jim Oct 07 '13 at 07:31

2 Answers2

40

Something like this should work (untested):

public static class Extensions
{
    public static async Task<HttpWebResponse> GetResponseAsync(this HttpWebRequest request, CancellationToken ct)
    {
        using (ct.Register(() => request.Abort(), useSynchronizationContext: false))
        {
            var response = await request.GetResponseAsync();
            ct.ThrowIfCancellationRequested();
            return (HttpWebResponse)response;
        }
    }
}

In theory, if cancellation is requested on ct and request.Abort is invoked, await request.GetResponseAsync() should throw a WebException. IMO though, it's always a good idea to check for cancellation explicitly when consuming the result, to mitigate race conditions, so I call ct.ThrowIfCancellationRequested().

Also, I assume that request.Abort is thread-safe (can be called from any thread), so I use useSynchronizationContext: false (I haven't verified that).

[UPDATED] to address the OP's comment on how to differentiate between WebException caused by cancellation and any other error. This is how it can be done, so TaskCanceledException (derived from OperationCanceledException) will be correctly thrown upon cancellation:

public static class Extensions
{
    public static async Task<HttpWebResponse> GetResponseAsync(this HttpWebRequest request, CancellationToken ct)
    {
        using (ct.Register(() => request.Abort(), useSynchronizationContext: false))
        {
            try
            {
                var response = await request.GetResponseAsync();
                return (HttpWebResponse)response;
            }
            catch (WebException ex)
            {
                // WebException is thrown when request.Abort() is called,
                // but there may be many other reasons,
                // propagate the WebException to the caller correctly
                if (ct.IsCancellationRequested)
                {
                    // the WebException will be available as Exception.InnerException
                    throw new OperationCanceledException(ex.Message, ex, ct);
                }

                // cancellation hasn't been requested, rethrow the original WebException
                throw;
            }
        }
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Couldn't this could be simplified further to ` public static async Task GetResponseAsync( this HttpWebRequest request, CancellationToken ct) { using (ct.Register(request.Abort, false)) { try { return (HttpWebResponse)await request.GetResponseAsync(); } catch (Exception) { ct.ThrowIfCancellationRequested(); throw; } } }` – Joseph Lennox Jun 17 '14 at 23:25
  • 1
    @JosephLennox, you can do so, but you'd loose access to `WebException` in the client code, which is currently available as `Exception.InnerException` when cancellation is requested. – noseratio Jun 17 '14 at 23:44
  • 1
    In Case this code reaches ct.ThrowIfCancellationRequested(); i think var response should be closed. – zirbel Nov 26 '15 at 16:13
  • @zirbel, you're right, `ThrowIfCancellationRequested` was racing there with a successful completion of `HttpWebRequest.GetResponseAsync`. I removed the former, leaving it up to the caller to deal with this race condition (which still was there anyway, for when `Extensions.GetResponseAsync` possibly completes at the same time the token gets cancelled). – noseratio Nov 26 '15 at 23:12
10
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken, Action action, bool useSynchronizationContext = true)
{
    using (cancellationToken.Register(action, useSynchronizationContext))
    {
        try
        {
            return await task;
        }
        catch (Exception ex)
        {

            if (cancellationToken.IsCancellationRequested)
            {
                // the Exception will be available as Exception.InnerException
                throw new OperationCanceledException(ex.Message, ex, cancellationToken);
            }

            // cancellation hasn't been requested, rethrow the original Exception
            throw;
        }
    }
}

Now you can use your cancellation token on any cancelable async method. For example WebRequest.GetResponseAsync:

var request = (HttpWebRequest)WebRequest.Create(url);

using (var response = await request.GetResponseAsync())
{
    . . .
}

will become:

var request = (HttpWebRequest)WebRequest.Create(url);

using (WebResponse response = await request.GetResponseAsync().WithCancellation(CancellationToken.None, request.Abort, true))
{
    . . .
}

See example http://pastebin.com/KauKE0rW

Marat Asadurian
  • 593
  • 1
  • 6
  • 18