1. 概述

REST 是一种无状态架构,客户端通过它访问并操作服务器上的资源。通常,REST 服务基于 HTTP 协议暴露一组资源,并提供 API 供客户端获取或修改这些资源的状态。

本文将深入探讨 REST API 错误处理的最佳实践,包括如何向调用方提供有意义的错误信息、主流平台的实际案例,以及通过一个 Spring REST 应用示例进行具体实现。

核心目标是:✅ 让客户端清晰理解错误原因,同时避免暴露敏感内部细节 ❌。


2. HTTP 状态码

当客户端发起请求并被服务器成功接收后,服务器必须明确告知该请求是否处理成功。

HTTP 使用五类状态码来传达这一信息:

  • 1xx(信息性):请求已接收,继续处理
  • 2xx(成功):请求已成功处理
  • 3xx(重定向):需要客户端进一步操作才能完成请求
  • 4xx(客户端错误):请求本身有问题,例如参数错误或权限不足
  • 5xx(服务器错误):服务器在处理合法请求时出错

客户端可以根据状态码快速判断请求结果。比如看到 404 就知道资源不存在,403 表示没权限,而 500 则意味着服务端出了问题。


3. 错误处理策略

错误处理的第一步是返回合适的 HTTP 状态码。但仅靠状态码往往不够,我们通常还需要在响应体中补充更多上下文信息。

3.1 基础响应

最简单的做法就是返回恰当的状态码。以下是常见场景:

  • 400 Bad Request:请求格式错误,如缺少必要参数或 body 格式不对
  • 401 Unauthorized:未提供身份认证信息或认证失败
  • 403 Forbidden:已认证但无权访问目标资源
  • 404 Not Found:请求的资源不存在
  • 412 Precondition Failed:请求头中的条件不满足(如 ETag 不匹配)
  • 500 Internal Server Error:服务端发生未预期异常
  • 503 Service Unavailable:服务暂时不可用(如正在维护)

⚠️ 特别注意:虽然 500 是默认兜底错误码,但我们应尽量避免直接抛出。
比如查询一本书但 ID 不存在,应主动捕获并返回 404,而不是让系统抛异常导致 500
否则不仅误导调用方以为是服务端问题,还可能掩盖真正的业务逻辑错误。

✅ 正确姿势:将可预知的异常转化为具体状态码,只在真正“不可控”的情况下才用 500

3.2 Spring 默认错误响应

Spring 的默认异常处理机制遵循了上述原则。以一个管理图书的 REST 接口为例:

curl -X GET -H "Accept: application/json" http://localhost:8082/spring-rest/api/book/1

如果 ID 为 1 的书不存在,控制器抛出 BookNotFoundException,默认响应如下:

{
    "timestamp":"2019-09-16T22:14:45.624+0000",
    "status":500,
    "error":"Internal Server Error",
    "message":"No message available",
    "path":"/api/book/1"
}

这个默认结构包含时间戳、状态码、错误类型、消息和路径,便于排查问题。

但问题来了:Spring 默认把所有未处理异常都映射为 500,这显然不合理。

✅ 解决方案:使用 @ControllerAdvice 全局拦截异常,将 BookNotFoundException 映射为 404

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BookNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public @ResponseBody ErrorResponse handleBookNotFound(BookNotFoundException ex) {
        return new ErrorResponse("book-404", "Book not found", ex.getMessage());
    }
}

这样就能把“业务性不存在”和“系统崩溃”区分开,提升 API 可用性。

3.3 更详细的错误响应

有时候状态码 + 简单消息仍不足以说明问题。这时就需要在响应体中提供更多细节。

推荐结构包含以下字段:

  • error:应用级唯一错误码(非 HTTP 状态码)
  • message:简洁的人类可读提示(可用于前端展示)
  • detail:详细解释,供开发者调试使用

例如登录失败时返回:

{
    "error": "auth-0001",
    "message": "Incorrect username and password",
    "detail": "Ensure that the username and password included in the request are correct"
}

关于字段设计的几点建议:

  • error 字段应为应用内唯一的标识符,常见格式如 auth-0001validation-002,便于日志追踪和文档索引。
  • message 要简洁且用户友好,若支持国际化(i18n),需根据 Accept-Language 头自动翻译。
  • detail 面向开发者,无需翻译,可包含技术细节,如字段校验规则、依赖服务名等。

