我看過有無數企業在設計 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)更加敏感。
相關連結