ASP.NET Core Web API异常处理

408

在使用常规的ASP.NET Web API多年后,我现在正在使用ASP.NET Core进行新的REST API项目。我没有看到任何很好的处理ASP.NET Core Web API异常的方式。我尝试实现一个异常处理过滤器/属性:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}

这是我的Startup过滤器注册:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

我遇到的问题是,当我的AuthorizationFilter发生异常时,它不被ErrorHandlingFilter处理。我希望它能像旧版ASP.NET Web API一样在那里被捕获。

那么我该如何捕获所有应用程序异常以及来自Action Filters的任何异常?


5
你尝试过使用 UseExceptionHandler 中间件吗? - Pawel
作为一种选择,可以尝试在不抛出异常的情况下处理“NotFound”。像 https://github.com/AKlaus/DomainResult 这样的 NuGet 包会对此有所帮助。 - Alex Klaus
1
@AlexKlaus 代码中有太多的噪音了.. 我绝不会向任何人推荐它。 - Andrei
12个回答

753

快速简便的异常处理

只需在 ASP.NET 路由之前将此中间件添加到您的中间件注册表中即可。

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

完成!


为日志记录和其他用途启用依赖注入

步骤1. 在您的启动项中,注册您的异常处理路由:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

第二步。 创建控制器来处理所有异常并生成错误响应:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

几点重要的注意事项和观察结果:

  • 您可以将依赖项注入到控制器的构造函数中。
  • [ApiExplorerSettings(IgnoreApi = true)]是必需的。否则,它可能会破坏您的Swashbuckle swagger。
  • 同样,在Startup Configure(...)方法中,app.UseExceptionHandler("/error");必须是非常顶部的注册之一。将其放在该方法的顶部可能是安全的。
  • app.UseExceptionHandler("/error")中的路径和控制器[Route("error")]中的路径应该相同,以使控制器处理从异常处理中间件重定向的异常。

这是Microsoft官方文档的链接


响应模型思路。

实现自己的响应模型和异常。 这个示例只是一个很好的起点。每个服务都需要以自己的方式处理异常。使用所描述的方法,您具有完全的灵活性和控制,可以处理异常并从您的服务返回正确的响应。

错误响应模型的例子(只是为了给您一些思路):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

对于更简单的服务,您可能希望实现HTTP状态代码异常,它可能如下所示:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}
这可以从任何地方这样抛出:
throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

那么您的处理代码可以简化为:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext.Features.Get<IExceptionHandlerFeature>()是什么鬼?

ASP.NET Core 开发者将功能划分为中间件的概念,例如 Auth、MVC、Swagger 等,在请求处理管道中逐个执行。每个中间件都可以访问请求上下文,并在必要时写入响应内容。如果重要性如 MVC 异常一样处理来自非-MVC 中间件的错误,则将异常处理从 MVC 中分离出来是有意义的,我认为这在现实的应用程序中非常普遍。因此,由于内置的异常处理中间件不是 MVC 的一部分,MVC 本身对其一无所知,反之亦然,除了异常处理中间件当然知道它发生在请求执行管道的某个位置之外,它并不真正知道异常来自何处。但两者可能需要彼此“连接”。因此,当异常没有被任何地方捕获时,异常处理中间件会捕获它并为其中注册的路由重新运行管道。这是您可以使用一致的内容协商或其他中间件将异常处理“传递”回 MVC 的方式。异常本身从通用中间件上下文中提取。看起来很有趣,但完成了工作:)。


5
今天我一直在努力让自定义中间件工作,它的基本功能与之前相同(我正在使用它来管理请求的工作单元/事务)。 我面临的问题是,在“next”中引发的异常未被中间件捕获。 可以想象,这很棘手。 我做错了什么/遗漏了什么?有任何提示或建议吗? - brappleye3
10
我已经理解了问题所在。我只是在 Startup.cs 类中错放了中间件注册代码。我将 app.UseMiddleware<ErrorHandlingMiddleware>(); 移到了 app.UseStaticFiles(); 的前面。现在异常似乎被正确捕获了。这使我相信 app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink(); 内部使用某些魔法中间件来确保中间件的顺序正确。 - Jamadan
4
我同意自定义中间件非常有用,但对于“未找到(NotFound)”、“未授权(Unauthorised)”和“错误请求(BadRequest)”情况,我会对使用异常提出疑问。为什么不直接设置状态代码(使用NotFound()等),然后在自定义中间件或通过UseStatusCodePagesWithReExecute进行处理呢?请参阅https://www.devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api以获取更多信息。 - Paul Hiles
4
这句话的意思是,它的问题在于总是将数据序列化为JSON格式,而完全忽略了内容协商。 - Konrad
9
@Konrad的观点很有道理。这就是为什么我说这个例子可以让你开始学习,而不是最终结果。对于99%的API来说,JSON已经足够了。如果你认为我的回答还不够好,请随时做出贡献。 - Andrei
显示剩余27条评论

