我们基于 Razor Class Library 实现了自定义错误页面的公用类库(详见之前的随笔),但是在实际使用时发现如果在 middleware 中发生了异常,则不能显示自定义错误页面,而是返回默认的 500 空白页面。
public static IApplicationBuilder UseCustomErrorPages(this IApplicationBuilder app)
{
app.UseExceptionHandler("/errors/500");
app.UseStatusCodePagesWithReExecute("/errors/{0}");
return app;
}
自定义错误页面使用的是上面的配置,当发生异常时,会走路由 /errors/500 到达对应的自定义错误页面的 mvc action 。 如果是 mvc 中产生异常,能正常到达;但是当 middleware 中产生异常时,在去往自定义错误页面的途中,又途径异常 middleware ,从而让自定义错误页面也产生了异常。这就是自定义错误页面不能显示的原因。
问题的原因想明白了,接下来通过集成测试重现这个问题。
public class ErrorPageTests : IClassFixture<WebApplicationFactory<Startup>>
{
[Fact]
public async Task ErrorPageForMiddleWareException()
{
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services => services.AddMvc());
builder.Configure(app =>
{
app.UseCustomErrorPages();
app.Use(next => context => throw new Exception("Failed in middleware"));
app.UseMvcWithDefaultRoute();
});
}).CreateClient();
var response = await client.GetAsync("/");
Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode);
var content = await response.Content.ReadAsStringAsync();
Assert.Contains($"请求失败:500", content);
}
}
先写好测试的好处之一是在尝试解决方法时可以快速得到反馈,虽然写测试代码会花去更多时间,但仅这一点好处就得远大于失。
有了测试代码的保驾护航,这时就可以放心大胆的动手尝试解决问题了。
既然问题的根源是“在去往自定义错误页面的途中,又途径异常 middleware”,那我们只要抄近路绕过这些 middlewares ,直接奔向 asp.net core mvc 的 middleware ,问题不就解决了吗?
那怎么绕过去呢?不用 app.UseExceptionHandler ,自己写个 middleware ?先别做这个啥事,先搞清楚 app.UseExceptionHandler 究竟干了些啥?如果能通过 app.UseExceptionHandler 解决这个问题,岂不更好?
打开 app.UseExceptionHandler 所在的 github 仓库 Microsoft.AspNetCore.Diagnostics ,找到对应的中间件实现源码 ExceptionHandlerMiddleware ,下面的删减后的源码:
public class ExceptionHandlerMiddleware
{
//...
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
//...
PathString originalPath = context.Request.Path;
if (_options.ExceptionHandlingPath.HasValue)
{
context.Request.Path = _options.ExceptionHandlingPath;
}
try
{
//...
await _options.ExceptionHandler(context);
//...
}
//...
}
}
}
原来在 ExceptionHandlerMiddleware 中只是将请求路径修改为 "/errors/500" 然后调用 ExceptionHandler ,但是我们在 app.UseExceptionHandler 只设置了请求路径,并没有设置 ExceptionHandler ,那就是默认 ExceptionHandler ,默认的 ExceptionHandler 是什么?
在 ExceptionHandlerMiddleware 的构造函数中找到了答案 —— 请求管线中的下一个中间件
if (_options.ExceptionHandler == null)
{
//...
_options.ExceptionHandler = _next;
}
只要将这里的 ExceptionHandler 修改为返回自定义错误页面的 handler ,问题就能解决。
ExceptionHandler 的类型是 RequestDelegate ,现在问题变成了如何在 RequestDelegate 中执行 mvc action 并返回响应内容?
在这个地方走了一些弯路,开始想通过 IActionResultExecutor 实现,但没成功。
后来想到最简单的方法就是利用已有的 mvc 中间件,基于 app.UseMvcWithDefaultRoute() 构建出 RequestDelegate 。基于这个思路,在 ExceptionHandlerExtensions 中以 Action<IApplicationBuilder> 为参数的扩展方法中学到一招:
var subAppBuilder = app.New();
configure(subAppBuilder);
var exceptionHandlerPipeline = subAppBuilder.Build();
return app.UseExceptionHandler(new ExceptionHandlerOptions
{
ExceptionHandler = exceptionHandlerPipeline
});
原来可以如此简单地通过 IApplicationBuilder 构建出 RequestDelegate 。
通过学习的这一招完美地解决了问题!
public static IApplicationBuilder UseCustomErrorPages(this IApplicationBuilder app)
{
var options = new ExceptionHandlerOptions
{
ExceptionHandlingPath = "/errors/500",
ExceptionHandler = app.New().UseMvcWithDefaultRoute().Build()
};
app.UseExceptionHandler(options);
app.UseStatusCodePagesWithReExecute("/errors/{0}");
return app;
}
|