[ASP.NET Web API 2]如何使用 OWIN Middleware 捕捉 OWIN Host 引發的例外

Web API 的 ExceptionFilterAttribute、IExceptionFilter 能幫我們處理應用程式等級的例外,但無法處理載體 (Host) 的例外,當使用 IIS 作為載體,假使發生未處理的例外時會出現黃頁,OWIN 作為載體時,ExceptionFilterAttribute、IExceptionFilter 卻攔截不到錯誤,此時應該改用 Microsoft.Owin.Diagnostics.IAppBuilder.UseErrorPage 擴充方法,接下來將會利用 Error Handler 這個情境,介紹 OWIN Middleware 的幾種使用方式。

有關 OWIN 的介紹可以參考以下

ASP.NET: Understanding OWIN, Katana, and the Middleware Pipeline

 

開發環境

  • VS IDE 2019
  • .NET Framework 4.8

新增 Console App 專案範本,安裝以下套件

Install-Package Microsoft.AspNet.WebApi.OwinSelfHost
Install-Package Microsoft.Owin.Diagnostics
Install-Package Microsoft.Owin.Host.SystemWeb

定義 MyExceptionHandler 印出錯誤訊息

public class MyExceptionHandler : ExceptionHandler
{
    public override void Handle(ExceptionHandlerContext context)
    {
        Console.WriteLine(context.Exception.Message);
    }
}

 

使用 app.Use Middleware 引發錯誤

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        var config = new HttpConfiguration();

        // Web API configuration and services
        config.Services.Replace(typeof(IExceptionHandler), new MyExceptionHandler());

        SwaggerConfig.Register(config);

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute("DefaultApi",
                                   "api/{controller}/{id}",
                                   new {id = RouteParameter.Optional}
                                  );

        app.Use((ctx, next) =>
                {
                    var msg = "故意引發例外";
                    Console.WriteLine(msg);
                    throw new Exception(msg);
                })
           .UseWebApi(config);
    }
}

 

啟動 OWIN

internal class Program
{
    private const string HOST_ADDRESS = "http://localhost:9527";

    private static void Main(string[] args)
    {
        var webApp = WebApp.Start<Startup>(HOST_ADDRESS);
        Console.WriteLine($"伺服器已啟動, 位置:{HOST_ADDRESS}/swagger");
        Console.WriteLine("按下任意建離開應用程式");
        Console.ReadLine();
        webApp.Dispose();
    }
}

 

按 F5 執行站台,然後用瀏覽器訪問 http://localhost:9527,得到 HTTP ERROR 500 錯誤,主控台也沒有攔截到錯誤,服務不知道怎麼死掉的,這造成了除錯的難度,執行結果如下:

 

為了解決問題,再新增一個捕捉例外的 Middleware

完整代碼如下:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ...

        app.Use(async (ctx, next) =>
                {
                    try
                    {
                        await next();
                    }
                    catch (Exception ex)
                    {
                        this.ErrorHandle(ex, ctx);
                    }
                })
           .Use((ctx, next) =>
                {
                    var msg = "故意引發例外";
                    Console.WriteLine(msg);
                    throw new Exception(msg);
                })
           .UseWebApi(config);
    }

    private void ErrorHandle(Exception ex, IOwinContext context)
    {
        //紀錄詳細訊息,完整的例外推疊 ex.ToString()
        context.Response.StatusCode   = (int) HttpStatusCode.InternalServerError;
        context.Response.ReasonPhrase = "Internal Server Error";
        context.Response.ContentType  = "application/json";

        //回應前端,部份訊息,ex.Message
        context.Response.Write(ex.Message);
    }
}

 

Middleware 有順序,可依照自己的需求定義

 

攔截到 OWIN 的錯誤了,執行結果如下:

 

攔截例外的代碼擠在 Startup.cs 有點亂,可以把它抽成一個 Middleware 類別,他需要 Func<IDictionary<string, object>, Task> 物件,這個是用來收集、傳遞參數

using AppFunc = Func<IDictionary<string, object>, Task>;

public class ErrorHandler
{
    private readonly AppFunc _next;

    public ErrorHandler(AppFunc next)
    {
        if (next == null)
        {
            throw new ArgumentNullException("next");
        }

        this._next = next;
    }

    public async Task Invoke(IDictionary<string, object> environment)
    {
        try
        {
            await this._next(environment);
        }
        catch (Exception ex)
        {
            var owinContext = new OwinContext(environment);

            this.ErrorHandle(ex, owinContext);
        }
    }

    private void ErrorHandle(Exception ex, IOwinContext context)
    {
        context.Response.StatusCode   = (int) HttpStatusCode.InternalServerError;
        context.Response.ReasonPhrase = "Internal Server Error";
        context.Response.ContentType  = "application/json";
        context.Response.Write(ex.Message);
    }
}

 

調用端 app.Use<ErrorHandler>()

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ...
        app.Use<ErrorHandler>()
           .Use((ctx, next) =>
                {
                    var msg = "故意引發例外";
                    Console.WriteLine(msg);
                    throw new Exception(msg);
                })
           .UseWebApi(config);

    }       
}

 

或者是實作 OwinMiddleware,看起來更精簡

public class ErrorHandlerOwinMiddleware : OwinMiddleware
{
    public ErrorHandlerOwinMiddleware(OwinMiddleware next)
        : base(next)
    {
    }
 
    public override async Task Invoke(IOwinContext context)
    {
        try
        {
            await this.Next.Invoke(context);
        }
        catch (Exception ex)
        {
            try
            {
                this.Handle(ex, context);
                return;
            }
            catch (Exception)
            {
                // If there's a Exception while generating the error page, re-throw the original exception.
            }
 
            throw;
        }
    }
 
    private void Handle(Exception ex, IOwinContext context)
    {
        //Build a model to represet the error for the client
        context.Response.StatusCode   = (int) HttpStatusCode.InternalServerError;
        context.Response.ReasonPhrase = "Internal Server Error";
        context.Response.ContentType  = "application/json";
        context.Response.Write(JsonConvert.SerializeObject(ex));
    }
}

 

利用捕捉 OWIN Host 的案例,介紹了三種使用 Middleware 的方法,相信應該知道怎麼用它了,最後,在分享一個,Microsoft.Owin.Diagnostics 裡面有 IAppBuilder.UseErrorPage 擴充方法可以使用

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ...

        app.UseErrorPage()
           .UseWelcomePage("/")
           .Use((ctx, next) =>
                    {
                        var msg = "故意引發例外";
                        Console.WriteLine(msg);
                        throw new Exception(msg);
                    })
           .UseWebApi(config)
            ;
}

 

錯誤訊息相當的完整,執行結果如下:

 

範例位置

https://github.com/yaochangyu/sample.dotblog/tree/master/OWIN/Lab.OwinErrorHandler

 

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo