39

What is the proper way to implement a global Exception catcher-handler in a Katana (OWIN) implementation?

In a self-hosted OWIN/Katana implementation running as an Azure Cloud Service (worker role), I placed this code in a Middleware:

throw new Exception("pooo");

Then I placed this code in the Startup class Configuration method, setting a breakpoint in the event handler:

 AppDomain.CurrentDomain.UnhandledException += 
    CurrentDomain_UnhandledExceptionEventHandler;

and the event handler in the same class (with a breakpoint set on the first line):

private static void CurrentDomain_UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e)
{
    var exception = (Exception)e.ExceptionObject;
    Trace.WriteLine(exception.Message);
    Trace.WriteLine(exception.StackTrace);
    Trace.WriteLine(exception.InnerException.Message);
}

When the code runs the breakpoint is not hit. The Visual Studio Output window does include this however:

A first chance exception of type 'System.Exception' occurred in redacted.dll
A first chance exception of type 'System.Exception' occurred in mscorlib.dll

I also tried moving the wireup and handler to the Worker Role OnStart method but still the breakpoint is not hit.

I am not using WebAPI at all, but did look at posts on what is done there, but I found nothing clear, so here I am.

Running on .NET Framework 4.5.2, VS 2013.

All ideas appreciated. Thanks.

Daniel
  • 9,491
  • 12
  • 50
  • 66
Snowy
  • 5,942
  • 19
  • 65
  • 119

4 Answers4

43

Try writing a custom middleware and placing it as the first middleware:

public class GlobalExceptionMiddleware : OwinMiddleware
{
   public GlobalExceptionMiddleware(OwinMiddleware next) : base(next)
   {}

   public override async Task Invoke(IOwinContext context)
   {
      try
      {
          await Next.Invoke(context);
      }
      catch(Exception ex)
      {
          // your handling logic
      }
   }
 }

Place it as the first middleware:

public class Startup
{
    public void Configuration(IAppBuilder builder)
    {
        var config = new HttpConfiguration();

        builder.Use<GlobalExceptionMiddleware>();
        //register other middlewares
    }
}

When we register this middleware as the first middle, any exceptions happening in other middlewares (down the stacktrace) will propagate up and be caught by the try/catch block of this middleware.

It's not mandatory to always register it as the first middleware, in case you don't need global exception handling for some middlewares, just register these middlewares before this one.

