WebClient.CancelAsync
seems to be bugged in that it won't cancel an in-progress download made using (as far as I can tell) any of the Async methods of WebClient
. (I've tested DownloadFileAsync
, DownloadFileTaskAsync
, and DownloadDataTaskAsync
.)
Here is some sample code that reproduces the issue:
```c#
using System;
using System.Net;
using System.Threading.Tasks;
namespace CancelAsyncTest
{
class Program
{
static TaskCompletionSource
static async Task Main()
{
WebClient client = new WebClient();
client.DownloadProgressChanged += DownloadProgressChanged;
Task downloadTask = client.DownloadDataTaskAsync("https://file-examples.com/wp-content/uploads/2017/04/file_example_MP4_1920_18MG.mp4");
await progressSource.Task; // Canceling before we've had any download progress works fine.
Console.WriteLine("Attempting to cancel...");
client.CancelAsync();
try
{
await downloadTask;
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
}
private static void DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
Console.WriteLine($"{e.ProgressPercentage}%");
if(!progressSource.Task.IsCompleted)
progressSource.SetResult(null);
}
}
}
```
I would expect the above code to throw an exception at await downloadTask
almost immediately after the first DownloadProgressChanged
event. This indeed behaves as expected when running this code on .NET Framework 4.7.2, but when running the identical code on .NET Core 3.0, the exception only gets thrown after the file is 100% downloaded.
Note that if CancelAsync
is called immediately after DownloadDataTaskAsync
then it seems to work fine and is canceled immediately. But if CancelAsync
is called after the download has made any progress, then it doesn't cancel until the download is 100% completed. (That's why I have await progressSource.Task;
in the above code; without it, it works fine.)
Please let me know if there's any more information I can provide. Thanks!
Triage: We consider WebClient
legacy API. We recommend moving to HttpClient
instead for better perf, reliability and feature richness.
@karelz
Triage: We consider
WebClient
legacy API. We recommend moving toHttpClient
instead for better perf, reliability and feature richness.
That's disappointing to hear, considering this is a regression from .NET Framework that breaks existing code. Also, despite the claim of "feature richness", HttpClient does lack some features compared WebClient; for example, it lacks a convenient way to track download progress.
@karelz Please reconsider - as @waltdestler stated, using WebClient
is better in the case where download progress needs to be tracked. In all other cases, I agree that HttpClient
is better... but not this one.馃槈
@EatonZ If it helps, I wrote an async wrapper method around HttpClient that can be used to download a file, can track progress via a callback, can be canceled via a CancellationToken, and can handle local files (which HttpClient can't):
```C#
///
/// Downloads a file from the specified Uri into the specified stream.
///
/// An optional CancellationToken that can be used to cancel the in-progress download.
/// If not null, will be called as the download progress. The first parameter will be the number of bytes downloaded so far, and the second the total size of the expected file after download.
///
public static async Task DownloadFileAsync(Uri uri, Stream toStream, CancellationToken cancellationToken = default, Action
{
if(uri == null)
throw new ArgumentNullException(nameof(uri));
if(toStream == null)
throw new ArgumentNullException(nameof(toStream));
if(uri.IsFile)
{
await using Stream file = File.OpenRead(uri.LocalPath);
if(progressCallback != null)
{
long length = file.Length;
byte[] buffer = new byte[4096];
int read;
int totalRead = 0;
while((read = await file.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
{
await toStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
totalRead += read;
progressCallback(totalRead, length);
}
Debug.Assert(totalRead == length || length == -1);
}
else
{
await file.CopyToAsync(toStream, cancellationToken).ConfigureAwait(false);
}
}
else
{
using HttpClient client = new HttpClient();
using HttpResponseMessage response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
if(progressCallback != null)
{
long length = response.Content.Headers.ContentLength ?? -1;
await using Stream stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
byte[] buffer = new byte[4096];
int read;
int totalRead = 0;
while((read = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0)
{
await toStream.WriteAsync(buffer, 0, read, cancellationToken).ConfigureAwait(false);
totalRead += read;
progressCallback(totalRead, length);
}
Debug.Assert(totalRead == length || length == -1);
}
else
{
await response.Content.CopyToAsync(toStream).ConfigureAwait(false);
}
}
}
```
(Note that this creates a new HttpClient object on every call, which isn't ideal if you make a lot of downloads. In that case, you might want to use a global HttpClient object that lives for the duration of your program.)
Hope that helps!
@waltdestler Thanks for sharing that. Stuff like this should be built in by default!
If WebClient
is "legacy" and that is the reason you won't fix this buggy behavior, then remove the CancelASync
method from it so that people don't waste hours trying to fix something that is dead on arrivial.
Leaving this unfixed for years because it's 'legacy' is not an acceptable excuse.
Most helpful comment
@EatonZ If it helps, I wrote an async wrapper method around HttpClient that can be used to download a file, can track progress via a callback, can be canceled via a CancellationToken, and can handle local files (which HttpClient can't):
```C#A task that is completed once the download is complete. progressCallback = null)
///
/// Downloads a file from the specified Uri into the specified stream.
///
/// An optional CancellationToken that can be used to cancel the in-progress download.
/// If not null, will be called as the download progress. The first parameter will be the number of bytes downloaded so far, and the second the total size of the expected file after download.
///
public static async Task DownloadFileAsync(Uri uri, Stream toStream, CancellationToken cancellationToken = default, Action
{
if(uri == null)
throw new ArgumentNullException(nameof(uri));
if(toStream == null)
throw new ArgumentNullException(nameof(toStream));
```
(Note that this creates a new HttpClient object on every call, which isn't ideal if you make a lot of downloads. In that case, you might want to use a global HttpClient object that lives for the duration of your program.)
Hope that helps!