API 錯誤處理(Error Handling)

是什麼?

API 錯誤處理是設計一致且有用的錯誤回應格式,讓客戶端能快速理解問題所在並採取行動。RFC 7807(Problem Details for HTTP APIs)是業界標準的錯誤格式規範。

ℹ️RFC 7807 / RFC 9457

RFC 7807 定義了標準的錯誤回應格式,2023 年被 RFC 9457 取代(內容大致相同,更新了一些細節)。ASP.NET Core 內建支援 ProblemDetails 類別。

核心觀念

常見誤區

⚠️常見誤區

  • 所有錯誤都回 500:客戶端的錯(參數不對、未認證)用 4xx,只有伺服器自己的問題才用 5xx
  • 錯誤訊息直接丟 Exception Message:可能洩漏資料庫結構、內部路徑等敏感資訊
  • 每個 API 回傳不同的錯誤格式:整個系統必須統一錯誤格式,否則客戶端需要為每個 API 寫不同的錯誤處理邏輯

流程/步驟

1

定義錯誤格式

採用 RFC 7807 Problem Details 作為統一格式

2

分類錯誤碼

列出所有可能的錯誤類型,對應 HTTP 狀態碼

3

實作全域錯誤處理

用 Middleware / Exception Filter 統一攔截錯誤

4

加入驗證錯誤細節

Validation Error 附帶欄位層級的錯誤訊息

5

過濾敏感資訊

確保生產環境不洩漏 Stack Trace 和內部細節

流程解讀:先統一錯誤格式,再分類所有錯誤類型。透過全域 Middleware 攔截未處理的例外,確保所有錯誤都經過統一格式化。驗證錯誤要提供欄位層級的訊息,但必須過濾掉敏感資訊。

程式碼範例

C# 版本

csharp
// ASP.NET Core — Problem Details 全域錯誤處理
builder.Services.AddProblemDetails();
 
// 自訂 Exception Handler Middleware
app.UseExceptionHandler(appBuilder =>
{
    appBuilder.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;
 
        var problemDetails = new ProblemDetails
        {
            Type = "https://api.example.com/errors/internal",
            Title = "Internal Server Error",
            Status = 500,
            Detail = "An unexpected error occurred. Please try again later.",
            Instance = context.Request.Path
        };
 
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(problemDetails);
    });
});
 
// Controller 中的驗證錯誤回傳
[HttpPost]
public ActionResult<UserDto> Create(CreateUserDto dto)
{
    if (string.IsNullOrEmpty(dto.Email))
    {
        return BadRequest(new ValidationProblemDetails(
            new Dictionary<string, string[]>
            {
                ["email"] = new[] { "Email is required" }
            }
        ));
    }
    // ...
}

TypeScript 版本

typescript
// Express.js — 統一錯誤格式
interface ProblemDetails {
  type: string;
  title: string;
  status: number;
  detail: string;
  instance?: string;
  errors?: Record<string, string[]>;
}
 
// 全域錯誤處理 Middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  const problem: ProblemDetails = {
    type: "https://api.example.com/errors/internal",
    title: "Internal Server Error",
    status: 500,
    detail: "An unexpected error occurred.",
    instance: req.originalUrl,
  };
  res.status(500).json(problem);
});
 
// 驗證錯誤
app.post("/api/users", (req, res) => {
  const errors: Record<string, string[]> = {};
  if (!req.body.email) errors.email = ["Email is required"];
  if (!req.body.name) errors.name = ["Name is required"];
 
  if (Object.keys(errors).length > 0) {
    return res.status(400).json({
      type: "https://api.example.com/errors/validation",
      title: "Validation Error",
      status: 400,
      detail: "One or more fields failed validation.",
      errors,
    });
  }
});

Python 版本

python
# FastAPI — 統一錯誤處理
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
 
app = FastAPI()
 
class ApiError(Exception):
    def __init__(self, status: int, title: str, detail: str):
        self.status = status
        self.title = title
        self.detail = detail
 
@app.exception_handler(ApiError)
async def api_error_handler(request: Request, exc: ApiError):
    return JSONResponse(
        status_code=exc.status,
        content={
            "type": f"https://api.example.com/errors/{exc.status}",
            "title": exc.title,
            "status": exc.status,
            "detail": exc.detail,
            "instance": str(request.url),
        },
    )
 
@app.exception_handler(Exception)
async def generic_error_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=500,
        content={
            "type": "https://api.example.com/errors/internal",
            "title": "Internal Server Error",
            "status": 500,
            "detail": "An unexpected error occurred.",
        },
    )

架構圖/概念圖

Client Request
normal flow
Error Middleware
formats
Controller / Handler
throws exception
ProblemDetails (RFC 7807)
returns
JSON Response

正常流程中請求直接進入 Controller。當錯誤發生時,Exception 被 Error Middleware 攔截,轉換為標準的 ProblemDetails 格式後回傳。這確保所有錯誤都經過統一處理。

實戰補充

Q: 前端團隊抱怨錯誤訊息不統一,怎麼推動改善?

A: 先定義一份「Error Contract」文件,規範所有 API 錯誤都用 Problem Details 格式。用全域 Middleware 攔截,確保即使開發者忘了處理,也會有標準格式的錯誤回應。

Q: 要不要在錯誤回應中包含 Error Code?

A: 建議加上。除了 HTTP 狀態碼,自定義的 Error Code(如 USER_EMAIL_DUPLICATE)能讓前端做更精確的錯誤處理,例如在特定欄位下方顯示對應的提示訊息。

理解測驗

🤔 RFC 7807 Problem Details 中,哪個欄位用來放人類可讀的錯誤描述?

🤔 使用者送了一個 email 格式不正確的請求,應該回傳什麼狀態碼?

🤔 為什麼生產環境的錯誤回應不應包含 Stack Trace?

重點整理

💡一句話記住

好的錯誤回應 = 統一格式 + 有用資訊 + 不洩密。 口訣:「Problem Details 一統天下」

| 概念 | 說明 | |------|------| | RFC 7807 | 標準錯誤回應格式(type/title/status/detail/instance) | | 4xx vs 5xx | 4xx 客戶端錯(可修正)、5xx 伺服器錯(無法修正) | | Validation Error | 400 + 欄位層級錯誤訊息 | | 全域 Middleware | 攔截所有未處理例外,確保統一格式 | | 安全原則 | 不洩漏 Stack Trace、SQL、內部路徑 |

你可能也想看

API 版本控制分頁、過濾與排序

按 ← → 鍵切換課程