OWIN中间件的全局异常处理

39

我正在尝试在基于OWIN中间件(使用Owin.Host.SystemWeb的IIS HOST)构建的ASP.NET Web API 2.1项目中创建统一的错误处理/报告。 目前,我使用了一个自定义的异常记录器,它继承自System.Web.Http.ExceptionHandling.ExceptionLogger并使用NLog将所有异常记录如下:

public class NLogExceptionLogger : ExceptionLogger
{

    private static readonly Logger Nlog = LogManager.GetCurrentClassLogger();
    public override void Log(ExceptionLoggerContext context)
    {
       //Log using NLog
    } 
}
我想要将所有API异常的响应体更改为友好的统一响应,使用 System.Web.Http.ExceptionHandling.ExceptionHandler 来隐藏所有异常细节,代码如下所示:
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);
    }
}

当出现异常时,客户端将返回以下响应:

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

如果在 Api Controller 请求管道中发生任何异常,那么这将运行得非常顺利。

但是,在我的情况下,我正在使用中间件 Microsoft.Owin.Security.OAuth 生成令牌,而此中间件对 Web API 异常处理一无所知。例如,如果在方法 ValidateClientAuthentication 中抛出异常,则我的 NLogExceptionLogger 而非 ContentNegotiatedExceptionHandler 将不知道此异常,也不会尝试处理它。我在 AuthorizationServerProvider 中使用的示例代码如下:

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);
    }
}

希望能得到指导,解决以下两个问题:

1- 创建一个全局异常处理程序,只处理由OWIN中间件生成的异常?我按照这个答案创建了一个中间件来处理异常并将其注册为第一个中间件,我可以记录由"OAuthAuthorizationServerProvider"产生的异常,但我不确定这是否是最佳实践。

2- 当我像上一步一样实现了日志记录时,我真的不知道如何更改异常的响应,因为我需要为在"OAuthAuthorizationServerProvider"中发生的任何异常返回标准JSON模型。这里有一个相关的答案,我试图依赖它,但它没有起作用。

这是我的启动类和自定义的GlobalExceptionMiddleware,我为异常捕获/记录创建了它。缺失的部分是为任何异常返回统一的JSON响应。任何想法都将不胜感激。

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);
        }
    }
}

您可以在中间件(response.write)中直接编写响应。如果您想使用其他方法,请查看global.asax Application_Error。 - Imran Qadir Baksh - Baloch
3个回答

42

好的,这比预期的要容易,感谢@Khalid提供的提示,我最终创建了一个名为OwinExceptionHandlerMiddleware的owin中间件,专门处理在任何owin中间件中发生的任何异常(记录它并在将响应返回给客户端之前操作它)。

您需要在Startup类中将此中间件注册为第一个,如下所示:

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);
    }
}

以下是在 OwinExceptionHandlerMiddleware 中使用的代码:

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>();
    }
}

3
原文:Turns out it's something you have to write yourself. There is a good example, here: https://github.com/filipw/apress-recipes-webapi/blob/master/Chapter%2007/7-2/Apress.Recipes.WebApi/Apress.Recipes.WebApi/ContentNegotiatedExceptionHandler.cs翻译:原来这是需要自己编写的东西。这里有一个很好的例子: https://github.com/filipw/apress-recipes-webapi/blob/master/Chapter%2007/7-2/Apress.Recipes.WebApi/Apress.Recipes.WebApi/ContentNegotiatedExceptionHandler.cs - Jordan Morris
3
我想知道:为什么你停止继承 OwinMiddleware?既然在 OwinExceptionHandlerMiddleware 中不再继承任何东西,那么现在 Invoke() 方法是如何被调用的? - Zero3
1
@Zero3,请查看以下链接(http://benfoster.io/blog/how-to-write-owin-middleware-in-5-different-steps),了解编写OWIN中间件的5种不同方法。 - jumuro
1
据我所记,'NLogLogger'是一个静态类,我可以直接引用其中的'LogError'方法,而不需要注入它。如果你无法以这种方式实现,我会尝试查看该项目的源代码并回复你。如果你需要进一步帮助,请告诉我。 - Taiseer Joudeh
1
我尝试了你的方法,但仍然无法捕获 Web Api 中 OWIN 抛出的异常。似乎 Api 自动处理或吞掉了它... - Amir Chatrbahr
显示剩余6条评论

8
有几种方法可以实现您想要的功能:
  1. 创建一个注册为“首选项”的中间件,那么所有异常都将上升到该中间件。此时,只需通过 Response 对象通过 OWIN 上下文编写您的 JSON 即可。

  2. 您还可以创建一个包装中间件来包装 Oauth 中间件。在这种情况下,它仅捕获来自此特定代码路径的错误。

最终编写您的 JSON 消息是关于创建、序列化并通过 OWIN 上下文的响应将其写入的。

看起来您已经走在了第一种方法的正确路上。希望这可以帮助您,祝你好运 :)


3
接受的答案过于复杂,而且没有继承自OwinMiddleware类。
你只需要这样做:
 public class HttpLogger : OwinMiddleware
    {
        
        public HttpLogger(OwinMiddleware next) : base(next) { }

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

另外,不需要创建扩展方法... 它足够简单,可以直接引用。
 appBuilder.Use(typeof(HttpLogger));

如果你只想记录特定的请求,你可以根据上下文属性进行过滤:
例如:
if (context.Response.StatusCode != 200) { Log(context) }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接