128

已经有一个内置的中间件可以实现这个功能:

ASP.NET Core 5版本:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;
    
    await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));

旧版本(它们没有WriteAsJsonAsync扩展):

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;
    
    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));

它应该做几乎相同的事情,只需要写少量的代码。

重要提示:请记得在MapControllers\ UseMvc(或 .NET Core 3 中的 UseRouting)之前添加它,因为顺序很重要。


2
它是否支持将DI作为处理程序的参数,还是必须在处理程序内部使用服务定位器模式? - l p
请查看已接受的答案。通过这种方法,您可以使用 DI 并完全控制 API 响应。 - Andrei

35

这个广为人知的答案对我帮助很大,但我想在我的中间件中传递HttpStatusCode以在运行时管理错误状态码。

根据这个链接,我得到了一些做同样事情的思路。所以我将Andrei的答案与此合并。所以我的最终代码如下:


1. 基类

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}


2. 自定义异常类类型

public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) 
        : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) 
        : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) 
        : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}


3. 自定义异常中间件

public class CustomExceptionMiddleware
{
    private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() 
            {
                Message = exception.Message,
                StatusCode = (int)exception.StatusCode 
            }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() 
            { 
                Message = "Runtime Error",
                StatusCode = (int)HttpStatusCode.BadRequest
            }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() 
        { 
            Message = exception.Message,
            StatusCode = (int)HttpStatusCode.InternalServerError 
        }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}


4. 扩展方法

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
{
    app.UseMiddleware<CustomExceptionMiddleware>();
}

5. 在startup.cs中配置方法

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();

现在我的账户控制器中登录方法为:

try
{
    IRepository<UserMaster> obj 
        = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
    var result = obj.Get()
        .AsQueryable()
        .Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() 
            && sb.Password == objData.Password.ToEncrypt() 
            && sb.Status == (int)StatusType.Active)
        .FirstOrDefault();
    if (result != null)//User Found
        return result;
    else // Not Found
        throw new HttpStatusCodeException(HttpStatusCode.NotFound,
            "Please check username or password");
}
catch (Exception ex)
{
    throw ex;
}

如上所示,如果我没有找到用户,则会抛出HttpStatusCodeException异常,在其中我传递了HttpStatusCode.NotFound状态和自定义消息。
在中间件中:

catch (HttpStatusCodeException ex)

将调用blocked,该方法将控制权传递给

private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception) 方法


但是,如果在此之前发生运行时错误,该怎么办?为此,我使用了try catch块,它会抛出异常,并在catch(Exception exceptionObj)块中被捕获,然后将控制权传递给

Task HandleExceptionAsync(HttpContext context, Exception exception) 方法。

为了保持一致性,我使用了一个名为ErrorDetails的类。


3
您不希望使用异常,因为它们会减慢您的API速度。异常非常昂贵。 - lnaie
2
@Inaie,我不能确定...但是看起来你从来没有遇到过需要处理的异常.. 做得好! - Arjun
3
你确定要使用"throw ex;"而不是"throw;"吗? - Leszek P
@LeszekP,我认为两者都可以工作,虽然我还没有测试过。 - Arjun
@bytedev,我想根据等式抛出自定义的HTTP状态码。 - Arjun
显示剩余2条评论

35

您最好使用中间件来实现所需的日志记录。 您需要将异常日志记录放在一个中间件中,然后在另一个中间件中处理向用户显示的错误页面。 这样可以分离逻辑并遵循 Microsoft 使用 2 个中间件组件构建的设计。 这里是 Microsoft 文档的一个好链接:ASP.Net Core 中的错误处理

