您现在的位置是:网站首页> 编程资料编程资料

理解ASP.NET Core 错误处理机制(Handle Errors)_实用技巧_

2023-05-24 296人已围观

简介 理解ASP.NET Core 错误处理机制(Handle Errors)_实用技巧_

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

使用中间件进行错误处理

开发人员异常页

开发人员异常页用于显示未处理的请求异常的详细信息。当我们通过ASP.NET Core模板创建一个项目时,Startup.Configure方法中会自动生成以下代码:

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { // 添加开发人员异常页中间件 app.UseDeveloperExceptionPage(); } }

需要注意的是,与“异常处理”有关的中间件,一定要尽早添加,这样,它可以最大限度的捕获后续中间件抛出的未处理异常。

可以看到,当程序运行在开发环境中时,才会启用开发人员异常页,这很好理解,因为在生产环境中,我们不能将异常的详细信息暴露给用户,否则,这将会导致一系列安全问题。

现在我们在下方添加如下代码抛出一个异常:

 app.Use((context, next) => { throw new NotImplementedException(); });

当开发人员异常页中间件捕获了该未处理异常时,会展示类似如下的相关信息:

该异常页面展示了如下信息:

  • 异常消息
  • 异常堆栈追踪(Stack)
  • HTTP请求查询参数(Query)
  • Cookies
  • HTTP请求标头(Headers)
  • 路由(Routing),包含了终结点和路由信息

IDeveloperPageExceptionFilter

当你查看DeveloperExceptionPageMiddleware的源码时,你会在构造函数中发现一个入参,类型为IEnumerable。通过这个Filter集合,组成一个错误处理器管道,按照先注册先执行的原则,顺序进行错误处理。

下面是DeveloperExceptionPageMiddleware的核心源码:

 public class DeveloperExceptionPageMiddleware { public DeveloperExceptionPageMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment, DiagnosticSource diagnosticSource, IEnumerable filters) { // ... // 将 DisplayException 放置在管道最底部 // DisplayException 就用于向响应中写入我们上面见到的异常页 _exceptionHandler = DisplayException; foreach (var filter in filters.Reverse()) { var nextFilter = _exceptionHandler; _exceptionHandler = errorContext => filter.HandleExceptionAsync(errorContext, nextFilter); } } public async Task Invoke(HttpContext context) { try { await _next(context); } catch (Exception ex) { // 响应已经启动,则跳过处理,直接上抛 if (context.Response.HasStarted) { throw; } try { context.Response.Clear(); context.Response.StatusCode = 500; // 错误处理 await _exceptionHandler(new ErrorContext(context, ex)); // ... // 错误已成功处理 return; } catch (Exception ex2) { } // 若处理过程中抛出了新的异常ex2,则重新引发原始异常ex throw; } } }

这也就说明,如果我们想要自定义开发者异常页,那我们可以通过实现IDeveloperPageExceptionFilter接口来达到目的。

先看一下IDeveloperPageExceptionFilter接口定义:

 public interface IDeveloperPageExceptionFilter { Task HandleExceptionAsync(ErrorContext errorContext, Func next); } public class ErrorContext { public ErrorContext(HttpContext httpContext, Exception exception) { HttpContext = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); Exception = exception ?? throw new ArgumentNullException(nameof(exception)); } public HttpContext HttpContext { get; } public Exception Exception { get; } }

HandleExceptionAsync方法除了错误上下文信息外,还包含了一个Func next,这是干嘛的呢?其实,前面我们已经提到了,IDeveloperPageExceptionFilter的所有实现,会组成一个管道,当错误需要在管道中的后续处理器作进一步处理时,就是通过这个next传递错误的,所以,当需要传递错误时,一定要记得调用next

不废话了,赶紧实现一个看看效果吧:

 public class MyDeveloperPageExceptionFilter : IDeveloperPageExceptionFilter { public Task HandleExceptionAsync(ErrorContext errorContext, Func next) { errorContext.HttpContext.Response.WriteAsync($"MyDeveloperPageExceptionFilter: {errorContext.Exception}"); // 我们不调用 next,这样就不会执行 DisplayException return Task.CompletedTask; } } public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); }

当抛出一个异常,你会看到类似如下的页面:

异常处理程序

上面介绍了开发环境中的异常处理,现在我们来看一下生产环境中的异常处理,通过调用UseExceptionHandler扩展方法注册中间件ExceptionHandlerMiddleware

该异常处理程序:

  • 可以捕获后续中间件未处理的异常
  • 若无异常或HTTP响应已经启动(Response.HasStarted == true),则不做任何处理
  • 不会改变URL中的路径

默认情况下,会生成类似如下的模板:

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { // 添加异常处理程序 app.UseExceptionHandler("/Home/Error"); } }

通过lambda提供异常处理程序

我们可以通过lambda向UseExceptionHandler中提供一个异常处理逻辑:

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler(errorApp => { var loggerFactory = errorApp.ApplicationServices.GetRequiredService(); var logger = loggerFactory.CreateLogger("ExceptionHandlerWithLambda"); errorApp.Run(async context => { // 这里可以自定义 http response 内容,以下仅是示例 var exceptionHandlerPathFeature = context.Features.Get(); logger.LogError($"Exception Handled:{exceptionHandlerPathFeature.Error}"); var statusCode = StatusCodes.Status500InternalServerError; var message = exceptionHandlerPathFeature.Error.Message; if (exceptionHandlerPathFeature.Error is NotImplementedException) { message = "俺未实现"; statusCode = StatusCodes.Status501NotImplemented; } context.Response.StatusCode = statusCode; context.Response.ContentType = "application/json"; await context.Response.WriteAsJsonAsync(new { Message = message, Success = false, }); }); }); }

可以看到,当捕获到异常时,可以通过HttpContext.Features,并指定类型IExceptionHandlerPathFeatureIExceptionHandlerFeature(前者继承自后者),来获取到异常信息。

 public interface IExceptionHandlerFeature { // 异常信息 Exception Error { get; } } public interface IExceptionHandlerPathFeature : IExceptionHandlerFeature { // 未被转义的http请求资源路径 string Path { get; } }

再提醒一遍,千万不要将敏感的错误信息暴露给客户端。

异常处理程序页

除了使用lambda外,我们还可以指定一个路径,指向一个备用管道进行异常处理,这个备用管道对于MVC来说,一般是Controller中的Action,例如MVC模板默认的/Home/Error

 public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseExceptionHandler("/Home/Error"); } public class HomeController : Controller { [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult Error() { return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } }

当捕获到异常时,你会看到类似如下的页面:

你可以在ActionError中自定义错误处理逻辑,就像lambda一样。

需要注意的是,不要随意对Error添加[HttpGet][HttpPost]等限定Http请求方法的特性。一旦你加上了[HttpGet],那么该方法只能处理Get请求的异常。

不过,如果你就是打算将不同方法的Http请求分别进行处理,你可以类似如下进行处理:

 public class HomeController : Controller { // 处理Get请求的异常 [HttpGet("[controller]/error")] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult GetError() { _logger.LogInformation("Get Exception Handled"); return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } // 处理Post请求的异常 [HttpPost("[controller]/error")] [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] public IActionResult PostError() { _logger.LogInformation("Post Exception Handled"); return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } }

另外,还需要提醒一下,如果在请求备用管道(如示例中的Error)时也报错了,无论是Http请求管道中的中间件报错,还是Error里面报错,此时ExceptionHandlerMiddleware均会重新引发原始异常,而不是向外抛出备用管道的异常。

一般异常处理程序页是面向所有用户的,所以请保证它可以匿名访问。

下面一块看一下ExceptionHandlerMiddleware吧:

 public class ExceptionHandlerMiddleware { public ExceptionHandlerMiddleware( RequestDelegate next, ILoggerFactory loggerFactory, IOptions options, DiagnosticListener diagnosticListener) { // 要么手动指定一个异常处理器(如通过lambda) // 要么提供一个资源路径,重新发送给后续中间件,进行异常处理 if (_options.ExceptionHandler == null) { if (_options.ExceptionHandlingPath == null) { throw new Inva
                
                

-六神源码网