File: System\Net\Http\MessageProcessingHandler.cs
Web Access
Project: src\src\libraries\System.Net.Http\src\System.Net.Http.csproj (System.Net.Http)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
 
namespace System.Net.Http
{
    public abstract class MessageProcessingHandler : DelegatingHandler
    {
        protected MessageProcessingHandler()
        {
        }
 
        protected MessageProcessingHandler(HttpMessageHandler innerHandler)
            : base(innerHandler)
        {
        }
 
        protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request,
            CancellationToken cancellationToken);
        protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response,
            CancellationToken cancellationToken);
 
        protected internal sealed override HttpResponseMessage Send(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            ArgumentNullException.ThrowIfNull(request);
 
            // Since most of the SendAsync code is just Task handling, there's no reason to share the code.
            HttpRequestMessage newRequestMessage = ProcessRequest(request, cancellationToken);
            HttpResponseMessage response = base.Send(newRequestMessage, cancellationToken);
            HttpResponseMessage newResponseMessage = ProcessResponse(response, cancellationToken);
            return newResponseMessage;
        }
 
        protected internal sealed override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            ArgumentNullException.ThrowIfNull(request);
 
            // ProcessRequest() and ProcessResponse() are supposed to be fast, so we call ProcessRequest() on the same
            // thread SendAsync() was invoked to avoid context switches. However, if ProcessRequest() throws, we have
            // to catch the exception since the caller doesn't expect exceptions when calling SendAsync(): The
            // expectation is that the returned task will get faulted on errors, but the async call to SendAsync()
            // should complete.
            var tcs = new SendState(this, cancellationToken);
            try
            {
                HttpRequestMessage newRequestMessage = ProcessRequest(request, cancellationToken);
                Task<HttpResponseMessage> sendAsyncTask = base.SendAsync(newRequestMessage, cancellationToken);
 
                // We schedule a continuation task once the inner handler completes in order to trigger the response
                // processing method. ProcessResponse() is only called if the task wasn't canceled before.
                sendAsyncTask.ContinueWith(static (task, state) =>
                {
                    var sendState = (SendState)state!;
                    MessageProcessingHandler self = sendState._handler;
                    CancellationToken token = sendState._token;
 
                    if (task.IsFaulted)
                    {
                        sendState.TrySetException(task.Exception!.GetBaseException());
                        return;
                    }
 
                    if (task.IsCanceled)
                    {
                        sendState.TrySetCanceled(token);
                        return;
                    }
 
                    if (task.Result == null)
                    {
                        sendState.TrySetException(ExceptionDispatchInfo.SetCurrentStackTrace(new InvalidOperationException(SR.net_http_handler_noresponse)));
                        return;
                    }
 
                    try
                    {
                        HttpResponseMessage responseMessage = self.ProcessResponse(task.Result, token);
                        sendState.TrySetResult(responseMessage);
                    }
                    catch (OperationCanceledException e)
                    {
                        // If ProcessResponse() throws an OperationCanceledException check whether it is related to
                        // the cancellation token we received from the user. If so, cancel the Task.
                        HandleCanceledOperations(token, sendState, e);
                    }
                    catch (Exception e)
                    {
                        sendState.TrySetException(e);
                    }
                    // We don't pass the cancellation token to the continuation task, since we want to get called even
                    // if the operation was canceled: We'll set the Task returned to the user to canceled. Passing the
                    // cancellation token here would result in the continuation task to not be called at all. I.e. we
                    // would never complete the task returned to the caller of SendAsync().
                }, tcs, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
            }
            catch (OperationCanceledException e)
            {
                HandleCanceledOperations(cancellationToken, tcs, e);
            }
            catch (Exception e)
            {
                tcs.TrySetException(e);
            }
 
            return tcs.Task;
        }
 
        private static void HandleCanceledOperations(CancellationToken cancellationToken,
            TaskCompletionSource<HttpResponseMessage> tcs, OperationCanceledException e)
        {
            // Check if the exception was due to a cancellation. If so, check if the OperationCanceledException is
            // related to our CancellationToken. If it was indeed caused due to our cancellation token being
            // canceled, set the Task as canceled. Set it to faulted otherwise, since the OperationCanceledException
            // is not related to our cancellation token.
            if (cancellationToken.IsCancellationRequested && (e.CancellationToken == cancellationToken))
            {
                tcs.TrySetCanceled(cancellationToken);
            }
            else
            {
                tcs.TrySetException(e);
            }
        }
 
        // Private class used to capture the SendAsync state in
        // a closure, while simultaneously avoiding a tuple allocation.
        private sealed class SendState : TaskCompletionSource<HttpResponseMessage>
        {
            internal readonly MessageProcessingHandler _handler;
            internal readonly CancellationToken _token;
 
            public SendState(MessageProcessingHandler handler, CancellationToken token)
            {
                Debug.Assert(handler != null);
 
                _handler = handler;
                _token = token;
            }
        }
    }
}