我看過有無數企業在設計 Web API 的時候,會將所有可能的回應訊息,無論成功或失敗,全部一律回應 HTTP 狀態碼 200 (OK)。但這樣的設計完全違反 RESTful 架構精神,我們應該盡可能透過狀態碼表明回應的狀態才對。明明是一份不 OK 的訊息,硬要回應 OK 真的很怪。我就來透過這篇文章,告訴你為什麼大家會這樣設計,以及怎樣設計才正確。
建立一個範例 API 專案
- 
先用 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
 
- 
建立並啟動一個全新 API 專案 建立專案 dotnet new webapi --name api1
cd api1
 啟動專案 
- 設定 ASPNETCORE_ENVIRONMENT環境變數為Production可避免顯示預設的開發人員錯誤頁面。
- 使用 dotnet watch run執行專案,會自動監視所有程式變更,有變更就會自動重新編譯。
- 加入 --no-launch-profile就不會讀取.vscode/launch.json的啟動設定。
- 當透過 Visual Studio Code 開啟專案時,預設會建立這個檔案,而 dotnet run或dotnet watch run預設會讀取這個檔案設定。
 
 set ASPNETCORE_ENVIRONMENT=Production
dotnet watch run --no-launch-profile
 連結到以下網址:https://localhost:5001/api/values/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";
} 
 
- 
測試錯誤畫面 此時你連到 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.
所以,要寫出正確的程式碼,要先從正確的觀念著手。
這邊我點出兩種需要例外管理的情境:
- 可以在 Controller 中捕捉到的例外。
- 無法在 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 的例子,如下範例:
- 
加一條 app.UseExceptionHandler("/api/error");到Startup.cs的Configure()方法中。
 if (env.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/api/error");
    app.UseHsts();
}
 
- 
然後新增一個 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)更加敏感。
相關連結