202 Accepted 开启的那份契约
一句话总结
202 表示的不是“已经完成”,而是“已经接收”。因此,返回 202 的 API 必须同时说明“到哪里查看结果”。
为什么需要了解这些
把耗时 30 秒的任务设计成同步 API,会同时破坏三件事:触发负载均衡器的空闲超时(通常为 60 秒);客户端重试时,同一任务会执行两次;服务器 Worker 每次会被占用 30 秒。
异步 API 把它拆成两个阶段:提交与查询。提交快速结束并返回标识符;查询则使用该标识符获取状态。
工作原理
HTTP 已经为这种模式提供了现成的词汇。
提交响应使用 202 Accepted,并通过 Location 请求头返回状态查询 URL。响应正文中还要包含任务标识符和当前状态。缺少这三项,客户端就不知道下一步该做什么。
状态查询通过 200 返回当前状态。状态词汇越简单越好,例如 queued、running、succeeded、failed。仍在进行时,通过 Retry-After 建议下一次轮询时间。缺少它,客户端会各自每秒轮询一次,状态 API 就会成为新的瓶颈。
任务完成后,有两种方式提供结果:把结果直接放入状态响应正文,或通过 303 See Other 指向结果资源 URL。后者更符合 REST 风格,但客户端实现更复杂。结果较小时,前者已经足够。
提交操作也必须具备幂等性。如果客户端在收到 202 之前连接中断并重试,同一任务就会创建两份。可以要求提交时提供幂等键,并为同一个键返回相同的 job id。
Webhook 是轮询的替代方案。任务完成时,服务器向客户端 URL 发起 POST。它没有轮询延迟,也不产生轮询负载,但同样需要付出代价:客户端必须提供公开端点;端点可能暂时不可用,因此服务器需要重试与 DLQ;还必须通过签名防止伪造。实际工作经常同时提供 Webhook 与轮询。
生产现场中的常见情况
最常见的设计错误,是直接把真实结果资源的 ID 用作 job id。这意味着提前提供尚未创建资源的 ID;如果任务失败,该 ID 就会永远成为幽灵。把 job id 与 result id 分开更安全。
第二,不要永久保存任务状态。为已完成状态设置 TTL;查询已过期任务时返回 410 Gone,而不是 404,这样客户端就能区分“从未存在”和“曾经存在但已过期”。
返回 202 后的契约
202 Accepted 只表示“已接收”,并不表示“必然成功”。因此,必须同时提供三项内容,客户端才能编写可靠代码。
HTTP/1.1 202 Accepted
Location: /api/jobs/7f3a-91cd
Retry-After: 3
Content-Type: application/json
{"job_id": "7f3a-91cd", "status": "queued",
"poll_url": "/api/jobs/7f3a-91cd", "estimated_sec": 30}
Location——应到哪里查询。它是标准请求头,工具能够识别。Retry-After——多久后再次查询。没有它,客户端可能每秒请求十次。- 任务 ID——识别重试请求是否属于同一任务的关键。
状态查询至少应区分四种状态。
{"status": "queued"} → 아직 시작 안 함
{"status": "running", "progress": 0.4} → 진행 중
{"status": "succeeded", "result_url": "/files/…"} → 결과가 있다
{"status": "failed", "error": {"code": "…","message": "…"}, "retriable": false}
retriable 很重要。客户端依据该值决定重新提交,还是要求人工处理。
用通知替代轮询
轮询简单,但会浪费资源。可以选择三种替代方案,每一种代价不同。
| 方式 | 服务器负担 | 客户端复杂度 | 防火墙 |
|---|---|---|---|
| 轮询 | 与请求数成正比 | 最简单 | 没有问题 |
| 长轮询 | 维持连接 | 简单 | 注意代理超时 |
| SSE | 维持连接 | 中等 | 通常可以通过 |
| Webhook | 最低 | 需要接收端点 | 对方必须有公开地址 |
使用 Webhook 时,必须同时设计重试与签名。接收方短暂宕机时需要重发;为了防止伪造,还应在正文上附加 HMAC 签名。
使用幂等键阻止重复提交
客户端在收到 202 前超时,会再次发送相同请求,从而创建两个任务。
POST /api/jobs
Idempotency-Key: 8f14e45f-ea3b-4d29-9a1c-2b3c4d5e6f70
服务器保存该键;再次收到相同键时,不创建新任务,而是原样返回首次创建任务的 202 响应。 键由客户端创建,但在多次重试间不能改变。因此,可以根据请求内容确定性地生成,或在请求开始时只生成一次并保存。
键的常见保留期限约为 24 小时。保留更久会让存储持续增长,过短则无法识别延迟较长的重试。
下一次实验要做什么
创建任务提交 API,准确返回 202 与 Location;让 Worker 推进任务状态;使用 Retry-After 调整轮询;使提交具备幂等性;最后添加完成 Webhook。