Restful API 设计的三点经验之谈

2015年冬天,我写下第一篇也是目前唯一一篇关于 Restful API 设计的文章。时间过的飞快,转眼三年前过去了。这三年间经历过的项目中,后台逐渐微服务化,restful 也成为大家耳熟能详的设计方案。这里记下些自己的经验和教训,以供对照。

Status code

基本的 code 原则很简单,2xx 表示成功,4xx 表示客户端错误,5xx 表示服务端错误。

那如何分辨是客户端还是服务端错误呢?我总结了以下几种常见的客户端错误,以及对应的错误码。

  • 401 - 未授权的访问比如访问资源需要 token 鉴权,如果不携带 token 或者 token 已过期,则返回 401.
  • 403 - forbidden,禁止访问。比如某些资源只允许管理员访问,非管理员则返回 403。
  • 404 - not found,不存在。

总之,凡是客户的锅,都返回 4xx 。如果恰好不在上面所列的三种情况中,则用 400 代替。

服务端自身错误则包含两类情况:

  • io 错误,比如读写文件,访问数据库
  • 自身逻辑错误,比如内存泄漏。

第一种错误是不可避免的,属于不可控的外部环境问题。第二种错误虽然可以通过 review 代码加上各种测试来预防,但最好有个兜底的错误处理以免程序挂掉。

我司对于服务端错误统一返回 500(internal server error),因为考虑到服务端错误对于客户来讲毫无建设意义,毕竟客户绝对没有办法帮助我们解决错误。即使对于工程师来说,日志也比 code 更有表现力。相对而言,客户端错误则尽量设计的详细因为大部分情况下客户端要据此来引导用户回到正常的业务中来。比如,如果返回 401,则引导用户登陆或者注册。如果业务比较复杂,还要考虑扩展 reponse 来指明更加具体的错误。如:

400 bad request
{
"code": 123,
"message": "Name is required"
}

List API

GET /orders

200 OK
{
"offset": 0,
"limit": 20,
"count": 100,
"elements": [...]
}

对于这个 List API,如果资源不存在,返回应该是什么。受 404 概念的普及影响,很多人会选择返回

404 NotFound

难道说,如果不存在 orders(订单) 就是错误吗?比如我从来没有在淘宝下过单,那订单列表也就应该显示客户端错误吗?这显然是不对的。实际上,404 是指所请求的资源不存在。而对于 orders 来说,它是一个集合概念。不管下没下过单,这个集合总归是存在的。按照这个理论,正确的返回应该是:

200 OK
{
"offset": 0,
"limit": 20,
"count": 100,
"elements": [] // 空数组
}

所以对于 List API 来说,没有 404。

Parent resource

restful API 的路径可以表现资源的从属关系。比如,用户可以有多个地址。

/users/{user_id}/addresses/

那么,对于一个并不存在的用户而言,访问上述 API,应该返回什么?

用户不存在,他的地址也必然不存在,那似乎是个简单的客户端错误。但我们确实有必要参考 Parent resource 的状态吗?这从理论上讲似乎毫无破绽,但实际操作及其困难。假如 Parent resource 的状态为 s1, Child resource 的状态为 s2,如果必须参考 s1 才能定义 s2,则 Child resource 的状态为 s1 * s2。这还是简单的层次,如果 Parent 之上还有 Parent,则最终 Child 的状态会变成 s0 * s1 * s2。如果随着业务的升级,每个节点的状态推算都要这样越来越复杂,那结果必然是整个系统的崩塌。

所以,目前比较推崇的做法是,仅仅考虑目标资源或者资源集合的状态。即,addresses,不管它从属于谁。