可选扩展字段

还可添加帮助链接,方便开发者快速定位:

{
    "error": "auth-0001",
    "message": "Incorrect username and password",
    "detail": "Ensure that the username and password included in the request are correct",
    "help": "https://api.example.com/docs/errors/auth-0001"
}

多错误响应

当一次请求涉及多个校验项时,可能需要返回多个错误:

{
    "errors": [
        {
            "error": "auth-0001",
            "message": "Incorrect username and password",
            "detail": "Ensure credentials are correct",
            "help": "https://api.example.com/docs/errors/auth-0001"
        },
        {
            "error": "rate-limit-001",
            "message": "Too many login attempts",
            "detail": "Account locked for 15 minutes due to excessive failed logins",
            "help": "https://api.example.com/docs/errors/rate-limit-001"
        }
    ]
}

⚠️ 注意:是否返回多个错误取决于业务复杂度。简单场景下返回第一个或最严重错误即可,避免过度设计。

3.4 标准化响应体:RFC 7807

不同团队对错误结构的设计五花八门,导致客户端难以统一处理。为此,IETF 推出了 RFC 7807,定义了一种标准化的问题详情格式(Problem Details for HTTP APIs)。

其核心字段如下:

字段 说明
type 错误类型的 URI 标识符(分类)
title 错误简述(人类可读)
status HTTP 状态码(可选)
detail 具体错误描述
instance 当前错误实例的唯一标识 URI

示例:

{
    "type": "/errors/incorrect-user-pass",
    "title": "Incorrect username or password.",
    "status": 401,
    "detail": "Authentication failed due to incorrect username or password.",
    "instance": "/login/log/abc123"
}

💡 type 类似于错误类名,instance 则是具体实例。客户端可通过 type URI 获取错误文档,实现类似 HATEOAS 的导航能力。

虽然 RFC 7807 不是强制标准,但在微服务或开放平台中采用它,能显著提升 API 的一致性与可维护性。


4. 实际案例分析

主流平台虽各有实现细节,但错误处理的核心模式高度一致。

4.1 Twitter API

发送一个缺少认证信息的请求:

curl -X GET https://api.twitter.com/1.1/statuses/update.json?include_entities=true

响应如下:

{
    "errors": [
        {
            "code":215,
            "message":"Bad Authentication data."
        }
    ]
}

特点:

  • 使用数组包装错误(支持多错误)
  • 提供数字型 code 和可读 message
  • 未返回详细解释或帮助链接
  • 实际 HTTP 状态码为 400,而非更精确的 401

✅ 踩坑提醒:Twitter 选择用通用 400 而非 401,可能是为了简化后端异常分类逻辑。但在内部系统中,建议还是尽量细化状态码,便于前端做差异化处理。

4.2 Facebook Graph API

调用 OAuth 接口时遗漏必要参数:

curl -X GET https://graph.facebook.com/oauth/access_token?client_id=foo&client_secret=bar&grant_type=baz

返回:

{
    "error": {
        "message": "Missing redirect_uri parameter.",
        "type": "OAuthException",
        "code": 191,
        "fbtrace_id": "AWswcVwbcqfgrSgjG80MtqJ"
    }
}

亮点:

  • type 字段明确错误类别(OAuthException)
  • code 提供机器可读编号
  • fbtrace_id 是内部追踪 ID,方便技术支持定位日志

💡 这种设计非常适合大型平台:外部暴露有限信息,内部可通过 trace ID 快速查日志,安全又高效。


5. 总结

REST API 错误处理的关键原则:

使用最具体的 HTTP 状态码
避免滥用 500,把业务异常转化为明确的 4xx

在响应体中提供结构化错误信息
至少包含错误码、用户提示、开发详情,必要时附帮助链接。

考虑标准化(如 RFC 7807)
尤其适用于对外开放或跨团队协作的 API。

统一异常处理机制
借助 Spring 的 @ControllerAdvice 实现全局拦截,减少重复代码。

最终目标是:让调用方既能快速定位问题,又不会被无关细节干扰。简洁、清晰、一致,才是高质量 API 的标志。

文中示例代码可在 GitHub 获取:https://github.com/eugenp/tutorials/tree/master/spring-web-modules/spring-rest-simple


原始标题:Best Practices for REST API Error Handling