Global exception handling in OWIN middleware
Asked Answered
S

3

39

I'm trying to create a unified error handling/reporting in ASP.NET Web API 2.1 Project built on top of OWIN middleware (IIS HOST using Owin.Host.SystemWeb). Currently I used a custom exception logger which inherits from System.Web.Http.ExceptionHandling.ExceptionLogger and uses NLog to log all exceptions as the code below:

public class NLogExceptionLogger : ExceptionLogger
{

    private static readonly Logger Nlog = LogManager.GetCurrentClassLogger();
    public override void Log(ExceptionLoggerContext context)
    {
       //Log using NLog
    } 
}

I want to change the response body for all API exceptions to a friendly unified response which hides all exception details using System.Web.Http.ExceptionHandling.ExceptionHandler as the code below:

public class ContentNegotiatedExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        var errorDataModel = new ErrorDataModel
        {
            Message = "Internal server error occurred, error has been reported!",
            Details = context.Exception.Message,
            ErrorReference = context.Exception.Data["ErrorReference"] != null ? context.Exception.Data["ErrorReference"].ToString() : string.Empty,
            DateTime = DateTime.UtcNow
        };

        var response = context.Request.CreateResponse(HttpStatusCode.InternalServerError, errorDataModel);
        context.Result = new ResponseMessageResult(response);
    }
}

And this will return the response below for the client when an exception happens:

{
  "Message": "Internal server error occurred, error has been reported!",
  "Details": "Ooops!",
  "ErrorReference": "56627a45d23732d2",
  "DateTime": "2015-12-27T09:42:40.2982314Z"
}

Now this is working all great if any exception occurs within an Api Controller request pipeline.

But in my situation I'm using the middleware Microsoft.Owin.Security.OAuth for generating bearer tokens, and this middleware doesn't know anything about Web API exception handling, so for example if an exception has been in thrown in method ValidateClientAuthentication my NLogExceptionLogger not ContentNegotiatedExceptionHandler will know anything about this exception nor try to handle it, the sample code I used in the AuthorizationServerProvider is as the below:

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        //Expcetion occurred here
        int x = int.Parse("");

        context.Validated();
        return Task.FromResult<object>(null);
    }

    public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        if (context.UserName != context.Password)
        {
            context.SetError("invalid_credentials", "The user name or password is incorrect.");
            return;
        }

        var identity = new ClaimsIdentity(context.Options.AuthenticationType);

        identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));

        context.Validated(identity);
    }
}

So I will appreciate any guidance in implementing the below 2 issues:

1 - Create a global exception handler which handles only exceptions generated by OWIN middle wares? I followed this answer and created a middleware for exception handling purposes and registered it as the first one and I was able to log exceptions originated from "OAuthAuthorizationServerProvider", but I'm not sure if this is the optimal way to do it.

2 - Now when I implemented the logging as the in the previous step, I really have no idea how to change the response of the exception as I need to return to the client a standard JSON model for any exception happening in the "OAuthAuthorizationServerProvider". There is a related answer here I tried to depend on but it didn't work.

Here is my Startup class and the custom GlobalExceptionMiddleware I created for exception catching/logging. The missing peace is returning a unified JSON response for any exception. Any ideas will be appreciated.

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();

        httpConfig.MapHttpAttributeRoutes();

        httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

        httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };

        app.Use<GlobalExceptionMiddleware>();

        app.UseOAuthAuthorizationServer(OAuthServerOptions);
        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        app.UseWebApi(httpConfig);
    }
}

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)
        {
            NLogLogger.LogError(ex, context);
        }
    }
}
Sholokhov answered 27/12, 2015 at 10:18 Comment(1)
You can directly write in response in middleware(response.write). If you wanna alternate approach, check global.asax Application_ErrorGina
S
42

Ok, so this was easier than anticipated, thanks for @Khalid for the heads up, I have ended up creating an owin middleware named OwinExceptionHandlerMiddleware which is dedicated for handling any exception happening in any Owin Middleware (logging it and manipulating the response before returning it to the client).

You need to register this middleware as the first one in the Startup class as the below:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var httpConfig = new HttpConfiguration();

        httpConfig.MapHttpAttributeRoutes();

        httpConfig.Services.Replace(typeof(IExceptionHandler), new ContentNegotiatedExceptionHandler());

        httpConfig.Services.Add(typeof(IExceptionLogger), new NLogExceptionLogger());

        OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
        {
            AllowInsecureHttp = true,
            TokenEndpointPath = new PathString("/token"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
            Provider = new AuthorizationServerProvider()
        };

        //Should be the first handler to handle any exception happening in OWIN middlewares
        app.UseOwinExceptionHandler();

        // Token Generation
        app.UseOAuthAuthorizationServer(OAuthServerOptions);

        app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        app.UseWebApi(httpConfig);
    }
}

