Search code examples
c#dotnet-httpclientaggregateexception

HttpClient async methods, aggregate exceptions


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?


Solution

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