REST API错误处理的最佳实践

1. 介绍

REST是一种无状态的架构,客户端可以在其中访问和操作服务器上的资源。通常,REST服务利用HTTP发布它们管理的一组资源,并提供允许客户机获取或更改这些资源状态的API。

在本教程中,我们将学习处理REST API错误的一些最佳实践,包括为用户提供相关信息的有用方法、来自大型网站的示例以及使用示例Spring REST应用程序的具体实现。

2. HTTP状态码

当客户端向HTTP服务器发出请求时——服务器成功接收到请求——服务器必须通知客户端请求是否被成功处理。HTTP完成这与五类状态代码:

  • 10x(信息性): 服务器确认请求
  • 20x(成功): 服务器按预期完成请求
  • 30x(重定向): 客户端需要执行进一步的操作来完成请求
  • 40x(客户端错误): 客户端发送了一个无效的请求
  • 50x(服务器错误): 服务器由于服务器错误而无法满足有效请求

客户端可以根据响应代码推测特定请求的结果。

3.处理错误

处理错误的第一步是向客户机提供正确的状态码。此外,我们可能需要在响应体中提供更多信息。

3.1 基本响应

处理错误最简单的方法是使用适当的状态码进行响应。

一些常见的回应码包括:

  • 400错误的请求: 客户端发送了一个无效的请求,例如缺少必需的请求体或参数
  • 401未经授权: 客户端对服务器进行身份验证失败
  • 403禁止: 经过身份验证的客户端,但没有访问请求资源的权限
  • 404未找到: 所请求的资源不存在
  • 412先决条件失败: 请求头字段中的一个或多个条件被评估为false
  • 500内部服务器错误: 一个通用错误发生在服务器上
  • 503服务不可用: 所请求的服务不可用

虽然很基本,但这些代码允许客户机了解所发生错误的广泛性质。例如,我们知道如果我们收到一个403错误,说明我们没有权限访问我们请求的资源。

然而,在许多情况下,我们需要在我们的答复中提供补充细节。

500错误表明服务器在处理请求时发生了一些问题或异常。一般来说,这个内部错误与我们的客户无关。

因此,为了尽量减少对客户机的响应,我们应该努力尝试处理或捕获内部错误,并在可能的情况下使用其他适当的状态代码进行响应。例如,如果由于请求的资源不存在而发生异常,我们应该将其公开为404错误,而不是500错误。

这并不是说不应该返回500,而是说应该将其用于阻止服务器执行请求的意外情况(如服务中断)。

3.2. 默认Spring错误响应

这些原则是如此普遍,以至于Spring已经在其默认的错误处理机制中编写了它们。

为了演示,假设我们有一个简单的Spring REST应用程序,它管理图书,有一个端点根据ID检索图书:

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

如果没有ID为1的书,我们期望控制器会抛出BookNotFoundException异常。在这个端点上执行GET,我们看到这个异常被抛出,响应体为:

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

注意,这个默认的错误处理程序包括错误发生时的时间戳、HTTP状态代码、标题(错误字段)、消息(默认为空)和错误发生时的URL路径。

这些字段为客户端或开发人员提供信息,以帮助解决问题,还构成了标准错误处理机制的一些字段。

另外,请注意,当BookNotFoundException被抛出时,Spring会自动返回一个HTTP状态码为500。尽管有些api会返回500状态码或其他通用代码,正如我们将在Facebook和Twitter api中看到的那样——为了简单起见,对于所有错误,最好尽可能使用最具体的错误代码。

在我们的示例中,我们可以添加一个@ControllerAdvice,这样当BookNotFoundException被抛出时,我们的API会返回一个状态404,表示没有找到,而不是500内部服务器错误。

3.3. 更多的响应细节

正如在上面的Spring示例中看到的,有时状态代码不足以显示错误的细节。在需要时,我们可以使用响应体向客户机提供附加信息。在提供详细回应时,我们应包括:

  • 错误:错误的唯一标识符
  • 消息:一个简短的人类可读的消息
  • 细节: 对错误的更长的解释

例如,如果客户端发送了一个带有错误凭据的请求,我们可以发送一个包含以下内容的401响应:

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

错误字段不应该与响应代码匹配。相反,它应该是应用程序特有的错误代码。通常,错误字段没有约定,希望它是唯一的。

通常,该字段只包含字母数字和连接字符,如破折号或下划线。例如,0001、auth-0001和incorrect-user-pass都是错误代码的典型示例。

通常认为主体的消息部分在用户界面上是可显示的。因此,如果我们支持国际化,就应该翻译这个标题。因此,如果客户端发送一个带有对应于法语的Accept-Language头的请求,则title值应该被翻译成法语。

细节部分是为客户端的开发人员而不是最终用户使用的,因此不需要进行翻译。

此外,我们还可以提供一个URL -如帮助字段-客户可以跟踪发现更多的信息:

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

有时,我们可能希望为一个请求报告多个错误。在这种情况下,我们应该返回一个列表中的错误:

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

当出现单个错误时,我们使用包含一个元素的列表进行响应。注意,对于简单的应用程序来说,响应多个错误可能过于复杂。在许多情况下,使用第一个或最重要的错误来响应就足够了。

3.4. 标准响应体

虽然大多数REST api遵循类似的约定,但具体细节通常不同,包括字段的名称和响应体中包含的信息。这些差异使得库和框架很难统一地处理错误。

为了标准化REST API错误处理,IETF设计了RFC 7807,它创建了一个通用的错误处理模式。

这个方案由五部分组成:

  • 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分别以类似于类和对象的方式标识错误的特定发生。

通过使用uri,客户机可以按照这些路径查找有关错误的更多信息,就像使用HATEOAS链接导航REST API一样。

4. 示例

上述实践在一些最流行的REST api中很常见。虽然字段或格式的具体名称可能在不同的站点之间有所不同,但一般的模式几乎是通用的。

4.1. Twitter

例如,让我们发送一个GET请求而不提供必需的身份验证数据:

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

Twitter API响应一个错误,如下正文:

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

此响应包括一个包含单个错误的列表,以及错误代码和消息。在Twitter的例子中,没有详细的信息,并且使用一个普遍的错误——而不是更具体的401错误——来表示认证失败。

有时更通用的状态代码更容易实现,我们将在下面的Spring示例中看到这一点。它允许开发人员捕获异常组,而不区分应该返回的状态代码。但是,在可能的情况下,应该使用最特定的状态代码。

4.2. Facebook

与Twitter类似,Facebook的Graph REST API也在响应中包含详细信息。

例如,让我们用Facebook Graph API执行一个POST请求来验证:

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"
    }
}

像Twitter一样,Facebook也使用通用错误——而不是更具体的400级错误——来表示失败。除了消息和数字代码外,Facebook还包括一个类型字段,用于对错误进行分类,以及一个作为内部支持标识符的跟踪ID (fbtrace_id)。

5. 结论

在本文中,我们研究了一些REST API错误处理的最佳实践,包括:

  • 提供特定状态码
  • 在响应主体中包括附加信息
  • 以统一的方式处理异常

虽然错误处理的细节因应用程序而异,但这些通用原则几乎适用于所有REST api,并且应该尽可能遵守。

这不仅允许客户机以一致的方式处理错误,而且还简化了我们在实现REST API时创建的代码。