And the code used in the OwinExceptionHandlerMiddleware as the below:

using AppFunc = Func<IDictionary<string, object>, Task>;

public class OwinExceptionHandlerMiddleware
{
    private readonly AppFunc _next;

    public OwinExceptionHandlerMiddleware(AppFunc next)
    {
        if (next == null)
        {
            throw new ArgumentNullException("next");
        }

        _next = next;
    }

    public async Task Invoke(IDictionary<string, object> environment)
    {
        try
        {
            await _next(environment);
        }
        catch (Exception ex)
        {
            try
            {

                var owinContext = new OwinContext(environment);

                NLogLogger.LogError(ex, owinContext);

                HandleException(ex, owinContext);

                return;
            }
            catch (Exception)
            {
                // If there's a Exception while generating the error page, re-throw the original exception.
            }
            throw;
        }
    }
    private void HandleException(Exception ex, IOwinContext context)
    {
        var request = context.Request;

        //Build a model to represet the error for the client
        var errorDataModel = NLogLogger.BuildErrorDataModel(ex);

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ReasonPhrase = "Internal Server Error";
        context.Response.ContentType = "application/json";
        context.Response.Write(JsonConvert.SerializeObject(errorDataModel));

    }

}

public static class OwinExceptionHandlerMiddlewareAppBuilderExtensions
{
    public static void UseOwinExceptionHandler(this IAppBuilder app)
    {
        app.Use<OwinExceptionHandlerMiddleware>();
    }
}
Sholokhov answered 28/12, 2015 at 11:53 Comment(11)
Where do you get ContentNegotiatedExceptionHandler from? Thanks.Endblown
Turns out it's something you have to write yourself. There is a good example, here: github.com/filipw/apress-recipes-webapi/blob/master/…Endblown
I wonder: Why did you stop inheriting from OwinMiddleware? And how is the Invoke() method called now that you don't inherit from anything in OwinExceptionHandlerMiddleware?Lanark
@Zero3, see following link to see 5 different ways to write an OWIN middleware.Proprietor
@Taiseer How were you able to reference NLogger from inside? I tried to inject my log4net exception handler, and I wasn`t able to. TksEmpale
@Empale as far as I remember 'NLogLogger' was a static class where I can reference the method 'LogError' directly, I didn't inject it. If you were not able to do it this way I will try to check the source code for this project and get back to you. Let me know if you need further help.Sholokhov
Nice copy & paste solution. Thx!Jarrell
The OP provides an implementation of ContentNegotiatedExceptionHandler in the original question. -- Basically you just inherit System.Web.Http.ExceptionHandling.ExceptionHandler and override the Handle method. Then, in the body of the Handle method set context.Result = ... to something that makes sense (i.e. StatusCode 500, plus relevant exceptions details).Bertrand
@Taiseer did you get rid of the ContentNegotiatedExceptionHandler with this solution? As I still see you are replacing IExceptionHandler with it in Startup.Catarina
I tried your approach but still can't reach an exception thrown by OWIN in Web Api. Seems Api handle it automatically or swallow it ...Catarina
This is a good working example although it still didn't catch my custom exceptions. I eventually got it working by changing the base class for my exceptions from HttpResponseException to HttpException.Whomever
T
8

There are a few ways to do what you want:

  1. Create middleware that is registered first, then all exceptions will bubble up to that middleware. At this point just write out your JSON out via the Response object via the OWIN context.

  2. You can also create a wrapping middleware which wraps the Oauth middleware. In this case it will on capture errors originating from this specific code path.

Ultimately writing your JSON message is about creating it, serializing it, and writing it to the Response via the OWIN context.

It seems like you are on the right path with #1. Hope this helps, and good luck :)

Therm answered 27/12, 2015 at 15:42 Comment(0)
I
2

The accepted answer is unnecessarily complex and doesn't inherit from OwinMiddleware class

All you need to do is this:

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

        public override async Task Invoke(IOwinContext context)
        {
            
            await Next.Invoke(context);
            Log(context)
            
        }
    }

Also, no need to create extension method.. it is simple enough to reference without

 appBuilder.Use(typeof(HttpLogger));

And if you wanna log only specific requests, you can filter on context properties:

ex:

if (context.Response.StatusCode != 200) { Log(context) }
Illinois answered 14/7, 2021 at 16:15 Comment(1)
Shouldn't await Next.Invoke(context) be wrapped in a try-catch block?Profundity

© 2022 - 2024 — McMap. All rights reserved.