HttpClient async methods, aggregate exceptions

3.2k views Asked by At

HttpClient async methods (e.g. PostAsync, GetAsync etc.), if an exception is thrown, returns an AggregateException. Now an aggregate exception can contain a list of inner exceptions. My question is can someone provide an example where one of the async methods on an http client leads to more than one inner exception?

Is it safe to assume that although there is potentially a list of inner exceptions, in most cases you will only get one inner exception?

I'm aware of why they are being throw and how to handle it.

So to make this clearer, is it possible for a single call to an http client async method that throws an aggregate exception to have more than one exception in it's list of inner exceptions?

2

There are 2 answers

0
Yuval Itzchakov On BEST ANSWER

If you look inside HttpClient.SendAsync (which is the inner method being used to send all requests), you'll see that the Task being created is a simple TaskCompletionSource<HttpResponseMessage>. Inside the calling method, it sets this.SetTaskFaulted multiple times, but always inside an if-else block. What could potentially happen is that when SetTaskFaulted sets the exception, it could set off another exception:

private void SetTaskFaulted(HttpRequestMessage request, CancellationTokenSource cancellationTokenSource, TaskCompletionSource<HttpResponseMessage> tcs, Exception e, TimerThread.Timer timeoutTimer)
{
    this.LogSendError(request, cancellationTokenSource, "SendAsync", e);
    tcs.TrySetException(e);
    HttpClient.DisposeCancellationTokenAndTimer(cancellationTokenSource, timeoutTimer);
}

DisposeCancellationTokenAndTimer internally disposes the CancellationToken, and inside the finally block disposes the timer:

private static void DisposeCancellationTokenAndTimer(CancellationTokenSource cancellationTokenSource, TimerThread.Timer timeoutTimer)
{
    try
    {
        cancellationTokenSource.Dispose();
    }
    catch (ObjectDisposedException)
    {
    }
    finally
    {
        HttpClient.DisposeTimer(timeoutTimer);
    }
}

The timer could potentially throw an exception from its Dispose method. Although im sure that's very rare.

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken)
{
    if (request == null)
    {
        throw new ArgumentNullException("request");
    }
    this.CheckDisposed();
    HttpClient.CheckRequestMessage(request);
    this.SetOperationStarted();
    this.PrepareRequestMessage(request);
    CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, this.pendingRequestsCts.Token);
    TimerThread.Timer timeoutTimer = this.SetTimeout(linkedCts);
    TaskCompletionSource<HttpResponseMessage> tcs = new TaskCompletionSource<HttpResponseMessage>();
    try
    {
        base.SendAsync(request, linkedCts.Token).ContinueWithStandard(delegate(Task<HttpResponseMessage> task)
        {
            try
            {
                this.DisposeRequestContent(request);
                if (task.IsFaulted)
                {
                    this.SetTaskFaulted(request, linkedCts, tcs, task.Exception.GetBaseException(), timeoutTimer);
                }
                else
                {
                    if (task.IsCanceled)
                    {
                        this.SetTaskCanceled(request, linkedCts, tcs, timeoutTimer);
                    }
                    else
                    {
                        HttpResponseMessage result = task.Result;
                        if (result == null)
                        {
                            this.SetTaskFaulted(request, linkedCts, tcs, new InvalidOperationException(SR.net_http_handler_noresponse), timeoutTimer);
                        }
                        else
                        {
                            if (result.Content == null || completionOption == HttpCompletionOption.ResponseHeadersRead)
                            {
                                this.SetTaskCompleted(request, linkedCts, tcs, result, timeoutTimer);
                            }
                            else
                            {
                                this.StartContentBuffering(request, linkedCts, tcs, result, timeoutTimer);
                            }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                if (Logging.On)
                {
                    Logging.Exception(Logging.Http, this, "SendAsync", ex);
                }
                tcs.TrySetException(ex);
            }
        });
    }
    catch
    {
        HttpClient.DisposeTimer(timeoutTimer);
        throw;
    }
    return tcs.Task;
0
Steve Lillis On

If you await Task.WhenAll(), and multiple tasks in that WhenAll throw exceptions, their exceptions will be aggregated.

The TPL throwing an aggregate exception like this is by design, so that exceptions for one task are not lost just because exceptions happened in another.

example:

var failTask1 = Task.Run(() => { throw new Exception("Example 1"); });
var failTask2 = Task.Run(() => { throw new Exception("Another example"); });
await Task.WhenAll(failTask1, failTask2);