Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why no error notification for UploadFileAsync with WebClient?

When I execute the following code:

public static async Task UploadFile(string serverPath, string pathToFile, string authToken)
{
    serverPath = @"C:\_Series\S1\The 100 S01E03.mp4";
    var client = new WebClient();
    var uri = new Uri($"http://localhost:50424/api/File/Upload?serverPath={WebUtility.UrlEncode(serverPath)}");
    client.UploadProgressChanged += UploadProgressChanged;
    client.UploadFileCompleted += UploadCompletedCallback;
    //client.UploadFileAsync(uri, "POST", pathToFile);
    client.UploadFile(uri, "POST", pathToFile);
}

I get the exception:

System.Net.WebException: 'The remote server returned an error: (404) Not Found.'

I'm not too worried about the 404, I'm busy tracing down why the WebClient can't find it, but my big concern is that if I call UploadFileAsync with the same uri, the method just executes as if nothing is wrong.

The only indication that something is wrong is that neither of the two event handlers is invoked. I strongly suspect that I don't get an exception because the async call is not async/await but event based, but then I would expect some kind of event or property that indicates an exception has occurred.

How is one supposed to use code that hides errors like this, especially network errors which are relatively more common, in production?

like image 713
ProfK Avatar asked Oct 12 '25 23:10

ProfK


1 Answers

Why no error notification for UploadFileAsync with WebClient?

Citing WebClient.UploadFileAsync Method (Uri, String, String) Remarks

The file is sent asynchronously using thread resources that are automatically allocated from the thread pool. To receive notification when the file upload completes, add an event handler to the UploadFileCompleted event.

Emphasis mine.

You get no errors because it is being executed on another thread so as not to block the current thread. To see the error you can access it in the stated event handler via the UploadFileCompletedEventArgs.Exception.

I was curious as to why using WebClient and not HttpClient which is already primarily async, but then my assumption was because of the upload progress.

I would suggest wrapping the WebClient call with event handlers in a Task using a TaskCompletionSource to take advantage of TAP.

The following is similar to the examples provided here How to: Wrap EAP Patterns in a Task

public static async Task UploadFileAsync(string serverPath, string pathToFile, string authToken, IProgress<int> progress = null) {
    serverPath = @"C:\_Series\S1\The 100 S01E03.mp4";
    using (var client = new WebClient()) {
        // Wrap Event-Based Asynchronous Pattern (EAP) operations 
        // as one task by using a TaskCompletionSource<TResult>.
        var task = client.createUploadFileTask(progress);

        var uri = new Uri($"http://localhost:50424/api/File/Upload?serverPath={WebUtility.UrlEncode(serverPath)}");
        client.UploadFileAsync(uri, "POST", pathToFile);
        //wait here while the file uploads
        await task;
    }
}

Where createUploadFileTask is a custom extension method used to wrap the Event-Based Asynchronous Pattern (EAP) operations of the WebClient as one task by using a TaskCompletionSource<TResult>.

private static Task createTask(this WebClient client, IProgress<int> progress = null) {
    var tcs = new TaskCompletionSource<object>();
    #region callbacks
    // Specifiy the callback for UploadProgressChanged event
    // so it can be tracked and relayed through `IProgress<T>`
    // if one is provided
    UploadProgressChangedEventHandler uploadProgressChanged = null;
    if (progress != null) {
        uploadProgressChanged = (sender, args) => progress.Report(args.ProgressPercentage);
        client.UploadProgressChanged += uploadProgressChanged;
    }
    // Specify the callback for the UploadFileCompleted
    // event that will be raised by this WebClient instance.
    UploadFileCompletedEventHandler uploadCompletedCallback = null;
    uploadCompletedCallback = (sender, args) => {
        // unsubscribing from events after asynchronous
        // events have completed
        client.UploadFileCompleted -= uploadCompletedCallback;
        if (progress != null)
            client.UploadProgressChanged -= uploadProgressChanged;

        if (args.Cancelled) {
            tcs.TrySetCanceled();
            return;
        } else if (args.Error != null) {
            // Pass through to the underlying Task
            // any exceptions thrown by the WebClient
            // during the asynchronous operation.
            tcs.TrySetException(args.Error);
            return;
        } else
            //since no result object is actually expected
            //just set it to null to allow task to complete
            tcs.TrySetResult(null);
    };
    client.UploadFileCompleted += uploadCompletedCallback;
    #endregion

    // Return the underlying Task. The client code
    // waits on the task to complete, and handles exceptions
    // in the try-catch block there.
    return tcs.Task;
}

Going one step further and creating another extension method to wrap the upload file to make it await able...

public static Task PostFileAsync(this WebClient client, Uri address, string fileName, IProgress<int> progress = null) {
    var task = client.createUploadFileTask(progress);
    client.UploadFileAsync(address, "POST", fileName);//this method does not block the calling thread.
    return task;
}

Allowed your UploadFile to be refactored to

public static async Task UploadFileAsync(string serverPath, string pathToFile, string authToken, IProgress<int> progress = null) {
    using (var client = new WebClient()) {
        var uri = new Uri($"http://localhost:50424/api/File/Upload?serverPath={WebUtility.UrlEncode(serverPath)}");
        await client.PostFileAsync(uri, pathToFile, progress);
    }
}

This now allow you to call the upload asynchronously and even keep track of the progress with your very own Progress Reporting (Optional)

For example if in an XAML based platform

public class UploadProgressViewModel : INotifyPropertyChanged, IProgress<int> {

    public int Percentage {
        get {
            //...return value
        }
        set {
            //...set value and notify change
        }
    }

    public void Report(int value) {
        Percentage = value;
    }
}

Or using the out of the box Progress<T> Class

So now you should be able to upload the file without blocking the thread and still be able to await it, get progress notifications, and handle exceptions, provided you have a try/catch in place.

like image 199
Nkosi Avatar answered Oct 14 '25 13:10

Nkosi



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!