错误
每一次失败都返回同一种机器可读的 problem 文档,以及每种类型对你的重试逻辑意味着什么。
每一次失败——包括访问一条并不存在的路由——都以 application/problem+json 作答,
也就是 RFC 9457 定义的那种结构。这个 API 里没有任何一条路径会返回 HTML 错误页面,
所以一个在非 2xx 响应上直接假定是 JSON 的解析器是安全的。
{
"type": "https://api.backresto.com/problems/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "The authenticated client lacks the required scope.",
"instance": "/v1/restaurants/restaurant-1/deliveries/8f2c/images",
"requestId": "b1c8b0a4-6c1e-4f6b-9e1c-3b2a5d7f0e91"
}
type 是那个稳定的标识符:按它做分支,而不是按 detail——后者是散文,措辞可能会
改。requestId 是写支持邮件时值得附上的值——它同时也在 X-Request-ID 响应头里
返回,而且你可以自己设置这个请求头,用来和你自己的追踪对应起来。
目录
type 后缀 | 状态码 | 发生了什么 | 要重试吗? |
|---|---|---|---|
invalid-request | 400 | 某个参数缺失或格式不对:一段前后颠倒或过长的收货记录区间、一个未知的集合,或者一个与你正在翻页的餐厅、集合和 limit 对不上的游标。 | 不要——把请求改对。 |
unauthorized | 401 | 没有 Authorization 请求头,或者密钥格式不对、未知、已吊销、已过期,又或者一份授权都不剩了。 | 不要——这把密钥需要处理。 |
forbidden | 403 | 密钥对这家餐厅有授权,但没有这个端点需要的权限范围。 | 不要——需要另外申请一把带这个权限范围的密钥。 |
not-found | 404 | 对这家餐厅没有授权,或者没有这条收货记录或这条记录。这几种情形被有意做成无法区分。 | 不要。 |
conflict | 409 | 请求与一个已有的资源冲突。 | 不要。 |
payload-too-large | 413 | 请求体超出了上限。 | 不要。 |
rate-limit-exceeded | 429 | 你的客户端用超了它的请求额度。 | 要,等过了 Retry-After 再重试。 |
dependency-unavailable | 503 | API 需要的某个组件短暂不可用。 | 要,带退避。 |
internal-error | 500 | 一次意外的失败,我们这边已经记录下来了。 | 重试一次,带退避;之后把 requestId 告诉我们。 |
客户端该怎么做
三类,除此之外的代码都不值得写:
不要重试——400、401、403、404、409、413。这个请求会以完全一样的方式再失败一次。
把 type 和 requestId 记下来,并把它显示出来:尤其是 401,它意味着你客户那边
有个人吊销了什么,而一个重试循环只会把这件事掩盖一个星期。
带退避重试——429、503。在 429 上遵守 Retry-After;它是你的额度重新充满还需要
的秒数,不是一个建议。对 503,用带抖动的指数退避,而且宁可放弃也不要一直猛敲。
重试一次,然后上报——500。如果它在同一个请求上再次出现,那就不是暂时性的。
这个 API 里的每个端点都是 GET,所以从 API 的角度看,重试永远是安全的;不重试
4xx 的理由是它浪费你的额度,而不是它有写两次的风险。
与你自己的日志对应起来
发出你自己的标识符,它会原样回来:
curl "https://api.backresto.com/v1/restaurants/$RESTAURANT_ID/deliveries?from=1787846400000&to=1787932800000" \
-H "Authorization: Bearer $BACKRESTO_PARTNER_API_KEY" \
-H "X-Request-ID: 3f7a1c9e-2b4d-4a86-9f0c-5e1d8b6a2c40" \
--include
这个值会在响应头里、也会在一份 problem 文档的 requestId 里回显出来,这让一次
grep 就能回答「我的任务在 04:12 失败的时候,BackResto 看到的是什么?」。