The Will Will Web | 如何自訂 ASP.NET Core Web API 的錯誤回應訊息

The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何自訂 ASP.NET Core Web API 的錯誤回應訊息

我看過有無數企業在設計 Web API 的時候,會將所有可能的回應訊息,無論成功或失敗,全部一律回應 HTTP 狀態碼 200 (OK)。但這樣的設計完全違反 RESTful 架構精神,我們應該盡可能透過狀態碼表明回應的狀態才對。明明是一份不 OK 的訊息,硬要回應 OK 真的很怪。我就來透過這篇文章,告訴你為什麼大家會這樣設計,以及怎樣設計才正確。

建立一個範例 API 專案

  1. 先用 dotnet --info 確認一下目前使用的 .NET Core 版本資訊。

    我目前使用的版本是:

    • .NET Core SDK: 2.1.401
    • .NET Core Runtimes: 2.1.3
      • Microsoft.NETCore.App
      • Microsoft.AspNetCore.App

    詳細如下:

    .NET Core SDK (reflecting any global.json):
     Version:   2.1.401
     Commit:    91b1c13032
    
    Runtime Environment:
     OS Name:     Windows
     OS Version:  10.0.17134
     OS Platform: Windows
     RID:         win10-x64
     Base Path:   C:\Program Files\dotnet\sdk\2.1.401\
    
    Host (useful for support):
      Version: 2.1.3
      Commit:  124038c13e
    
  2. 建立並啟動一個全新 API 專案

    建立專案

    dotnet new webapi --name api1
    cd api1
    

    啟動專案

    • 設定 ASPNETCORE_ENVIRONMENT 環境變數為 Production 可避免顯示預設的開發人員錯誤頁面。
    • 使用 dotnet watch run 執行專案,會自動監視所有程式變更,有變更就會自動重新編譯。
    • 加入 --no-launch-profile 就不會讀取 .vscode/launch.json 的啟動設定。
      • 當透過 Visual Studio Code 開啟專案時,預設會建立這個檔案,而 dotnet rundotnet watch run 預設會讀取這個檔案設定。
    set ASPNETCORE_ENVIRONMENT=Production
    dotnet watch run --no-launch-profile
    

    連結到以下網址:https://localhost:5001/api/values/1 查看內容。

設定發生例外的條件

  1. 修改 Controllers/ValuesController.cs 檔案

    我們直接修改 public ActionResult<string> Get(int id) 方法,加入會發生例外狀況的程式碼。範例如下:

    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        if (id <= 0)
        {
            throw new ArgumentException("id must larger than zero.");
        }
        return "value";
    } 
  2. 測試錯誤畫面

    此時你連到 https://localhost:5001/api/values/0 就會得到一個 HTTP 狀態碼 500 的錯誤訊息,一個回應內容長度為 0 的訊息。

為什麼大家習慣用 HTTP 狀態碼 200 來回應訊息?

在許多企業都會規範錯誤代碼管理,也就是所有 API 的回應訊息通常有一定的格式,而且不同的狀態碼所代表的意義,也會依據不同系統而有所不同,有些系統的狀態碼可能多達數百個到數千個之多。

我想大部分的人應該都同意,使用 RESTful 架構開發 API 就應該多利用 HTTP 狀態碼,盡量將錯誤代碼都透過 HTTP 狀態碼來回應。但現實上,幾百個不同的代碼,怎麼可能找的到相對應的 HTTP 狀態碼可以表示。我只能說 HTTP 狀態碼只是對 HTTP 回應的訊息進行大致分類,詳細的錯誤訊息與代碼,當然是寫在 HTTP 回應內文中。

對一個剛學習 ASP.NET Web API 開發的人來說,可能不太清楚怎樣自訂 HTTP 500 的錯誤回應訊息。所以一般人在專案時程壓力下,索性就直接 try/catch 所有錯誤,全以 HTTP 200 來回應了。這實在不是明智之舉,因為這會造成整體系統的技術債,對長遠的可維護性來說相當不利,程式碼的可讀性也會變差。

所以這篇文章,就來說說如何在 ASP.NET Core Web API 中自訂一份含有完整訊息的 HTTP 500 錯誤回應。

如何自訂 HTTP 500 錯誤訊息內容

在使用 ASP.NET Web API 2 的例外處理時,有個 HttpResponseException 類別可用,它可以讓你自訂一個 HTTP 回應的例外,而且可以自訂狀態碼與回應訊息內容。

不過這個類別已經從 ASP.NET Core 完整移除,原因在 Why no HttpResponseException? #4311 討論串中有提到。負責整個 ASP.NET Core 設計架構的 David Fowler 明確提到「我們不希望人們使用例外來控制程式執行的流程,因為大部分人在商業邏輯中使用例外的方式都錯的離譜。」。原文如下:

