How to most efficiently deal with asynchrony inside ASP.NET Web API HTTP Message Handlers with TaskHelpers NuGet package or C# 5.0 asynchronous language features
@ 09-08-2012
by Tugberk Ugurlu


ASP.NET Web API has a concept of Message Handlers for processing HTTP messages on both the client and server. If we take look under the hood inside the framework, we will see that Initialize method of the HttpServer instance is invoking CreatePipeline method of System.Net.Http.HttpClientFactory’s. CreatePipeline method accepts two parameters: HttpMessageHandler and IEnumerable<DelegatingHandler>.HttpServer.Initialize method is passing System.Web.Http.Dispatcher.HttpControllerDispatcher for HttpMessageHandlerparameter as the last HttpMessageHandler inside the chain and HttpConfiguration.MessageHandlers for IEnumerable<DelegatingHandler> parameter.

What happens inside the CreatePipeline method is very clever IMO:

public static HttpMessageHandler CreatePipeline(
    HttpMessageHandler innerHandler, 
    IEnumerable<DelegatingHandler> handlers) {
    
    if (innerHandler == null)
    {
        throw Error.ArgumentNull("innerHandler");
    }

    if (handlers == null)
    {
        return innerHandler;
    }

    // Wire handlers up in reverse order starting with the inner handler
    HttpMessageHandler pipeline = innerHandler;
    IEnumerable<DelegatingHandler> reversedHandlers = handlers.Reverse();
    foreach (DelegatingHandler handler in reversedHandlers)
    {
        if (handler == null)
        {
            throw Error.Argument("handlers", 
                Properties.Resources.DelegatingHandlerArrayContainsNullItem, 
                typeof(DelegatingHandler).Name);
        }

        if (handler.InnerHandler != null)
        {
            throw Error.Argument("handlers", 
                Properties.Resources
                .DelegatingHandlerArrayHasNonNullInnerHandler, 
                typeof(DelegatingHandler).Name, 
                "InnerHandler", 
                handler.GetType().Name);
        }

        handler.InnerHandler = pipeline;
        pipeline = handler;
    }

    return pipeline;
}

As you can see, the message handler order is reversed and the Matryoshka Doll is created but be careful here: it is ensured that HttpControllerDispatcher is the last message handler to run inside the chain. Also there is a bit of a misunderstanding about message handlers, IMHO, about calling the handler again on the way back to the client. This is actually not quite correct because the message handler’s SendAsync method itself won’t be called twice, the continuation delegate that you will chain onto SendAsync method will be invoked on the way back to client with the generated response message that you can play with.

For example, let's assume that the following two are the message handler:

public class XMagicMessageHandler : DelegatingHandler {

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken) {
            
        //Play with the request here

        return base.SendAsync(request, cancellationToken)
            .ContinueWith(task => {

                //inspect the generated response
                var response = task.Result;

                //Add the X-Magic header
                response.Headers.Add("X-Magic", "ThisIsMagic");

                return response;
        });
    }
}

Besides this I have a tiny controller which serves cars list if the request is a GET request and I registered this message handler through the GlobalConfiguration object as below:

protected void Application_Start(object sender, EventArgs e) {

    var config = GlobalConfiguration.Configuration;
    config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}");
    config.MessageHandlers.Add(new XMagicMessageHandler());
    config.MessageHandlers.Add(new SecondMessageHandler());
}

When we send a request to /api/cars, the result should be similar to below one:

image

Worked great! But, there is very nasty bug inside our message handler? Can you spot it? OK, I’ll wait a minute for you...

Did u spot it? No! OK, no harm there. Let’s try to understand what it was. I added another message handler and that message handler runs after the XMagicMessageHandler but on the way back, it runs first. The continuation delegate for the message handler will throw DivideByZeroException exception on purpose.

public class SecondMessageHandler : DelegatingHandler {

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) {

        return base.SendAsync(request, cancellationToken).ContinueWith(task => {

            throw new DivideByZeroException();

            return new HttpResponseMessage();
        });
    }
}

Now let’s put a breakpoint inside the continuation method of our XMagicMessageHandler and see what we got.

image

As you might expect, the Result property of the task doesn’t contain the response message generated by the controller. When we try to reach out to the Result property, the exception thrown by the SecondMessageHandler will be re-thrown here again and more importantly, IMO, your code doesn’t do what it is supposed to do. So, how do we get around this? Surely, you can put a try/catch block around the task.Result but that'd be a lame solution. The answer depends on what version of .NET Framework are you running on.

If you are running on .NET v4.0, things are a bit harder for you as you need to deal with TPL face to face. Thanks to ASP.NET team, it is now easier with TaskHelpers NuGet package. The TaskHelpers is actively being used by some major ASP.NET Frameworks internally at Microsoft such as ASP.NET Web API which embraces TPL from top to bottom all the way through the pipeline.

