11

I'm using HttpClient.PostAsync() and the response is an HttpResponseMessage. Its Content property is of type HttpContent which has a CopyToAsync() method. Unfortunately, this is not cancelable. Is there a way to get the response copied into a Stream and pass a CancellationToken?

I am not stuck with CopyToAsync()! If there is a workaround, that would be fine. Like read a couple of bytes, check if canceled, continue reading and so on.

The HttpContent.CreateContentReadStreamAsync() methods looks like it would be a candidate. Unfortunately, it's not available with my selected profile. Also unclear if it would read all data in one go and waste a lot of memory.

Note: I'm using this inside a PCL targeting WP8, Windows Store 8, .NET 4.5, Xamarin.iOS and Xamarin.Android

Krumelur
  • 32,180
  • 27
  • 124
  • 263

3 Answers3

19

I believe this should work:

public static async Task DownloadToStreamAsync(string uri, HttpContent data, Stream target, CancellationToken token)
{
    using (var client = new HttpClient())
    using (var response = await client.PostAsync(uri, data, token))
    using (var stream = await response.Content.ReadAsStreamAsync())
    {
        await stream.CopyToAsync(target, 4096, token);
    }
}

Note that ReadAsStreamAsync calls CreateContentReadStreamAsync, which for stream responses just returns the underlying content stream without buffering it into memory.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 5
    I have used a variation of this to ensure the response content is streamed, using SendAsync instead of PostAsync. With SendAsync it was necessary to use the HttpCompletionOption.ResponseHeadersRead argument, otherwise the SendAsync call did not return until the entire response was read, including all the content (MB, GB, etc) - i.e. not desirable as all data is read into memory (can be seen in Task Manager memory used). Using HttpCompletionOption.ResponseHeadersRead means the data can be properly streamed. I am not sure which way PostAsync does it, but hopefully this info helps others. – cbailiss Nov 09 '14 at 23:39
  • `CopyToAsync` on stream returned from `httpClient.GetStreamAsync` or `Content.ReadAsStreamAsync` just ignores token. [Details](https://stackoverflow.com/a/12431633/2336304) – SerG Mar 13 '19 at 16:04
2

You can't cancel a non cancellable operation. See How do I cancel non-cancelable async operations?.

You can however allow your code flow to behave as if the underlying operation was canceled, with WithCancellation.

public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
    return task.IsCompleted
        ? task
        : task.ContinueWith(
            completedTask => completedTask.GetAwaiter().GetResult(),
            cancellationToken,
            TaskContinuationOptions.ExecuteSynchronously,
            TaskScheduler.Default);
}

Usage:

await httpContent.PostAsync(stream).WithCancellation(new CancellationTokenSource(1000).Token);
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • This is scary stuff. What happens if I cancel while the response is still being read? Assume the stream is going into a file. User cancels have the way then downloads the same thing again. As stated in the linked article, all kinds of things can go wrong. – Krumelur Jan 03 '14 at 12:03
  • That is very true. And probably why there isn't an overload that takes a cancellation token and WithCancellation is not a part of .Net. But as i said, you can't cancel an async operation if the developer hasn't given you a way to do so. You can only cut it off and continue on, probably catching ObjectDisposedException or sometimes NullReferenceException. – i3arnon Jan 03 '14 at 12:59
1

HttpClient.CancelPendingRequests is expected to cancel all pending operations. I haven't verified though if that applies to CopyToAsync too. Feel free to try it:

public static async Task CopyToAsync(
    System.Net.Http.HttpClient httpClient, 
    System.Net.Http.HttpContent httpContent,
    Stream stream, CancellationToken ct)
{
    using (ct.Register(() => httpClient.CancelPendingRequests()))
    {
        await httpContent.CopyToAsync(stream);
    }
}

[UPDATE] Verified: unfortunately, this doesn't cancel HttpContent.CopyToAsync. I'm keeping this answer though, as the pattern itself may be useful for cancelling other operations on HttpClient.

noseratio
  • 59,932
  • 34
  • 208
  • 486