对于您的具体示例,您可能想要使用StatusCodePage 中间件中的扩展功能,或像这样自己编写。

您可以在此处找到有关日志记录异常的示例:ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}

如果您不喜欢那个具体的实现,那么您也可以使用ELM中间件,这里有一些示例:Elm异常中间件

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}

如果这不能满足您的需求,您可以通过查看ExceptionHandlerMiddleware和ElmMiddleware的实现来掌握构建自己中间件组件的概念,从而创建自己的中间件组件。

重要的是将异常处理中间件添加在StatusCodePages中间件之下,但在所有其他中间件组件之上。这样,您的异常中间件将捕获异常并记录它,然后允许请求继续前往StatusCodePage中间件,该中间件将向用户显示友好的错误页面。


1
请注意,Elm不会持久化日志,建议使用Serilog或NLog提供序列化。请参阅ELM logs disappears. Can we persist it to a file or DB? - Michael Freidgeim
2
链接现在已经失效。 - Mathias Lykkegaard Lorenzen
@AshleyLee,我质疑在Web API服务实现中使用UseStatusCodePages是否有用。没有视图或HTML,只有JSON响应... - Paul Michalik

21

若要针对每种异常类型配置异常处理行为,您可以使用NuGet包中的中间件:

代码示例:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}

20
首先,感谢安德烈,因为我的解决方案基于他的示例。 我包含自己的代码作为一个更完整的样例,可能会节省读者的一些时间。 安德烈方法的限制是不处理日志记录、捕获潜在有用的请求变量和内容协商(它始终返回JSON,无论客户端请求什么 - XML/纯文本等)。 我的方法是使用ObjectResult,这允许我们使用MVC中内置的功能。 这段代码还防止响应的缓存。 错误响应已被修饰以便XML序列化器可以对其进行序列化。
public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}

如果您想查看当前的源代码并从这种方法中添加内容,请参阅 https://github.com/dotnet/aspnetcore/blob/master/src/Middleware/Diagnostics/src/ExceptionHandler/ExceptionHandlerMiddleware.cs。 - perustaja

10

首先,配置ASP.NET Core 2 Startup,以便对于来自Web服务器的任何错误和任何未处理的异常,重新执行到错误页面。

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}

接下来,定义一个异常类型,让您能够抛出带有HTTP状态代码的错误。

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}

最后,在您的错误页面控制器中,根据错误原因和响应是否直接由最终用户查看来自定义响应。该代码假定所有API网址都以/api/开头。

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}

ASP.NET Core将记录错误详细信息以供调试,因此状态码可能是您要提供给(潜在的不受信任的)请求者的全部内容。如果您想显示更多信息,可以加强HttpException来提供它。对于API错误,您可以通过将return StatusCode...替换为return Json...,将JSON编码的错误信息放入消息正文中。


6

这里是Microsoft官方指南,涵盖了所有版本的.NET,包括WebAPI和MVC。

对于Web API,建议重定向到专用控制器端点以返回ProblemDetails。由于可能会导致暴露不应直接调用的端点在OpenAPI规范中,我建议采用更简单的解决方案:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseExceptionHandler(a => a.Run(async context =>
    {
        var error = context.Features.Get<IExceptionHandlerFeature>().Error;
        var problem = new ProblemDetails { Title = "Critical Error"};
        if (error != null)
        {
            if (env.IsDevelopment())
            {
                problem.Title = error.Message;
                problem.Detail = error.StackTrace;
            }
            else
                problem.Detail = error.Message;
        }
        await context.Response.WriteAsJsonAsync(problem);
    }));
    ...
}

在这种情况下,我们利用一个标准的中间件来返回自定义细节(在开发模式下带有堆栈跟踪),避免创建'内部'端点。
附注:请注意,在.NET v3之前以及此后(到目前为止的v5)官方指南依赖于IExceptionHandlerPathFeature - IExceptionHandlerFeature
另外,如果您从域层抛出异常以将其转换为4xx代码,则建议使用khellang的ProblemDetailsMiddleware,或返回DomainResult,然后可以将其转换为IActionResultIResult。后一选项可帮助您在不引入异常开销的情况下实现相同的结果。

