DEV Community

WDSEGA
WDSEGA

Posted on

REST API Error Handling Best Practices

你有没有遇到过这样的API:返回200状态码,但body里藏着错误信息?或者500错误只返回一行"Internal Server Error",完全不知道哪里出了问题?好的错误处理不是锦上添花,而是API设计的基础设施。这篇文章我会从状态码选择、错误响应格式、到具体实现,完整地讲清楚REST API错误处理的最佳实践。

一、HTTP状态码的正确使用

状态码是API错误处理的第一层信息。很多开发者要么全部返回200,要么全部返回500,这两种极端都不对。

2xx 成功系列

状态码 含义 使用场景
200 OK 请求成功 GET请求返回数据
201 Created 资源创建成功 POST请求创建新资源
204 No Content 成功但无返回内容 DELETE请求成功删除

4xx 客户端错误

状态码 含义 使用场景
400 Bad Request 请求参数错误 缺少必填字段、格式不对
401 Unauthorized 未认证 缺少Token或Token过期
403 Forbidden 无权限 已认证但无权访问该资源
404 Not Found 资源不存在 请求的资源ID无效
409 Conflict 资源冲突 重复创建、版本冲突
422 Unprocessable Entity 语义错误 参数格式正确但语义不对
429 Too Many Requests 请求过于频繁 触发限流

5xx 服务端错误

状态码 含义 使用场景
500 Internal Server Error 服务器内部错误 未预期的异常
502 Bad Gateway 网关错误 上游服务不可用
503 Service Unavailable 服务不可用 维护中或过载

核心原则:4xx是客户端的错,5xx是服务端的错。不要用4xx掩盖服务端bug,也不要用5xx表示客户端参数错误。

二、统一的错误响应格式

选对状态码只是第一步,错误响应的body同样重要。推荐使用以下格式:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "请求参数验证失败",
    "details": [
      {
        "field": "email",
        "message": "邮箱格式不正确",
        "rejected_value": "not-an-email"
      },
      {
        "field": "age",
        "message": "年龄必须在18-120之间",
        "rejected_value": 15
      }
    ],
    "request_id": "req_abc123def456",
    "doc_url": "https://docs.example.com/errors/VALIDATION_ERROR"
  }
}
Enter fullscreen mode Exit fullscreen mode

这个格式包含了:

  • code:机器可读的错误码,方便客户端程序化处理
  • message:人类可读的错误描述
  • details:详细的错误信息列表(验证错误特别需要)
  • request_id:请求追踪ID,方便排查问题
  • doc_url:错误文档链接,帮助开发者自助解决

三、Python实现方案

以FastAPI为例,展示如何实现完整的错误处理体系:

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr, validator
from typing import Optional, List
import uuid
import time

app = FastAPI()

# 自定义异常类
class AppError(Exception):
    def __init__(self, code: str, message: str, status_code: int = 400,
                 details: Optional[list] = None):
        self.code = code
        self.message = message
        self.status_code = status_code
        self.details = details or []


class NotFoundError(AppError):
    def __init__(self, resource: str, resource_id: str):
        super().__init__(
            code="NOT_FOUND",
            message=f"{resource}不存在",
            status_code=404,
            details=[{"field": "id", "message": f"未找到ID为{resource_id}{resource}"}]
        )


class ConflictError(AppError):
    def __init__(self, message: str):
        super().__init__(
            code="CONFLICT",
            message=message,
            status_code=409
        )


class RateLimitError(AppError):
    def __init__(self, retry_after: int = 60):
        super().__init__(
            code="RATE_LIMIT_EXCEEDED",
            message=f"请求过于频繁,请在{retry_after}秒后重试",
            status_code=429
        )
        self.retry_after = retry_after


# 全局异常处理器
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
    request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": {
                "code": exc.code,
                "message": exc.message,
                "details": exc.details,
                "request_id": request_id,
                "doc_url": f"https://docs.example.com/errors/{exc.code}"
            }
        },
        headers={"X-Request-ID": request_id}
    )


@app.exception_handler(Exception)
async def general_error_handler(request: Request, exc: Exception):
    request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
    # 生产环境不要暴露异常详情
    import logging
    logging.error(f"[{request_id}] 未处理异常: {exc}", exc_info=True)

    return JSONResponse(
        status_code=500,
        content={
            "error": {
                "code": "INTERNAL_ERROR",
                "message": "服务器内部错误,请稍后重试",
                "details": [],
                "request_id": request_id
            }
        },
        headers={"X-Request-ID": request_id}
    )


# 请求ID中间件
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())[:12])
    request.state.request_id = request_id
    response = await call_next(request)
    response.headers["X-Request-ID"] = request_id
    return response


# 数据模型
class CreateUserRequest(BaseModel):
    email: EmailStr
    username: str
    age: int

    @validator("username")
    def validate_username(cls, v):
        if len(v) < 3 or len(v) > 20:
            raise ValueError("用户名长度必须在3-20之间")
        if not v.isalnum():
            raise ValueError("用户名只能包含字母和数字")
        return v

    @validator("age")
    def validate_age(cls, v):
        if v < 18 or v > 120:
            raise ValueError("年龄必须在18-120之间")
        return v


# 路由示例
@app.post("/users")
async def create_user(data: CreateUserRequest):
    # 检查用户名是否已存在
    if await check_username_exists(data.username):
        raise ConflictError(f"用户名 '{data.username}' 已被使用")

    user = await save_user(data)
    return {"id": user.id, "username": user.username}


@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await find_user(user_id)
    if not user:
        raise NotFoundError("用户", str(user_id))
    return user
Enter fullscreen mode Exit fullscreen mode

四、错误码设计规范

错误码是客户端程序化处理错误的关键。设计时遵循以下原则:

格式: {DOMAIN}_{SPECIFIC_ERROR}

示例:
VALIDATION_INVALID_EMAIL
VALIDATION_REQUIRED_FIELD
AUTH_TOKEN_EXPIRED
AUTH_INVALID_CREDENTIALS
RESOURCE_NOT_FOUND
RESOURCE_ALREADY_EXISTS
RATE_LIMIT_EXCEEDED
PAYMENT_CARD_DECLINED
PAYMENT_INSUFFICIENT_BALANCE
INTERNAL_SERVICE_UNAVAILABLE
INTERNAL_DATABASE_ERROR
Enter fullscreen mode Exit fullscreen mode

规则

  1. 全大写,用下划线分隔
  2. 第一段是领域(VALIDATION, AUTH, RESOURCE等)
  3. 具体到错误类型,不要用笼统的GENERAL_ERROR
  4. 新增错误码时更新文档

五、版本兼容性处理

API版本升级时,错误响应也要考虑兼容性:


python
# 在响应头中包含API版本
@app.middleware("http")

---
*本文首发于[我的技术博客](https://wdsega.github.io),欢迎访问获取更多技术文章。*
*如果你是内容创作者或自由职业者,推荐看看我整理的[Creator Pro Bundle](https://segauser.gumroad.com/l/rrhmbb)工具包,包含AI提示词系统、内容创作工具、副业指南和自动化脚本,源码全开放。*
Enter fullscreen mode Exit fullscreen mode

Top comments (0)