Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 903|回复: 0

解决 ASP.NET Core 自定义错误页面对 Middleware 异常无效的问题

[复制链接]
  • TA的每日心情
    奋斗
    4 天前
  • 签到天数: 803 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726482
    发表于 2021-9-7 16:47:56 | 显示全部楼层 |阅读模式

    我们基于 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;
    }

     

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-11-21 18:18 , Processed in 1.079902 second(s), 30 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表