public class Startup
{
    public void Configuration(IAppBuilder builder)
    {
        var config = new HttpConfiguration();

        //register middlewares that don't need global exception handling. 
        builder.Use<GlobalExceptionMiddleware>();
        //register other middlewares
    }
}
Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83
Khanh TO
  • 48,509
  • 13
  • 99
  • 115
  • Could there be thread locking/blocking concerns doing this? It definitely works but I have not seen this solution anywhere before (which is why I asked on SO). – Snowy Jul 08 '15 at 13:44
  • @Snowy: there should be no thread locking, this is just a normal owin middleware just like any others. – Khanh TO Jul 09 '15 at 12:53
  • 34
    That doesn't work. Web API exceptions are handled by default. The code will not enter the catch block. Try it. – Dr Schizo Dec 16 '15 at 22:56
  • 3
    @Dr Schizo: If the exception is caught and `swallowed` by web api (it means the web api has done the recovery). This code sure won't enter the catch block, just like any normal exception handling code. If you catch an exception down the stack trace and don't re-throw it, the code above the stack trace won't catch any exception. This is expected behavior. – Khanh TO Dec 17 '15 at 03:13
  • 8
    @Dr Schizo: `Web API exceptions are handled by default` => this means the exception is `handled`, so that it does not propagate up the stacktrace. This global middleware only handles `unhandled` exceptions which makes a lot of sense that it does not handle `handled` exceptions (by web api). For any exceptions thrown unhandled (by other middleware) or rethrown inside web api, this middleware should be able to catch them. – Khanh TO Dec 17 '15 at 12:19
  • 2
    My assumption here is if I had a controller which threw an exception upon calling its action I would expect the middleware (you've described) to catch this and the implementation to do whatever it needs to do, i.e. log it etc. Trouble is the middleware doesn't actually enter the catch block thus is unaware of the exception raised nor is it present in the dictionary. Have I misunderstood? – Dr Schizo Dec 23 '15 at 09:19
  • @Dr Schizo: there are many levels where exceptions can be caught (web api, middleware,..). If your web api catches the exception and `handles` it, it means the exception `has been handled`, it will not propagate. If you handle your exceptions inside web api level by purpose and also expect the exceptions to be handled by middleware, it's really a sign of design flaw. – Khanh TO Dec 24 '15 at 12:11
  • @Dr Schizo: As I said, any exceptions thrown unhandled by other middleware are caught by this middleware. There are many levels you can place your unhandled exceptions handler (web api, middleware,..), this is by design, there is nothing right or wrong about where to put the handlers or at which levels. – Khanh TO Dec 24 '15 at 12:17
  • @Dr Schizo: Does it answer your question? I'm happy to discuss further with you in case you still have some concerns. – Khanh TO Dec 25 '15 at 13:13
  • @KhanhTO I'm using the OWIN middleware OAuthAuthorizationServerProvider for token authentication, you suggestion worked for logging the exception using log4net. That's good. But on the same time I'm using my own "ExceptionHandler" to unify 500 errors returned to the user, but in case of an exception happening in the owin middleware, the full stack trace is returned in the response. I'm trying to return HTTP 500 with the below response for all exceptions: { "Message": "Internal server error", "Details": "Input string was not in a correct format", "ErrorReference": "c7f445c73e959738" } – Taiseer Joudeh Dec 26 '15 at 01:29
  • @Taiseer Joudeh: The problem is your `OAuthAuthorizationServerProvider` middleware and `web api` middleware are separate middlewares. Exceptions happening inside `OAuthAuthorizationServerProvider` is not handled by `ExceptionHandler` middleware (it may work if we place `OAuthAuthorizationServerProvider` after web api but it will not do authentication correctly as authentication is done after web api). In this case, you may need to handle the "unify" in this global exception handling instead of "ExceptionHandler": http://stackoverflow.com/questions/26214113/ – Khanh TO Dec 26 '15 at 04:33
  • 1
    @Khanh in the basic example I had up and running I had a single controller and a single action which threw an exception. Nowhere is this handled apart from default web API behaviour which I believe handles this. Thus, the middleware doesn't do anything. You have to replace the global exception handler web API has in place then this middleware will work. See thread I linked to. – Dr Schizo Dec 26 '15 at 12:27
  • 1
    @Dr Schizo: Sure, if the exception is caught and handled inside web api, the middleware doesn't do anything. One thing I want to emphasize is that when we develop a global middleware, we don't develop for web api (a very `specific` middleware), if the web api handles the exceptions itself, then fine, the exceptions are handled, this middleware does not do anything, that's the design choice. But if we use this middleware with other ones (assuming not web api), this should work. As I said, there are many levels where exceptions can be handled. – Khanh TO Dec 26 '15 at 15:08
13

Try this:

public class CustomExceptionHandler : IExceptionHandler
{    
    public Task HandleAsync(ExceptionHandlerContext context, CancellationToken cancellationToken)
    {
      // Perform some form of logging

      context.Result = new ResponseMessageResult(new HttpResponseMessage
      {
        Content = new StringContent("An unexpected error occurred"),
        StatusCode = HttpStatusCode.InternalServerError
      });

      return Task.FromResult(0);
    }
}

And at startup:

public void Configuration(IAppBuilder app)
{
  var config = new HttpConfiguration();

  config.Services.Replace(typeof(IExceptionHandler), new CustomExceptionHandler());
}
Greg R Taylor
  • 3,470
  • 1
  • 25
  • 19
0

@Khanh TO's answer is excellent; Nice terse code, and well explained.

It was not working for me. I added a test exception to one of my .NET MVC controllers like this:

throw new Exception("Test GlobalExceptionMiddleware");

The exception was not caught in the Invoke method.

This is very easy to debug:

  1. Put a breakpoint on the test exception.
  2. Hit F10 or step over each line until you get to the code that is handling the exception.

For me, there was code added to a BaseController overriding OnException. This code was handling the exception and not rethrowing. I hope this is a helpful addendum to @Khanh TO's answer.

Update

OK, I realize this question is tagged with Web API, and my use case is .NET MVC, but I found this question on a search for Owin middleware. When I debug my solution with the test exception, and watch $exception in the Locals view, the exception is gone when I get back to the Owin middleware. Therefore I have not used the code in @Khanh TO's answer. Instead I have added try/catch blocks to my Startup and Global.asax. I have also OnException in my base controller like this:

protected override void OnException(ExceptionContext filterContext)
{
    if (filterContext.ExceptionHandled)
    {
        return;
    }
    var e = filterContext.Exception;
    Log.Error("BaseController.OnException caught exception:", e);
    filterContext.ExceptionHandled = true;
}

This answer will catch any exceptions on application start up and any exception in a controller. That is enough for my needs.

Jess
  • 23,901
  • 21
  • 124
  • 145
0

You can use custom exception filter attributes.

public static class WebApiConfiguration
{
    public static void Register(HttpConfiguration config)
    {
        config.RegisterExceptionFilters();
    }
}

public static class HttpConfigurationFilterExtensions
{
    public static HttpConfiguration RegisterExceptionFilters(this HttpConfiguration httpConfig)
    {
        httpConfig.Filters.Add(new CustomExceptionFilterAttribute());

        return httpConfig;
    }
}

public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext context)
    {
        if (context.Exception is CustomException)
        {
            context.Response = new HttpResponseMessage((HttpStatusCode)CustomHttpStatusCode.CustomCode);
        }
    }
}

public class CustomException : Exception
{
    public CustomException(string message)
        : base(message)
    {
    }
}
Joseph Katzman
  • 1,959
  • 6
  • 21
  • 47