NET8 WebAPI 錯誤訊息回應研究
引言
又一個無言以對的問題。若 WebAPI 執行都成功如預期那就好辦了。 實際上會有失敗的狀況,而錯誤訊息回應並沒有標準答案也沒有通同典範。 剛好有機會就研究一下。
論 HTTP Status Code
HTTP Status Code 指向成功的代碼有 200, 204,這 200 是文字、HTML、JSON、FILE 由工程師決定,這在實務上沒有問題,因為已預期成功送回的格式。 HTTP Status Code 指向失敗的代碼有 4XX, 5XX 一大堆。對工程師有意義的只有 400, 422, 500。簡介如下:
200 OK
格式未知,不過可預期實務上沒有問題。
204 NoContent
成功,沒有回傳內容。實務上是有的。
400 Bad Request
格式未知,也不可預期,實務上問題可大了。
401 Unauthorized
未授權。一般指未登入。(不在本文討論)
403 Forbidden
授權不足。一般指已登入但權限不夠不能執行某些功能。(不在本文討論)
404 Not Found
功能不存在。(不在本文討論)
422 Unprocessable Entity / Unprocessable Content
文字描述有二種不過意義是一樣的,就是客戶端給的封包不能處理。 原因當然是封包內容驗證失敗。送回錯誤原由建議用 JSON 送回。
500 Internal Server Error
基本上是系統級錯誤,一般轉成文字再處置。
先下結論
以 Web API 來說, 成功訊息就回傳 200 JSON object。若是下載檔案也是預期之中看是 byte[] 還是 stream 都是預期之中。 另一個成功訊息 204 NoContent 也是預期之中的格式處置上沒有問題。 失敗訊息可能是4xx或5xx。建議直接送回以文字解析就好,不強改 status code 因為可能發生預期之外的系統級例外。進一步的失敗訊息可用 422 送回 JSON 格式的錯誤訊息,比如放表單的 validation message。
回應指令成功指令有2個 Ok()
,NoContent()
,不過失敗指令有數個:BadRequest()
、UnprocessableEntity()
、ValidationProblem()
、Problem()
、throw out exception!!! 。
簡述封包送到前端的解析動作
Ok(JSON | FILE) => 200 JSON | FILE
NoContent => empty
BadRequest(TEXT) => 400 TEXT
UnprocessableEntity(JSON) => 422 JSON --- 建議放 validation message。
Problem(...) => 500 TEXT --- 這指令不建議使用。一般是例外無法處置且機率不大,乾脆轉成文字就好。
ValidationProblem(...) => 400 TEXT --- 這指令不建議使用,因為與 BadRequest 的 status code 突衝(囧)。
throw out exception!!! => 500 TEXT --- 這不該發生,莫非定律說就是會發生。
其他4xx => 4xx TEXT
其他5xx => 5xx TEXT
結論的結論
一般用下面指令與格式送回封包就可以了,暨符合學理又實務。權限相關的 401, 403, 404 等不在本文討論。
成功,有結果 => Ok(JSON | FILE) => 200 JSON | FILE
成功,無結果 => NoContent => 204 empty
表單驗證失敗 => UnprocessableEntity(JSON) => 422 JSON --- (option)
錯誤與例外 => BadRequest(TEXT) => 400 TEXT
系統級例外 => (system throw out) => 500 TEXT
回傳指令與回應封包紀錄
Ok(JSON) ==> 200; application/json;
return Ok(new { Message = $"你輸入了{num} JSON" });
==>
Status: 200 OK
application/json; charset=utf-8, 34 bytes
{
"message": "你輸入了200 JSON"
}
Ok(TEXT) ==> 200; text/plain;
return Ok($"你輸入了{num}");
Status: 200 OK
text/plain; charset=utf-8, 15 bytes
你輸入了201
NoContent() ==> 204;
return NoContent();
Status: 204 No Content
0 bytes
BadRequest(JSON)==> 400; application/json;
return BadRequest(new { Message = "模擬錯誤訊息 JSON。" });
Status: 400 Bad Request
application/json; charset=utf-8, 40 bytes
{
"message": "模擬錯誤訊息 JSON。"
}
BadRequest(TEXT)==> 400; text/plain;
return BadRequest("模擬錯誤訊息 TEXT。");
Status: 400 Bad Request
text/plain; charset=utf-8, 26 bytes
模擬錯誤訊息 TEXT。
UnprocessableEntity(JSON)==> 422; application/json;
return UnprocessableEntity(new { Message = "模擬錯誤訊息 JSON。" });
Status: 422 Unprocessable Entity
application/json; charset=utf-8, 40 bytes
{
"message": "模擬錯誤訊息 JSON。"
}
UnprocessableEntity(TEXT)==> 422; text/plain;
return UnprocessableEntity("模擬錯誤訊息 TEXT。");
Status: 422 Unprocessable Entity
text/plain; charset=utf-8, 26 bytes
模擬錯誤訊息 TEXT。
ValidationProblem(TEXT)==>
400; application/problem+json;
return ValidationProblem("模擬錯誤訊息 TEXT。");
Status: 400 Bad Request
application/problem+json; charset=utf-8, 242 bytes
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"detail": "模擬錯誤訊息 TEXT。",
"errors": {},
"traceId": "00-ee20408ed761a2a36ddb449bb160b5d4-f94a3619b8b73459-00"
}
ValidationProblem(this.ModelState)==>
400; application/problem+json;
this.ModelState.AddModelError("某個欄位", "模擬某個欄位錯誤訊息!");
return ValidationProblem(this.ModelState);
Status: 400 Bad Request
application/problem+json; charset=utf-8, 254 bytes
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"某個欄位": [
"模擬某個欄位錯誤訊息!"
]
},
"traceId": "00-5d453b222aaa907c2269b9698f38528d-4539c827740335df-00"
}
Problem(TEXT) ==>
500; application/problem+json;
return Problem("模擬錯誤訊息 TEXT。");
Status: 500 Internal Server Error
application/problem+json; charset=utf-8, 239 bytes
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An error occurred while processing your request.",
"status": 500,
"detail": "模擬錯誤訊息 TEXT。",
"traceId": "00-ad743e71650cbbae96626ef2572375cb-a6ea3a8d5f61c3be-00"
}
throw out ==>
500; text/plain;
throw new ApplicationException("模擬錯誤訊息 TEXT。");
Status: 500 Internal Server Error
text/plain; charset=utf-8, 2730 bytes
System.ApplicationException: 模擬錯誤訊息 TEXT。
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Rin.Middlewares.DiagnosticsMiddleware.InvokeAsync(HttpContext context)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Rin.Middlewares.RequestRecorderMiddleware.InvokeAsync(HttpContext context, RinOptions options)
at Rin.Middlewares.RequestRecorderMiddleware.InvokeAsync(HttpContext context, RinOptions options)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Host: localhost:7144
traceparent: 00-b323e54f2f4be7dddfbaab2761d85c1c-f3cb38ac9d6283c4-00
Content-Length: 0
附上測試程式碼
[AllowAnonymous]
[Http G("[action]/{num}")]
public IActionResult FailHandlingLab(int num)
{
try
{
if (num == 400)
return BadRequest(new { Message = "模擬錯誤訊息 JSON。" });
if (num == 4002)
return BadRequest("模擬錯誤訊息 TEXT。");
if (num == 4003)
return ValidationProblem("模擬錯誤訊息 TEXT。");
if (num == 4004)
return ValidationProblem("模擬錯誤訊息 TEXT。", "我是 instance", 400, "我是 title", "我是 type");
if (num == 4005)
{
this.ModelState.AddModelError("某個欄位", "模擬某個欄位錯誤訊息!");
return ValidationProblem(this.ModelState);
}
if (num == 422)
return UnprocessableEntity("模擬錯誤訊息 TEXT。");
if (num == 4222)
return UnprocessableEntity(new { Message = "模擬錯誤訊息 JSON。" });
if (num == 500)
return Problem("模擬錯誤訊息 TEXT。");
if (num == 600)
throw new ApplicationException("模擬錯誤訊息 TEXT。");
if (num == 200)
return Ok(new { Message = $"你輸入了{num} JSON" });
if (num == 204)
return NoContent();
return Ok($"你輸入了{num}");
}
catch
{
throw;
}
}
(EOF)
Last updated