2
我喜欢这个方法,因为它简单易行,只需添加上述代码即可获得一个全局异常处理程序。注意:如果您正在使用 app.UseDeveloperExceptionPage(),请不要忘记将其删除,以便此类解决方案能够正常工作。 - Tawab Wakil
然而我注意到,当从除主线程以外的线程抛出异常时,异常处理程序并没有被调用。因此,在这种情况下,我在我的新线程中使用了一个简单的try/catch作为解决方法(以记录异常)。也许有更好的方法。 - Tawab Wakil
2
刚刚测试了一下,它确实可以处理来自其他线程抛出的异常(我检查了Thread.CurrentThread.ManagedThreadId以证明这一点)。你的情况更可能有其他原因(例如异常映射中间件)。此外,请注意中间件的注册顺序,正如这个SO帖子所强调的那样。 - Alex Klaus

4
通过添加自己的“异常处理中间件”,使得重用一些好的内置的异常处理逻辑(例如在发生错误时向客户端发送“符合RFC 7807标准的有效负载”)变得困难。
我所做的是将内置异常处理程序扩展到Startup.cs类之外,以处理自定义异常或覆盖现有异常的行为。例如,将ArgumentException转换为BadRequest而不更改其他异常的默认行为:
在Startup.cs上添加:
app.UseExceptionHandler("/error");

并扩展ErrorController.cs,类似于以下内容:

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;

namespace Api.Controllers
{
    [ApiController]
    [ApiExplorerSettings(IgnoreApi = true)]
    [AllowAnonymous]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = context.Error.GetType();
            
            if (exceptionType == typeof(ArgumentException)
                || exceptionType == typeof(ArgumentNullException)
                || exceptionType == typeof(ArgumentOutOfRangeException))
            {
                if (webHostEnvironment.IsDevelopment())
                {
                    return ValidationProblem(
                        context.Error.StackTrace,
                        title: context.Error.Message);
                }

                return ValidationProblem(context.Error.Message);
            }

            if (exceptionType == typeof(NotFoundException))
            {
                return NotFound(context.Error.Message);
            }

            if (webHostEnvironment.IsDevelopment())
            {
                return Problem(
                    context.Error.StackTrace,
                    title: context.Error.Message
                    );
            }
            
            return Problem();
        }
    }
}

请注意:
  1. NotFoundException 是自定义异常,您只需要执行 throw new NotFoundException(null);throw new ArgumentException("Invalid argument."); 即可。
  2. 不应向客户端提供敏感错误信息。提供错误信息会带来安全风险。

我这样做是为了返回与netcore相同的结构: var result = JsonSerializer.Serialize(new { errorCode = error.ErrorCode, errorDescription = error.ErrorDescription,
}); 但是它存在一些问题,例如TraceId。
- Ilya Chernomordik
@IlyaChernomordik 我猜你是在返回result变量?正如你在我的代码中看到的,我返回了一个内置的BaseController.ValidationProblem或BaseController.Problem。HTTP 400响应{ "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1", "title": "发生一个或多个验证错误。", "status": 400, "detail": "文件扩展名不允许。", "traceId": "|79eb7d85-40b4e4f64c19c86f.", "errors": {} } - r.pedrosa
1
是的,我知道。自己生成并确保例如TraceId正确,这很麻烦,而且它们在不同版本之间进行更改。因此,在中间件中无法使用ValidationProblem。我在自定义标头验证方面也遇到了同样的问题:我想以完全相同的方式返回响应,但由于它不直接用作参数,所以我无法使用属性验证,在中间件中我必须自己“模拟”ValidationProblem json... - Ilya Chernomordik

3

使用中间件或IExceptionHandlerPathFeature都可以。在eshop中还有另一种方式。

创建一个异常过滤器并注册它。

public class HttpGlobalExceptionFilter : IExceptionFilter
{
  public void OnException(ExceptionContext context)
  {...}
}

services.AddMvc(options =>
{
  options.Filters.Add(typeof(HttpGlobalExceptionFilter));
})

非常感谢您的回答!!!你救了我!!我一直在实现IActionFilter接口,但它没有捕获所有的异常。将其更改为IExceptionFilter对我有用。非常感谢! - David Oganov

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