欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

如何创建一个自定义的`ErrorHandlerMiddleware`方法

程序员文章站 2022-05-18 19:26:35
在本文中,我将讲解如何通过自定义 ,以便在中间件管道中发生错误时创建自定义响应,而不是提供一个“重新执行”管道的路径。 作者:依乐祝 译文:https://www.cnblogs.com/yilezhu/p/12497937.html 原文:https://andrewlock.net/creati ......

在本文中,我将讲解如何通过自定义exceptionhandlermiddleware,以便在中间件管道中发生错误时创建自定义响应,而不是提供一个“重新执行”管道的路径。

作者:依乐祝
译文:
原文:

razor页面中的异常处理

所有的.net应用程序都有可能会产生错误,并且不幸地引发异常,因此在asp.net中间件管道中处理这些异常显得非常重要。服务器端呈现的应用程序(如razor pages)通常希望捕获这些异常并重定向到一个错误页面。

例如,如果您创建一个使用razor pages(dotnet new webapp)的新web应用程序,您将在startup.configure中看到如下的中间件配置:

public void configure(iapplicationbuilder app, iwebhostenvironment env)
{
    if (env.isdevelopment())
    {
        app.usedeveloperexceptionpage();
    }
    else
    {
        app.useexceptionhandler("/error");
    }

    // .. other middleware not shown
}

development环境中运行时,应用程序将捕获处理请求时引发的所有异常,并使用一个非常有用的developerexceptionmiddleware方法将其以网页的形式进行显示:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

这在本地开发期间非常有用,因为它使您可以快速检查堆栈跟踪,请求标头,路由详细信息以及其他内容。

当然,这些都是您不想在生产中公开的敏感信息。因此,当不在开发阶段时,我们将使用其他异常处理程序exceptionhandlermiddleware。此中间件允许您提供一个请求路径,默认情况下是"/error",并使用它“重新执行”中间件管道,以生成最终响应:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

razor pages应用程序的最终结果是,每当生产中发生异常时,就会返回这个error.cshtml 的razor 页面:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

这涵盖了razor 页面的异常处理,但是web api呢?

web api的异常处理

web api模板(dotnet new webapi)中的默认异常处理类似于razor pages使用的异常处理,但有一个重要的区别:

public void configure(iapplicationbuilder app, iwebhostenvironment env)
{
    if (env.isdevelopment())
    {
        app.usedeveloperexceptionpage();
    }

    // .. other middleware not shown
}

如您所见developerexceptionmiddleware,在development环境中仍会添加,但是在生产中根本没有添加错误处理!这没有听起来那么糟糕:即使没有异常处理中间件,asp.net core也会在其底层架构中捕获该异常,将其记录下来,并向客户端返回一个空白的500响应:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

如果您正在使用该[apicontroller]属性(你可能应该这样使用),并且该错误来自您的web api控制器,那么problemdetails默认情况下会得到一个结果,或者您可以进一步对其进行自定义。

对于web api客户端来说,这实际上还不错。您的api使用者应能够处理错误响应,因此最终用户将不会看到上面的“中断”页面。但是,它通常不是那么简单。

例如,也许您使用的是错误的标准格式,例如problemdetails格式。如果您的客户期望所有错误都具有该格式,那么在某些情况下生成的空响应很可能导致客户端中断。同样,在development环境中,当客户端期望返回json时而你返回一个html开发人员异常页面,这可能会导致问题!

描述了一种解决方案,建议您创建errorcontroller并具有两个终结点的:

[apicontroller]
public class errorcontroller : controllerbase
{
    [route("/error-local-development")]
    public iactionresult errorlocaldevelopment() => problem(); // add extra details here

    [route("/error")]
    public iactionresult error() => problem();
}

然后使用razor pages应用程序中使用的相同“重新执行”功能来生成响应:

public void configure(iapplicationbuilder app, iwebhostenvironment env)
{
    if (env.isdevelopment())
    {
        app.useexceptionhandler("/error-local-development");
    }
    else
    {
        app.useexceptionhandler("/error");
    }

    // .. other middleware
}

这可以正常工作,但是对于使用生成异常的同一基础结构(例如razor pages或mvc)来生成异常消息,总有一些困扰我。由于被第二次抛出异常,我多次被失败的错误响应所困扰!因此,我喜欢采取稍微不同的方法。

使用exceptionhandler代替exceptionhandlingpath

当我第一次开始使用asp.net core时,解决此问题的方法是编写自己的自定义exceptionhandler中间件来直接生成响应。“处理异常不是那么难,对吧”?

事实证明,这要复杂得多(我知道,令人震惊)。您需要处理各种边缘情况,例如:

  • 如果在发生异常时响应已经开始发送,则您将无法拦截它。
  • 如果在endpointmiddleware发生异常时已执行,则需要对选定的端点进行一些处理
  • 您不想缓存错误响应

exceptionhandlermiddleware处理所有这些情况,所以重新写你自己的版本不是一条要走的路。幸运的是,尽管通常显示的方法是为中间件提供重新执行的路径,但还有另一种选择-直接提供处理函数。

exceptionhandlermiddleware中有一个exceptionhandleroptions参数。该选项对象具有两个属性

public class exceptionhandleroptions
{
    public pathstring exceptionhandlingpath { get; set; }
    public requestdelegate exceptionhandler { get; set; }
}

