GetResponseAsync does not accept cancellationToken

2019-01-07 23:42发布

问题:

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.

回答1:

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;
            }
        }
    }
}


回答2:

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