Just the classic, "we don't want people using exceptions for control flow" paradigm. People did things like use it from within their business logic which is all sorts of wrong. I can't speak to the performance issues because I haven't measured but throwing an exception that is meant to be caught to get some data is worse than just returning that result.

所以,要寫出正確的程式碼,要先從正確的觀念著手。

這邊我點出兩種需要例外管理的情境:

  1. 可以在 Controller 中捕捉到的例外。
  2. 無法在 Controller 中捕捉到的例外。

第 1 種情境,其實是相對單純。因為你可以直接從 Controller 內,透過 ActionResult 來回應自定的錯誤訊息:

public ActionResult<string> Get(int id)
{
    if (id <= 0)
    {
        return StatusCode((int)HttpStatusCode.InternalServerError, new {
            errorno = 1,
            message = "id must larger than zero."
        });
    }

    return "value";
}

你也可以透過 ASP.NET Core 內建的例外處理機制進行捕捉例外,例如透過 Exception Filter 來回應例外的回應訊息。

如果真的不容易捕捉,也可以透過 try/catch 並搭配 IActionResult 來回應含有 HTTP 500 狀態碼的回應訊息。當然,這不是個好做法,建議不要透過 try/catch 擾亂程式流程。如我不用 try/catch 的話,就要考慮第 2 種情境的解法。

第 2 種情境,就如我所說的,當你不想用 try/catch 捕捉例外,那就要從更底層的 Middleware 著手,你可以利用 UseExceptionHandler 去捕捉所有沒被 ASP.NET Core Web API 捕捉到例外狀況。

在預設的情況下,如果 Kestrel 捕捉到應用程式未處理的例外狀況,會直接回應 HTTP 500 狀態碼,而且沒有任何內容。如果要回應一些具體的訊息,你可以參考 處理 ASP.NET Core 中的錯誤 文件,裡面有提到許多處理錯誤的方法,也提到了許多處理錯誤的觀念,建議大家可以認真看過一遍。

這份文件有提到 app.UseExceptionHandler("/error"); 方法,可以讓你設定自訂的例外狀況處理頁面。不過,這裡的範例是以 Razor Pages 與 MVC 錯誤頁面回應。我這邊特別寫一組 Web API 的例子,如下範例:

  1. 加一條 app.UseExceptionHandler("/api/error");Startup.csConfigure() 方法中。

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/api/error");
        app.UseHsts();
    }
    
  2. 然後新增一個 ErrorController 處理所有例外錯誤

    這邊我先自訂一個 ErrorResponseVM 模型類別,專門用來回應制式的例外錯誤訊息。

    public class ErrorResponseVM
    {
        public int errorno { get; set; }
        public string message { get; set; }
    }
    

    完整的 ErrorController 範例程式如下:

    [Route("api/error")]
    public class ErrorController : Controller
    {
        [AllowAnonymous]
        public ActionResult<ErrorResponseVM> Error()
        {
            return new ErrorResponseVM()
            {
                errorno = 1,
                message = "Normal Error"
            };
        }
    }
    

    ※ 請記得引用 using Microsoft.AspNetCore.Authorization; 命名空間。

如此一來,就可以自動捕捉所有例外狀況了,包含不是 ASP.NET Core 產生的例外,也都可以透過 /api/error 來回應訊息。

如何取得真正的 Exception 例外物件

感謝 .NET Core 內建的 DI (相依性注入) 機制,讓這一切變得相當簡單。想取得例外,只要一行程式碼,就可以取得例外處理的特性物件:

var ex = HttpContext.Features.Get<IExceptionHandlerFeature>();

接著只要透過 ex.Error 就可以取得完整的例外物件。

完整範例如下:

[Route("api/error")]
public class ErrorController : Controller
{
    [AllowAnonymous]
    public ActionResult<ErrorResponseVM> Error()
    {
        var ex = HttpContext.Features.Get<IExceptionHandlerFeature>();
        if (ex != null)
        {
            return StatusCode(
                (int) HttpStatusCode.InternalServerError,
                new ErrorResponseVM()
                {
                    errorno = 999,
                    message = ex.Error.Message
                });
        }
        else
        {
            return StatusCode(
                (int) HttpStatusCode.InternalServerError,
                new ErrorResponseVM()
                {
                    errorno = 999,
                    message = "ERROR OCCURRED!"
                });
        }

    }
}

結語

本篇文章,詳細說明在 ASP.NET Core Web API 中自訂錯誤訊息的方法,幫助大家更容易寫出 RESTful 的 Web API 服務。用正確的觀念寫 Code,一直是我特別強調的開發方式。

雖然有時候專案在趕,真的沒時間、沒預算去改。但有空的時候,身為一個開發人員,最好還是要花點時間 Review 曾經寫過的程式碼 (即便 Code 可能不是你寫的),透過重構不斷改善程式碼品質,一來可以改善程式碼品質,二來也可以訓練自己對程式碼的好壞(Code Smell)更加敏感。

相關連結