API 设计让每个响应码都说明白
文本 · 通用对话模型
HTTP 状态码决策顾问
根据请求方法、实际结果、认证状态和服务依赖情况,为 API 选择合适的 HTTP 状态码,并解释替代方案、相关响应头与错误体结构。
Prompt
---
name: http-status-code-advisor
description: Picks the correct HTTP status code for an API response from a plain description of what happened (created, validation failed, not found, conflict, rate limited, upstream down) and explains why, with common mistakes to avoid. Use when designing REST endpoints, reviewing API error handling, or asking "should this be 400, 404, 409, or 422?". Includes a stdlib Python suggester.
---
# HTTP 状态码顾问
你帮助 API 设计者和评审者选择最符合请求实际情况的 HTTP 状态码。你根据请求方法、处理结果以及责任方(客户端、服务器或上游)进行判断,并优先选择客户端可以据此采取行动的最具体标准状态码。你遵循 RFC 9110 的语义,不编造非标准状态码。
## 此技能中的文件
- `scripts/suggest_status.py` - 将简短的场景描述(关键词加方法)映射为推荐状态码、备选项和一句话理由(仅使用 Python 3 标准库)
- `references/status-cheatsheet.md` - API 中重要的状态码,按类别分组,并说明各自的适用场景
- `references/common-mistakes.md` - 常见误用(带错误响应体的 200、401 与 403、空列表时使用 404、验证时使用 500)
- `templates/decision-tree.md` - 通过逐步提问确定状态码的流程
- `examples/api-scenarios.md` - 包含所选状态码和响应结构的示例场景
## 工作流程
### 1. 收集事实
请对方提供,或根据代码和描述推断:
- HTTP 方法和端点(例如 `POST /orders`)
- 发生了什么:成功、部分成功、已接受异步处理、输入问题、身份验证问题、资源不存在、状态冲突、速率限制、服务器或依赖项故障
- 是否会返回响应体,以及是否创建了新资源
- 团队已经遵循的任何 API 惯例
### 2. 按决策树逐步判断
使用 `templates/decision-tree.md`。先确定类别(2xx、3xx、4xx、5xx),再确定具体状态码。故障责任方决定使用 4xx 还是 5xx。
### 3. 运行辅助脚本(可选)
```
python3 scripts/suggest_status.py --method POST "created new order"
python3 scripts/suggest_status.py --method GET "user not logged in"
python3 scripts/suggest_status.py --json "email already exists"
```
将脚本结果视为初步猜测,并对照速查表确认。
### 4. 检查错误
将选择与 `references/common-mistakes.md` 比较,尤其检查 401 与 403、400 与 422,以及空 200 列表时使用 404 的情况。
### 5. 作答
返回:
1. **推荐状态码**和原因短语(例如 `409 Conflict`)
2. **理由**:用一到两句话结合实际情况说明
3. **考虑过的备选项**,以及它们不如推荐项的原因
4. 相关的**响应头**(`Location` 用于 201,`Retry-After` 用于 429/503,`WWW-Authenticate` 用于 401,`Allow` 用于 405)
5. 对于 4xx/5xx,使用 RFC 9457 Problem Details 格式编写**错误响应体草案**(`type`、`title`、`status`、`detail`、`instance`)
## 规则
- 操作失败时,绝不返回 2xx。
- 只有服务器或其依赖的上游发生故障时才使用 5xx,绝不用于客户端输入错误。
- 如果团队现有惯例合理,就保持一致;若其与标准选择不同,只需说明一次标准选择。
- 如果出于安全原因必须隐藏资源是否存在,在建议使用 404 而非 403 时明确说明。
FILE:references/common-mistakes.md
# 常见状态码错误
1. **带错误响应体的 200**(`{"success": false}`)。客户端、代理、缓存和监控系统都会将 200 视为成功。应使用正确的 4xx/5xx 状态码,并将详细信息放入响应体。
2. **混淆 401 和 403。**401 表示“你是谁?”(凭据缺失或无效)。403 表示“我知道你是谁,但答案是不允许。”已登录但无权限的用户应收到 403,而不是 401。
3. **输入错误时使用 500。**验证失败应由客户端负责:使用 400 或 422。500 应留给程序缺陷和意外的服务器故障。500s 激增应意味着你这边出了问题。
4. **空集合时使用 404。**没有匹配项的 `GET /users?role=admin` 应返回带有 `[]` 的 `200`。404 用于资源不存在的情况,例如 `/users/9999`。
5. **所有情况都使用 400。**如果更具体的状态码能帮助客户端采取行动,就优先使用它:重复项用 409,内容过大用 413,内容类型错误用 415,超出速率限制用 429。
6. **使用 201 却不带 `Location`。**创建了带有独立 URL 的资源时,应发送 `Location: /orders/123`。
7. **204 带有响应体。**204 不得包含内容。如果要返回资源,请使用 200。
8. **在 API 中使用 302。**客户端可能会将 POST 改为 GET。方法必须保持不变时,请使用 307 或 308。
9. **混淆 503 和 500。**503 表示临时故障且可以重试(维护、过载),最好附上 `Retry-After`。500 表示意外错误。
10. **通过 403 泄露资源是否存在。**对于私有资源,向未授权用户返回 404 可以防止枚举。请审慎决定并记录这一做法。
11. **不一致地使用 422 和 400。**选定一种规则(400 = 无法解析,422 = 已解析但无效),并应用于每个端点。
12. **使用非标准状态码**(例如在生产环境中使用 418、499、自定义 6xx)。许多客户端和代理无法妥善处理它们。请使用已注册的状态码。
FILE:references/status-cheatsheet.md
# API 的 HTTP 状态码速查表
依据 RFC 9110(HTTP 语义)、RFC 6585(429)和 RFC 9457(Problem Details)。
## 2xx 成功
| 状态码 | 名称 | 适用场景 |
|---|---|---|
| 200 | OK(成功) | 请求成功,响应体包含结果(GET,或返回更新后资源的 PUT/PATCH)。 |
| 201 | Created(已创建) | 创建了新资源。发送带有资源 URL 的 `Location`;响应体通常包含该资源。 |
| 202 | Accepted(已接受) | 任务已加入队列,稍后完成。返回状态 URL 或任务 ID。 |
| 204 | No Content(无内容) | 操作成功且没有响应体(DELETE,或不返回内容的 PUT/PATCH)。 |
| 206 | Partial Content(部分内容) | 根据 `Range` 请求头提供字节范围内容。 |
## 3xx 重定向
| 状态码 | 名称 | 适用场景 |
|---|---|---|
| 301 | Moved Permanently(永久移动) | 资源有了新的永久 URL(旧客户端可能会将方法改为 GET)。 |
| 304 | Not Modified(未修改) | 条件 GET(`If-None-Match` / `If-Modified-Since`),且资源未发生变化。 |
| 307 | Temporary Redirect(临时重定向) | 临时移动;客户端必须使用相同的方法和响应体重新发送请求。 |
| 308 | Permanent Redirect(永久重定向) | 永久移动;保持相同的方法和响应体。 |
## 4xx 客户端错误
| 状态码 | 名称 | 适用场景 |
|---|---|---|
| 400 | Bad Request(错误请求) | 语法格式错误:JSON 无效、内容结构错误、缺少必需的查询参数。 |
| 401 | Unauthorized(未授权) | 没有凭据,或凭据无效/已过期。发送 `WWW-Authenticate`。 |
| 403 | Forbidden(禁止访问) | 已通过身份验证,但不允许执行此操作。 |
| 404 | Not Found(未找到) | 资源不存在(或需要隐藏其存在)。 |
| 405 | Method Not Allowed(方法不允许) | URL 存在,但不支持此方法。发送 `Allow`。 |
| 406 | Not Acceptable(不可接受) | 无法生成 `Accept` 中列出的任何格式。 |
| 409 | Conflict(冲突) | 请求与当前状态冲突:唯一键重复、版本冲突、状态转换无效。 |
| 410 | Gone(资源已删除) | 资源曾经存在,但已被永久删除。 |
| 412 | Precondition Failed(前置条件失败) | `If-Match` / `If-Unmodified-Since` 检查失败(乐观锁定)。 |
| 413 | Content Too Large(内容过大) | 响应体超过大小限制。 |
| 415 | Unsupported Media Type(不支持的媒体类型) | 不支持 `Content-Type`。 |
| 422 | Unprocessable Content(无法处理的内容) | 语法正确,但值未通过验证(邮箱无效、结束日期早于开始日期)。 |
| 428 | Precondition Required(需要前置条件) | 服务器要求使用条件请求,以防止更新丢失。 |
| 429 | Too Many Requests(请求过多) | 超出速率限制。发送 `Retry-After`。 |
## 5xx 服务器错误
| 状态码 | 名称 | 适用场景 |
|---|---|---|
| 500 | Internal Server Error(内部服务器错误) | 代码中发生意外故障。 |
| 501 | Not Implemented(尚未实现) | 服务器完全不支持此功能。 |
| 502 | Bad Gateway(网关错误) | 上游返回了无效响应。 |
| 503 | Service Unavailable(服务不可用) | 暂时过载或正在维护。若已知,请发送 `Retry-After`。 |
| 504 | Gateway Timeout(网关超时) | 上游未能及时响应。 |
FILE:templates/decision-tree.md
# 状态码决策树
按顺序回答。遇到第一个符合的情况时停止。
1. **请求成功了吗?**
- 是,处理已完成且创建了新资源 -> **201**(+ `Location`)
- 是,已接受处理,但稍后才会完成 -> **202**(+ 状态 URL)
- 是,没有内容需要返回 -> **204**
- 是,返回了数据 -> **200**
- 否 -> 转到 2
2. **客户端是否正在被重定向?**
- 条件 GET,资源未改变 -> **304**
- 资源已移动,必须保留请求方法 -> **307**(临时)/ **308**(永久)
- 否 -> 转到 3
3. **这是身份验证问题吗?**
- 没有凭据,或令牌无效/已过期 -> **401**(+ `WWW-Authenticate`)
- 否 -> 转到 4
4. **已通过身份验证的调用者是否无权执行操作?**
- 是,且可以透露资源是否存在 -> **403**
- 是,但必须隐藏资源是否存在 -> **404**
- 否 -> 转到 5
5. **目标资源不存在,或请求方法错误吗?**
- 资源不存在 -> **404**
- 资源已被永久删除 -> **410**
- URL 存在,但不支持此方法 -> **405**(+ `Allow`)
- 否 -> 转到 6
6. **请求本身无法使用吗?**
- 无法解析(JSON 错误、类型错误)-> **400**
- `Content-Type` 错误 -> **415**
- 响应体过大 -> **413**
- 无法满足 `Accept` -> **406**
- 否 -> 转到 7
7. **输入有效,但被拒绝了吗?**
- 未通过业务验证 -> **422**
- 与当前状态冲突(重复项、版本、状态机)-> **409**
- `If-Match` 前置条件检查失败 -> **412**
- 请求过多 -> **429**(+ `Retry-After`)
- 否 -> 转到 8
8. **服务器端故障**
- 上游返回了无效内容 -> **502**
- 上游超时 -> **504**
- 暂时不可用/正在维护 -> **503**(+ `Retry-After`)
- 完全不支持此功能 -> **501**
- 意外的程序缺陷 -> **500**
## 回答模板
```
Recommended: <code> <reason phrase>
Why: <1-2 sentences>
Alternatives: <code> - <why not>; <code> - <why not>
Headers: <header: value>
Body (Problem Details):
{
"type": "https://example.com/problems/<slug>",
"title": "<short title>",
"status": <code>,
"detail": "<specific explanation>",
"instance": "<request path or id>"
}
```
FILE:examples/api-scenarios.md
# API 场景示例
## 1. 使用已注册的邮箱注册
使用 `{"email": "ana@example.com"}` 的 `POST /users`,且该邮箱已被占用。
- **推荐:**409 Conflict(冲突)
- **理由:**输入格式正确且有效,但与已有状态(唯一键)冲突。
- **备选项:**422(如果团队对所有规则失败都使用 422,也可以接受,但 409 能让客户端知道具体问题是状态冲突);400(过于笼统)。
- **响应体:**
```json
{"type": "https://example.com/problems/email-taken", "title": "Email already registered", "status": 409, "detail": "An account with this email already exists.", "instance": "/users"}
```
## 2. 创建订单
`POST /orders` 成功并返回订单。
- **推荐:**201 Created(已创建),`Location: /orders/8812`
- **备选项:**200(失去了“已创建”这一信号),202(仅当履约为异步处理且订单尚未持久化时使用)。
## 3. 报告导出任务已排队
`POST /reports/exports` 启动一个 5 分钟的任务。
- **推荐:**202 Accepted(已接受),并带有 `{"jobId": "exp_41", "statusUrl": "/reports/exports/exp_41"}`
## 4. 查看者尝试删除项目
具有只读角色的已登录用户调用 `DELETE /projects/7`。
- **推荐:**403 Forbidden(禁止访问)
- **理由:**用户身份已知,但缺少权限。401 会错误地暗示用户应重新登录。
## 5. 访问令牌已过期
`GET /me` 使用了已过期的 JWT。
- **推荐:**401 Unauthorized(未授权),`WWW-Authenticate: Bearer error="invalid_token"`
## 6. 搜索没有匹配项
`GET /products?q=zzzz`
- **推荐:**200 OK(成功),响应体为 `{"items": [], "total": 0}`
- **理由:**集合存在,只是内容为空。404 会导致将其视为错误的客户端出现问题。
## 7. 开始日期晚于结束日期
`POST /bookings` 使用了 `start=2026-10-20`,`end=2026-10-18`。
- **推荐:**422 Unprocessable Content(无法处理的内容)
- **备选项:**400(如果团队惯例将解析错误和验证错误合并处理,也可以接受)。
## 8. 支付服务提供商超时
`POST /checkout`,且支付网关在 10 秒内未作出响应。
- **推荐:**504 Gateway Timeout(网关超时)
- **备选项:**502(如果网关返回了无效响应),503(如果你有意削减负载)。
## 9. 登录尝试次数过多
- **推荐:**429 Too Many Requests(请求过多),`Retry-After: 60`
## 10. 过期版本更新
`PUT /docs/3` 使用了 `If-Match: "v4"`,但当前版本是 `"v5"`。
- **推荐:**412 Precondition Failed(前置条件失败)
FILE:scripts/suggest_status.py
#!/usr/bin/env python3
"""根据简短的场景描述推荐 HTTP 状态码。
用法:
python3 suggest_status.py [--method METHOD] [--json] "场景文本"
python3 suggest_status.py --self-test
基于关键词,刻意保持简单:提供初步猜测和备选项。请始终对照 references/status-cheatsheet.md 确认。
仅使用 Python 3 标准库。
"""
import argparse
import json
import re
import sys
# 有序规则:第一个匹配项生效。每条规则:(关键词、状态码、理由、备选项、响应头)
RULES = [
(["rate limit", "too many", "throttl", "quota exceeded"], 429,
"客户端超出速率限制或配额。", [503], ["Retry-After"]),
(["未登录", "没有令牌", "缺少令牌", "令牌已过期", "无效令牌",
"未通过身份验证", "没有凭据", "凭据错误", "需要登录"], 401,
"凭据缺失或无效。", [403], ["WWW-Authenticate"]),
(["没有权限", "不允许", "禁止访问", "角色权限不足", "未获授权",
"只读用户", "不是所有者"], 403,
"调用者已通过身份验证,但缺少权限。", [404], []),
(["已存在", "重复", "冲突", "版本不匹配", "已被占用",
"状态无效", "状态转换"], 409,
"请求与资源的当前状态冲突。", [422, 412], []),
(["precondition", "if-match", "etag mismatch", "stale"], 412,
"条件请求头不匹配。", [409], ["ETag"]),
(["too large", "payload size", "file too big", "body too big"], 413,
"请求响应体超出允许的大小。", [400], []),
(["unsupported media", "content-type", "wrong content type"], 415,
"不支持此 Content-Type。", [400], ["Accept"]),
(["格式错误", "JSON 无效", "解析错误", "无法解析", "语法错误",
"缺少必需内容"], 400,
"请求无法解析或缺少必需部分。", [422], []),
(["验证", "邮箱无效", "超出范围", "必须", "早于开始日期",
"值无效", "未通过规则"], 422,
"请求格式正确,但值未通过验证。", [400], []),
(["method not allowed", "wrong method", "unsupported method"], 405,
"URL 存在,但不支持此方法。", [404], ["Allow"]),
(["gone", "permanently deleted", "removed permanently"], 410,
"资源曾经存在,但已被永久删除。", [404], []),
(["not found", "does not exist", "no such", "unknown id"], 404,
"目标资源不存在。", [410], []),
(["upstream timeout", "gateway timeout", "timed out", "timeout"], 504,
"上游依赖未能及时响应。", [502, 503], []),
(["bad gateway", "upstream error", "invalid upstream", "upstream returned"], 502,
"上游依赖返回了无效响应。", [504], []),
(["maintenance", "overloaded", "unavailable", "down for"], 503,
"服务暂时不可用。", [500], ["Retry-After"]),
(["not implemented", "unsupported feature"], 501,
"服务器不支持此功能。", [405], []),
(["crash", "exception", "bug", "unexpected error", "internal error"], 500,
"服务器端发生意外故障。", [503], []),
(["queued", "async", "background job", "accepted for processing", "will process"], 202,
"任务已接受,将在稍后完成。", [201], ["Location"]),
(["created", "new resource", "signed up", "registered"], 201,
"创建了新资源。", [200, 202], ["Location"]),
(["deleted", "no content", "nothing to return"], 204,
"操作成功,且没有响应体。", [200], []),
(["not modified", "unchanged", "cache hit"], 304,
"条件请求中的资源未发生变化。", [200], ["ETag"]),
(["empty list", "no results", "no matches", "empty collection"], 200,
"集合存在但为空:返回带有空列表的 200。", [404], []),
(["success", "ok", "returned", "fetched", "updated"], 200,
"请求成功并返回内容。", [204], []),
]
REASONS = {
200: "成功", 201: "已创建", 202: "已接受", 204: "无内容", 304: "未修改",
400: "错误请求", 401: "未授权", 403: "禁止访问", 404: "未找到",
405: "方法不允许", 409: "冲突", 410: "资源已删除", 412: "前置条件失败",
413: "内容过大", 415: "不支持的媒体类型", 422: "无法处理的内容",
429: "请求过多", 500: "内部服务器错误", 501: "尚未实现",
502: "网关错误", 503: "服务不可用", 504: "网关超时",
}
def suggest(text, method=None):
t = " " + re.sub(r"\s+", " ", text.lower()) + " "
for keywords, code, reason, alts, headers in RULES:
for kw in keywords:
# 对于 "ok" 这类很短的关键词,使用词边界匹配
if len(kw) <= 3:
if re.search(r"\b" + re.escape(kw) + r"\b", t):
break
elif kw in t:
break
else:
continue
m = (method or "").upper()
notes = []
if code == 200 and m == "DELETE":
code, reason, alts = 204, "DELETE 操作成功;通常不返回响应体。", [200]
if code == 201 and m and m not in ("POST", "PUT"):
notes.append("201 通常由 POST 或 PUT 返回。")
if code == 204 and m == "GET":
notes.append("GET 很少返回 204;优先选择带响应体的 200。")
return {
"code": code,
"reason_phrase": REASONS.get(code, ""),
"why": reason,
"alternatives": [f"{a} {REASONS.get(a, '')}".strip() for a in alts],
"headers": headers,
"matched_keyword": kw,
"notes": notes,
}
return {
"code": None,
"reason_phrase": "",
"why": "没有匹配的规则。请手动按照 templates/decision-tree.md 逐步判断。",
"alternatives": [],
"headers": [],
"matched_keyword": None,
"notes": [],
}
def self_test():
cases = [
("创建新订单", "POST", 201),
("用户未登录", "GET", 401),
("只读用户尝试删除", "DELETE", 403),
("邮箱已存在", "POST", 409),
("搜索无结果", "GET", 200),
("结束日期早于开始日期验证", "POST", 422),
("JSON 请求体无效", "POST", 400),
("支付网关超时", "POST", 504),
("登录尝试次数过多", "POST", 429),
("订单 ID 不存在", "GET", 404),
("报告导出任务已作为后台任务排队", "POST", 202),
("记录已删除", "DELETE", 204),
("处理程序中出现意外异常", "GET", 500),
("发生了异常情况", "GET", None),
]
failed = 0
for text, method, expected in cases:
got = suggest(text, method)["code"]
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {method:6} {text!r} -> {got} (期望为 {expected})")
print(f"\n{len(cases) - failed}/{len(cases)} 项通过")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="根据 API 场景推荐 HTTP 状态码。")
p.add_argument("scenario", nargs="*", help="对所发生情况的简短描述")
p.add_argument("--method", help="HTTP 方法,例如 POST")
p.add_argument("--json", action="store_true", help="打印 JSON 输出")
p.add_argument("--self-test", action="store_true", help="运行内置测试")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.scenario:
p.error("必须提供场景文本(或使用 --self-test)")
r = suggest(" ".join(a.scenario), a.method)
if a.json:
print(json.dumps(r, indent=2))
return 0
if r["code"] is None:
print(r["why"])
return 2
print(f"推荐:{r['code']} {r['reason_phrase']}")
print(f"理由:{r['why']}")
if r["alternatives"]:
print("备选项:" + ", ".join(r["alternatives"]))
if r["headers"]:
print("响应头:" + ", ".join(r["headers"]))
for n in r["notes"]:
print("备注:" + n)
return 0
if __name__ == "__main__":
sys.exit(main())---
name: http-status-code-advisor
description: Picks the correct HTTP status code for an API response from a plain description of what happened (created, validation failed, not found, conflict, rate limited, upstream down) and explains why, with common mistakes to avoid. Use when designing REST endpoints, reviewing API error handling, or asking "should this be 400, 404, 409, or 422?". Includes a stdlib Python suggester.
---
# HTTP Status Code Advisor
You help API designers and reviewers choose the HTTP status code that best matches what actually happened during a request. You reason from the request method, the outcome, and who is at fault (client, server, or upstream), and you prefer the most specific standard code that clients can act on. You follow RFC 9110 semantics and do not invent non-standard codes.
## Files in this skill
- `scripts/suggest_status.py` - maps a short scenario description (keywords plus method) to a recommended code, alternatives, and a one-line reason (Python 3 standard library only)
- `references/status-cheatsheet.md` - the codes that matter for APIs, grouped by class, with when to use each
- `references/common-mistakes.md` - frequent misuses (200 with an error body, 401 vs 403, 404 for empty lists, 500 for validation)
- `templates/decision-tree.md` - a step-by-step question flow to reach a code
- `examples/api-scenarios.md` - worked scenarios with the chosen code and response shape
## Workflow
### 1. Collect the facts
Ask for, or infer from the code or description:
- HTTP method and endpoint (e.g. `POST /orders`)
- What happened: success, partial success, async accepted, input problem, auth problem, missing resource, state conflict, rate limit, server or dependency failure
- Whether a response body is returned, and whether a new resource was created
- Any existing API conventions the team already follows
### 2. Walk the decision tree
Use `templates/decision-tree.md`. Decide the class first (2xx, 3xx, 4xx, 5xx), then the specific code. Who caused the failure decides 4xx vs 5xx.
### 3. Run the helper (optional)
```
python3 scripts/suggest_status.py --method POST "created new order"
python3 scripts/suggest_status.py --method GET "user not logged in"
python3 scripts/suggest_status.py --json "email already exists"
```
Treat the script as a first guess; confirm against the cheatsheet.
### 4. Check for mistakes
Compare the choice against `references/common-mistakes.md`, especially 401 vs 403, 400 vs 422, and 404 vs an empty 200 list.
### 5. Answer
Return:
1. **Recommended code** and reason phrase (e.g. `409 Conflict`)
2. **Why** in one or two sentences, tied to the facts
3. **Alternatives** considered and why they lose
4. **Headers to include** when relevant (`Location` for 201, `Retry-After` for 429/503, `WWW-Authenticate` for 401, `Allow` for 405)
5. **Error body sketch** using RFC 9457 Problem Details (`type`, `title`, `status`, `detail`, `instance`) for 4xx/5xx
## Rules
- Never return 2xx for a failed operation.
- Use 5xx only when the server or an upstream it depends on failed, never for bad client input.
- Keep consistency with the team's existing conventions if they are reasonable; mention the standard choice once if they differ.
- If security requires hiding a resource's existence, say so explicitly when recommending 404 over 403.
FILE:references/common-mistakes.md
# Common Status Code Mistakes
1. **200 with an error body** (`{"success": false}`). Clients, proxies, caches, and monitoring treat 200 as success. Use the right 4xx/5xx and put details in the body.
2. **401 vs 403 confusion.** 401 means "who are you?" (missing or bad credentials). 403 means "I know who you are, and the answer is no." A logged-in user without permission gets 403, not 401.
3. **500 for bad input.** Validation failures are the client's fault: 400 or 422. Reserve 500 for bugs and unexpected server failures. A spike in 500s should mean something is broken on your side.
4. **404 for an empty collection.** `GET /users?role=admin` with no matches is `200` with `[]`. 404 is for a missing resource such as `/users/9999`.
5. **400 for everything.** Prefer the specific code when it helps clients act: 409 for duplicates, 413 for oversize, 415 for wrong content type, 429 for rate limits.
6. **201 without `Location`.** When you create a resource with its own URL, send `Location: /orders/123`.
7. **204 with a body.** 204 must not include content. If you return the resource, use 200.
8. **302 in APIs.** Clients may change POST to GET. Use 307 or 308 when the method must be preserved.
9. **503 vs 500.** 503 means temporary and retryable (maintenance, overload), ideally with `Retry-After`. 500 means an unexpected error.
10. **Leaking existence with 403.** For private resources, returning 404 to unauthorized users can prevent enumeration. Decide deliberately and document it.
11. **Using 422 and 400 inconsistently.** Pick one rule (400 = cannot parse, 422 = parsed but invalid) and apply it on every endpoint.
12. **Non-standard codes** (e.g. 418 in production, 499, custom 6xx). Many clients and proxies handle them poorly. Stick to registered codes.
FILE:references/status-cheatsheet.md
# HTTP Status Cheatsheet for APIs
Based on RFC 9110 (HTTP Semantics), RFC 6585 (429), and RFC 9457 (Problem Details).
## 2xx Success
| Code | Name | Use when |
|---|---|---|
| 200 | OK | The request succeeded and the body contains the result (GET, or PUT/PATCH returning the updated resource). |
| 201 | Created | A new resource was created. Send `Location` with its URL; body usually contains the resource. |
| 202 | Accepted | Work was queued and will finish later. Return a status URL or job id. |
| 204 | No Content | Success with no body (DELETE, or PUT/PATCH when you return nothing). |
| 206 | Partial Content | Serving a byte range in response to a `Range` header. |
## 3xx Redirection
| Code | Name | Use when |
|---|---|---|
| 301 | Moved Permanently | Resource has a new permanent URL (method may change to GET on old clients). |
| 304 | Not Modified | Conditional GET (`If-None-Match` / `If-Modified-Since`) and nothing changed. |
| 307 | Temporary Redirect | Temporary move; client must repeat the same method and body. |
| 308 | Permanent Redirect | Permanent move; same method and body preserved. |
## 4xx Client errors
| Code | Name | Use when |
|---|---|---|
| 400 | Bad Request | Malformed syntax: invalid JSON, wrong content shape, missing required query param. |
| 401 | Unauthorized | No credentials or invalid/expired credentials. Send `WWW-Authenticate`. |
| 403 | Forbidden | Authenticated, but not allowed to do this. |
| 404 | Not Found | The resource does not exist (or you hide that it exists). |
| 405 | Method Not Allowed | The URL exists but not for this method. Send `Allow`. |
| 406 | Not Acceptable | Cannot produce any format listed in `Accept`. |
| 409 | Conflict | Request conflicts with current state: duplicate unique key, version conflict, invalid state transition. |
| 410 | Gone | Resource existed and was permanently removed. |
| 412 | Precondition Failed | `If-Match` / `If-Unmodified-Since` failed (optimistic locking). |
| 413 | Content Too Large | Body exceeds the size limit. |
| 415 | Unsupported Media Type | `Content-Type` not supported. |
| 422 | Unprocessable Content | Syntax is fine but values fail validation (bad email, end date before start date). |
| 428 | Precondition Required | Server requires a conditional request to prevent lost updates. |
| 429 | Too Many Requests | Rate limit exceeded. Send `Retry-After`. |
## 5xx Server errors
| Code | Name | Use when |
|---|---|---|
| 500 | Internal Server Error | Unexpected failure in your code. |
| 501 | Not Implemented | The server does not support this functionality at all. |
| 502 | Bad Gateway | An upstream returned an invalid response. |
| 503 | Service Unavailable | Temporarily overloaded or in maintenance. Send `Retry-After` if known. |
| 504 | Gateway Timeout | An upstream did not respond in time. |
FILE:templates/decision-tree.md
# Status Code Decision Tree
Answer in order. Stop at the first match.
1. **Did the request succeed?**
- Yes, work finished and a new resource exists -> **201** (+ `Location`)
- Yes, work accepted but finishes later -> **202** (+ status URL)
- Yes, nothing to return -> **204**
- Yes, returning data -> **200**
- No -> go to 2
2. **Is the client being redirected?**
- Conditional GET, unchanged -> **304**
- Moved, must keep the method -> **307** (temporary) / **308** (permanent)
- No -> go to 3
3. **Is it an authentication problem?**
- No credentials, or invalid / expired token -> **401** (+ `WWW-Authenticate`)
- No -> go to 4
4. **Is the authenticated caller not allowed?**
- Yes, and existence can be revealed -> **403**
- Yes, but existence must be hidden -> **404**
- No -> go to 5
5. **Is the target missing or the method wrong?**
- Resource does not exist -> **404**
- Resource removed permanently -> **410**
- URL exists, method unsupported -> **405** (+ `Allow`)
- No -> go to 6
6. **Is the request itself unusable?**
- Cannot parse (bad JSON, wrong types) -> **400**
- Wrong `Content-Type` -> **415**
- Body too large -> **413**
- Cannot satisfy `Accept` -> **406**
- No -> go to 7
7. **Is the input valid but rejected?**
- Fails business validation -> **422**
- Conflicts with current state (duplicate, version, state machine) -> **409**
- `If-Match` precondition failed -> **412**
- Too many requests -> **429** (+ `Retry-After`)
- No -> go to 8
8. **Server side failure**
- Upstream returned garbage -> **502**
- Upstream timed out -> **504**
- Temporarily unavailable / maintenance -> **503** (+ `Retry-After`)
- Feature not supported at all -> **501**
- Unexpected bug -> **500**
## Answer template
```
Recommended: <code> <reason phrase>
Why: <1-2 sentences>
Alternatives: <code> - <why not>; <code> - <why not>
Headers: <header: value>
Body (Problem Details):
{
"type": "https://example.com/problems/<slug>",
"title": "<short title>",
"status": <code>,
"detail": "<specific explanation>",
"instance": "<request path or id>"
}
```
FILE:examples/api-scenarios.md
# Worked API Scenarios
## 1. Sign-up with an email that already exists
`POST /users` with `{"email": "ana@example.com"}` and that email is taken.
- **Recommended:** 409 Conflict
- **Why:** The input is well-formed and valid, but it conflicts with existing state (a unique key).
- **Alternatives:** 422 (works if your team uses 422 for all rule failures, but 409 tells the client the specific problem is state); 400 (too vague).
- **Body:**
```json
{"type": "https://example.com/problems/email-taken", "title": "Email already registered", "status": 409, "detail": "An account with this email already exists.", "instance": "/users"}
```
## 2. Order created
`POST /orders` succeeds and returns the order.
- **Recommended:** 201 Created, `Location: /orders/8812`
- **Alternatives:** 200 (loses the "created" signal), 202 (only if fulfillment is async and the order is not yet persisted).
## 3. Report export queued
`POST /reports/exports` starts a 5-minute job.
- **Recommended:** 202 Accepted with `{"jobId": "exp_41", "statusUrl": "/reports/exports/exp_41"}`
## 4. Viewer tries to delete a project
Logged-in user with read-only role calls `DELETE /projects/7`.
- **Recommended:** 403 Forbidden
- **Why:** Identity is known; permission is missing. 401 would wrongly suggest logging in again.
## 5. Expired access token
`GET /me` with an expired JWT.
- **Recommended:** 401 Unauthorized, `WWW-Authenticate: Bearer error="invalid_token"`
## 6. Search with no matches
`GET /products?q=zzzz`
- **Recommended:** 200 OK with `{"items": [], "total": 0}`
- **Why:** The collection exists; it is just empty. 404 would break clients that treat it as an error.
## 7. Start date after end date
`POST /bookings` with `start=2026-10-20`, `end=2026-10-18`.
- **Recommended:** 422 Unprocessable Content
- **Alternatives:** 400 (acceptable if your convention merges parse and validation errors).
## 8. Payment provider timed out
`POST /checkout` and the payment gateway did not answer within 10 seconds.
- **Recommended:** 504 Gateway Timeout
- **Alternatives:** 502 (if the gateway answered with an invalid response), 503 (if you are deliberately shedding load).
## 9. Too many login attempts
- **Recommended:** 429 Too Many Requests, `Retry-After: 60`
## 10. Stale update
`PUT /docs/3` with `If-Match: "v4"` but the current version is `"v5"`.
- **Recommended:** 412 Precondition Failed
FILE:scripts/suggest_status.py
#!/usr/bin/env python3
"""Suggest an HTTP status code from a short scenario description.
Usage:
python3 suggest_status.py [--method METHOD] [--json] "scenario text"
python3 suggest_status.py --self-test
Keyword-based and deliberately simple: it gives a first guess plus
alternatives. Always confirm against references/status-cheatsheet.md.
Python 3 standard library only.
"""
import argparse
import json
import re
import sys
# Ordered rules: first match wins. Each rule: (keywords, code, reason, alternatives, headers)
RULES = [
(["rate limit", "too many", "throttl", "quota exceeded"], 429,
"Client exceeded a rate limit or quota.", [503], ["Retry-After"]),
(["not logged in", "no token", "missing token", "expired token", "invalid token",
"unauthenticated", "no credentials", "bad credentials", "login required"], 401,
"Credentials are missing or invalid.", [403], ["WWW-Authenticate"]),
(["no permission", "not allowed", "forbidden", "insufficient role", "not authorized",
"read-only user", "not the owner"], 403,
"Caller is authenticated but lacks permission.", [404], []),
(["already exists", "duplicate", "conflict", "version mismatch", "already taken",
"invalid state", "state transition"], 409,
"Request conflicts with the current state of the resource.", [422, 412], []),
(["precondition", "if-match", "etag mismatch", "stale"], 412,
"A conditional request header did not match.", [409], ["ETag"]),
(["too large", "payload size", "file too big", "body too big"], 413,
"Request body exceeds the allowed size.", [400], []),
(["unsupported media", "content-type", "wrong content type"], 415,
"Content-Type is not supported.", [400], ["Accept"]),
(["malformed", "invalid json", "parse error", "cannot parse", "syntax error",
"missing required"], 400,
"Request cannot be parsed or is missing required parts.", [422], []),
(["validation", "invalid email", "out of range", "must be", "before start",
"invalid value", "fails rule"], 422,
"Request is well-formed but values fail validation.", [400], []),
(["method not allowed", "wrong method", "unsupported method"], 405,
"The URL exists but does not support this method.", [404], ["Allow"]),
(["gone", "permanently deleted", "removed permanently"], 410,
"Resource existed and was permanently removed.", [404], []),
(["not found", "does not exist", "no such", "unknown id"], 404,
"Target resource does not exist.", [410], []),
(["upstream timeout", "gateway timeout", "timed out", "timeout"], 504,
"An upstream dependency did not respond in time.", [502, 503], []),
(["bad gateway", "upstream error", "invalid upstream", "upstream returned"], 502,
"An upstream dependency returned an invalid response.", [504], []),
(["maintenance", "overloaded", "unavailable", "down for"], 503,
"Service is temporarily unavailable.", [500], ["Retry-After"]),
(["not implemented", "unsupported feature"], 501,
"The server does not support this functionality.", [405], []),
(["crash", "exception", "bug", "unexpected error", "internal error"], 500,
"Unexpected server-side failure.", [503], []),
(["queued", "async", "background job", "accepted for processing", "will process"], 202,
"Work was accepted and will complete later.", [201], ["Location"]),
(["created", "new resource", "signed up", "registered"], 201,
"A new resource was created.", [200, 202], ["Location"]),
(["deleted", "no content", "nothing to return"], 204,
"Success with no response body.", [200], []),
(["not modified", "unchanged", "cache hit"], 304,
"Conditional request and resource is unchanged.", [200], ["ETag"]),
(["empty list", "no results", "no matches", "empty collection"], 200,
"Collection exists but is empty: return 200 with an empty list.", [404], []),
(["success", "ok", "returned", "fetched", "updated"], 200,
"Request succeeded and returns content.", [204], []),
]
REASONS = {
200: "OK", 201: "Created", 202: "Accepted", 204: "No Content", 304: "Not Modified",
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found",
405: "Method Not Allowed", 409: "Conflict", 410: "Gone", 412: "Precondition Failed",
413: "Content Too Large", 415: "Unsupported Media Type", 422: "Unprocessable Content",
429: "Too Many Requests", 500: "Internal Server Error", 501: "Not Implemented",
502: "Bad Gateway", 503: "Service Unavailable", 504: "Gateway Timeout",
}
def suggest(text, method=None):
t = " " + re.sub(r"\s+", " ", text.lower()) + " "
for keywords, code, reason, alts, headers in RULES:
for kw in keywords:
# word-boundary match for very short keywords like "ok"
if len(kw) <= 3:
if re.search(r"\b" + re.escape(kw) + r"\b", t):
break
elif kw in t:
break
else:
continue
m = (method or "").upper()
notes = []
if code == 200 and m == "DELETE":
code, reason, alts = 204, "DELETE succeeded; usually no body.", [200]
if code == 201 and m and m not in ("POST", "PUT"):
notes.append("201 is normally returned by POST or PUT.")
if code == 204 and m == "GET":
notes.append("GET rarely returns 204; prefer 200 with a body.")
return {
"code": code,
"reason_phrase": REASONS.get(code, ""),
"why": reason,
"alternatives": [f"{a} {REASONS.get(a, '')}".strip() for a in alts],
"headers": headers,
"matched_keyword": kw,
"notes": notes,
}
return {
"code": None,
"reason_phrase": "",
"why": "No rule matched. Walk templates/decision-tree.md manually.",
"alternatives": [],
"headers": [],
"matched_keyword": None,
"notes": [],
}
def self_test():
cases = [
("created new order", "POST", 201),
("user not logged in", "GET", 401),
("read-only user tries to delete", "DELETE", 403),
("email already exists", "POST", 409),
("search returned no results", "GET", 200),
("end date before start date validation", "POST", 422),
("invalid json body", "POST", 400),
("payment gateway timed out", "POST", 504),
("too many login attempts", "POST", 429),
("order id does not exist", "GET", 404),
("report export queued as background job", "POST", 202),
("record deleted", "DELETE", 204),
("unexpected exception in handler", "GET", 500),
("something odd happened", "GET", None),
]
failed = 0
for text, method, expected in cases:
got = suggest(text, method)["code"]
ok = got == expected
failed += not ok
print(f"{'PASS' if ok else 'FAIL'} {method:6} {text!r} -> {got} (expected {expected})")
print(f"\n{len(cases) - failed}/{len(cases)} passed")
return 1 if failed else 0
def main(argv=None):
p = argparse.ArgumentParser(description="Suggest an HTTP status code for an API scenario.")
p.add_argument("scenario", nargs="*", help="Short description of what happened")
p.add_argument("--method", help="HTTP method, e.g. POST")
p.add_argument("--json", action="store_true", help="Print JSON output")
p.add_argument("--self-test", action="store_true", help="Run built-in tests")
a = p.parse_args(argv)
if a.self_test:
return self_test()
if not a.scenario:
p.error("scenario text is required (or use --self-test)")
r = suggest(" ".join(a.scenario), a.method)
if a.json:
print(json.dumps(r, indent=2))
return 0
if r["code"] is None:
print(r["why"])
return 2
print(f"Recommended: {r['code']} {r['reason_phrase']}")
print(f"Why: {r['why']}")
if r["alternatives"]:
print("Alternatives: " + ", ".join(r["alternatives"]))
if r["headers"]:
print("Headers: " + ", ".join(r["headers"]))
for n in r["notes"]:
print("Note: " + n)
return 0
if __name__ == "__main__":
sys.exit(main())
使用建议
使用建议:将接口方法、路径和实际发生的情况一起提供给对话模型;需要批量获得初步建议时,再在桌面环境运行随附的 Python 辅助脚本。