当你向useexceptionhandler(path)方法提供重新执行的路径时,实际上是在options对象上设置exceptionhandlingpath。同样的,如果需要的话,您可以设置exceptionhandler属性,并使用useexceptionhandler()exceptionhandleroptions的实例直接传递给中间件:

public void configure(iapplicationbuilder app, iwebhostenvironment env)
{
    app.useexceptionhandler(new exceptionhandleroptions
    {
        exceptionhandler = // .. to implement
    });

    // .. othe middleware
}

另外,您可以使用useexceptionhandler()的另一个重载方法并配置一个迷你中间件管道来生成响应:

public void configure(iapplicationbuilder app, iwebhostenvironment env)
{
    app.useexceptionhandler(err => err.usecustomerrors(env)); // .. to implement

    // .. othe middleware
}

两种方法都是等效的,因此更多是关于喜好的问题。在本文中,我将使用第二种方法并实现该usecustomerrors()功能。

创建自定义异常处理函数

对于此示例,我将假设我们在中间件管道中遇到异常时需要生成一个problemdetails的对象。我还要假设我们的api仅支持json。这就避免了我们不必担心xml内容协商等问题。在开发环境中,problemdetails响应将包含完整的异常堆栈跟踪,而在生产环境中,它将仅显示一般错误消息。

problemdetails是返回http响应中错误的机器可读详细信息方法。这是从asp.net core 3.x(在某种程度上在2.2版中)的web api返回错误消息的普遍支持的方法。

我们将从在静态帮助器类中定义usecustomerrors函数开始。该帮助类将一个生成响应的中间件添加到iapplicationbuilder方法扩展中。在开发环境中,它最终会调用writeresponse方法,并且设置includedetails: true。在其他环境中,includedetails`设置为false。

using system;
using system.diagnostics;
using system.text.json;
using system.threading.tasks;
using microsoft.aspnetcore.builder;
using microsoft.aspnetcore.diagnostics;
using microsoft.aspnetcore.hosting;
using microsoft.aspnetcore.http;
using microsoft.aspnetcore.mvc;
using microsoft.extensions.hosting;

public static class customerrorhandlerhelper
{
    public static void usecustomerrors(this iapplicationbuilder app, ihostenvironment environment)
    {
        if (environment.isdevelopment())
        {
            app.use(writedevelopmentresponse);
        }
        else
        {
            app.use(writeproductionresponse);
        }
    }

    private static task writedevelopmentresponse(httpcontext httpcontext, func<task> next)
        => writeresponse(httpcontext, includedetails: true);

    private static task writeproductionresponse(httpcontext httpcontext, func<task> next)
        => writeresponse(httpcontext, includedetails: false);

    private static async task writeresponse(httpcontext httpcontext, bool includedetails)
    {
        // .. to implement
    }
}

剩下的就是实现writeresponse方法来生成我们的响应的功能。这将从exceptionhandlermiddleware(通过iexceptionhandlerfeature)中检索异常,并构建一个包含要显示的详细信息的problemdetails对象。然后,它使用system.text.json序列化程序将对象写入response流。

private static async task writeresponse(httpcontext httpcontext, bool includedetails)
{
    // try and retrieve the error from the exceptionhandler middleware
    var exceptiondetails = httpcontext.features.get<iexceptionhandlerfeature>();
    var ex = exceptiondetails?.error;

    // should always exist, but best to be safe!
    if (ex != null)
    {
        // problemdetails has it's own content type
        httpcontext.response.contenttype = "application/problem+json";

        // get the details to display, depending on whether we want to expose the raw exception
        var title = includedetails ? "an error occured: " + ex.message : "an error occured";
        var details = includedetails ? ex.tostring() : null;

        var problem = new problemdetails
        {
            status = 500,
            title = title,
            detail = details
        };

        // this is often very handy information for tracing the specific request
        var traceid = activity.current?.id ?? httpcontext?.traceidentifier;
        if (traceid != null)
        {
            problem.extensions["traceid"] = traceid;
        }

        //serialize the problem details object to the response as json (using system.text.json)
        var stream = httpcontext.response.body;
        await jsonserializer.serializeasync(stream, problem);
    }
}

您可以在序列化problemdetails之前记录从httpcontext中检索的自己喜欢的任何其他值。

请注意,在调用异常处理程序方法之前,exceptionhandlermiddleware清除路由值,以使这些值不可用。

如果您的应用程序现在在development环境中引发异常,则您将在响应中获取作为json返回的完整异常:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

在生产环境中,您仍然会得到problemdetails响应,但是省略了详细信息:

如何创建一个自定义的`ErrorHandlerMiddleware`方法

与mvc /重新执行路径方法相比,此方法显然具有一些局限性,即您不容易获得模型绑定,内容协商,简单的序列化或本地化(取决于您的方法)。

如果您需要其中任何一个(例如,也许您使用pascalcase而不是camelcase从mvc进行序列化),那么使用此方法可能比其价值更麻烦。如果是这样,那么所描述的controller方法可能是明智的选择。

如果您不关心这些,那么本文中显示的简单处理程序方法可能是更好的选择。无论哪种方式,都不要尝试实现自己的版本exceptionhandlermiddleware-使用可用的扩展点!