PM> Install-Package TaskHelpers.Sources

If you would like to learn more about TaskHelpers class and how it helps you, @bradwilson has a nice blog posts series on this topic and I listed the links to those posts under resources section at the bottom of this post. After installing the TaskHelpers package from NuGet, we need to fix the bugs inside our message handlers.

public class SecondMessageHandler : DelegatingHandler {

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) {

        return base.SendAsync(request, cancellationToken).Then(response => {

            int left = 10, right = 0;
            var result = left / right;

            return response;

        }).Catch<HttpResponseMessage>(info => {

            var cacthResult = 
                new CatchInfoBase<Task<HttpResponseMessage>>.CatchResult();

            cacthResult.Task = TaskHelpers.FromResult(
                request.CreateErrorResponse(
                    HttpStatusCode.InternalServerError, info.Exception));

            return cacthResult;
        });
    }
}

So, what did we actually change? We now use Then method from the TaskHelpers package instead of directly using ContinueWith method. What Then method does is that it only runs the continuation if the Task’s status is RanToCompletion. So, if the base.SendAsync method here returns a faulted or cancelled task, the continuation delegate that we pass into the Then method won’t be invoked.

Secondly, we chain another method called Catch which only runs the continuation delegate if the task’s status is faulted. If the status is cancelled, the Catch method will return back a Task whose status is set to cancelled. Inside the continuation delegate, we construct a new HttpResponseMessage through CreateErrorResponse extension method for HttpRequestMessage by passing the response status and the exception. The exception details are only sent over the wire if the following conditions are met:

  • If you set GlobalConfiguration.Configuration.IncludeErrorDetailPolicy to IncludeErrorDetailPolicy.Always.
  • if you set GlobalConfiguration.Configuration.IncludeErrorDetailPolicy to IncludeErrorDetailPolicy.LocalOnly and you run your application locally.
  • If you set GlobalConfiguration.Configuration.IncludeErrorDetailPolicy to IncludeErrorDetailPolicy.Default and your host environment’s error policy allows you to expose error details (for ASP.NET hosting, customErrors element in the Web.config file).

The XMagicMessageHandler has been changed nearly the same way as SecondMessageHandler.

public class XMagicMessageHandler : DelegatingHandler {

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken) {
            
        //Play with the request here

        return base.SendAsync(request, cancellationToken)
            .Then(response => {

                //Add the X-Magic header
                response.Headers.Add("X-Magic", "ThisIsMagic");
                return response;

            }).Catch<HttpResponseMessage>(info => {

                var cacthResult = 
                    new CatchInfoBase<Task<HttpResponseMessage>>.CatchResult();

                cacthResult.Task = TaskHelpers.FromResult(
                    request.CreateErrorResponse(
                        HttpStatusCode.InternalServerError, info.Exception));

                return cacthResult;
            });
    }
}

As I am running the application locally, I should see the error details if I send a request.

image

If I were to send a request with application/xml Accept header, I would get the error response back in xml format.

image

If you are on .NET v4.5, you will get lots of the things, which we’ve just done, out of the box. The Then and Catch extensions method from TaskHelpers package sort of mimic the new async/await compiler features some parts of the new async/await compiler features. So, the .NET v4.5 version of our message handlers look as below:

public class SecondMessageHandler : DelegatingHandler {

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) {

        try {

            int left = 10, right = 0;
            var result = left / right;

            var response = await base.SendAsync(request, cancellationToken);
            return response;
        }
        catch (Exception ex) {

            return request.CreateErrorResponse(
                HttpStatusCode.InternalServerError, ex);
        }
    }
}
public class XMagicMessageHandler : DelegatingHandler {

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, 
        CancellationToken cancellationToken) {

        try {

            var response = await base.SendAsync(request, cancellationToken);
            response.Headers.Add("X-Magic", "ThisIsMagic");
            return response;
        }
        catch (Exception ex) {

            return request.CreateErrorResponse(
                HttpStatusCode.InternalServerError, ex);
        }
    }
}

I would like to clear out that the code that we wrote for .NET v4.0 would work just fine under .NET v4.5 but as you can see we get rid of all the noise here and have a nice and more readable code. The functionality also stays the same.

image

As expected stack trace is different but the result is the same. No matter which way you choose, just choose one. Don’t leave things to chance with message handlers!

Resources




Hi 🙋🏻‍♂️ I'm Tugberk Ugurlu.
Coder 👨🏻‍💻, Speaker 🗣, Author 📚, Microsoft MVP 🕸, Blogger 💻, Software Engineering at Deliveroo 🍕🍜🌯, F1 fan 🏎🚀, Loves travelling 🛫🛬
Lives in Cambridge, UK 🏡