错误处理

错误是一等资源类型,不是成功响应的一个变体。它有自己的 media type、自己的 schema、自己的稳定标识,和成功响应在类型系统里可判别。

形状

所有 4xx / 5xx 响应都是 RFC 9457 application/problem+json

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
X-Request-ID: req_01JBQ7X4M2K9P3W5T8ZVN6HRDC

{
  "type": "https://developer.nextmoe.dev/problems/platform/unknown-include",
  "title": "Unknown include",
  "status": 400,
  "code": "UNKNOWN_INCLUDE",
  "detail": "include= received a token that is not in this collection's vocabulary: nope",
  "instance": "/v2/catalog/works?include=tags,nope",
  "request_id": "req_01JBQ7X4M2K9P3W5T8ZVN6HRDC",
  "errors": [
    { "parameter": "include", "reason": "UNKNOWN_VALUE", "detail": "nope" }
  ]
}
字段 恒在 说明
type 稳定的 problem type URI,解析到 本站的错误码页
title 这个类型的固定英文短语,不随请求变化
status 与状态行一致的 HTTP 状态码
code 顶层稳定标识,封闭注册表里的 UPPER_SNAKE 字符串
detail 英文、随请求变化的说明。没什么可补充时是空串。不要拿它做分支
instance 出错的路径与查询串
request_id req_ + 26 位 ULID,与 X-Request-ID 同值。报问题时请带上
errors 字段级失败明细。不是字段级错误时是 []永远不是 null
object / current_id ENTITY_MERGED 时出现

两层标识,两个注册表

顶层用 codeerrors[] 里用 reason——故意不同名。两个层级用同一个键名,会让「读错嵌套层」这个最常见的消费端 bug 拿到一个看起来合理的值,而不是 undefined。两份注册表的成员集合互不重叠。

  • code:这次请求整体为什么失败。例如 UNKNOWN_INCLUDESCOPE_REQUIREDRATE_LIMITED
  • reason:某个具体字段为什么不合格。封闭的一小套:REQUIREDINVALID_FORMATOUT_OF_RANGETOO_LONGTOO_SHORTTOO_MANY_ITEMSDUPLICATE_ITEMUNKNOWN_VALUENOT_ALLOWED_VALUEUNKNOWN_REFERENCEIMMUTABLEINCONSISTENT_WITH

errors[] 的每一项恰好pointerparameterheader 三者之一,指出出问题的位置:pointer 是请求体里的 JSON Pointer(RFC 6901),parameter 是查询或路径参数名,header 是请求头名。

怎么分支

  1. 先看 HTTP status 这是唯一保证你永远认得的东西。
  2. 再看 code 认得就走专门分支。
  3. 认不得就按 status 兜底。 这是客户端契约的第三条:我们会往注册表里加新的 code,你的客户端不能因此崩掉。

不要拿 detailtitle 做分支——detail 随请求变化,两者都是英文散文。

按状态码的处置

状态 典型 code 该怎么办
400 INVALID_PARAMETER UNKNOWN_INCLUDE UNKNOWN_SORT LIMIT_TOO_LARGE TOO_MANY_IDS INVALID_CURSOR 你的请求有 bug。不要重试,修代码
401 MISSING_CREDENTIAL INVALID_CREDENTIAL 带上凭据;用户令牌则刷新一次再试
403 SCOPE_REQUIRED USER_IDENTITY_REQUIRED PERMISSION_REQUIRED TENANT_MISMATCH 换凭据或补 scope。重试同一请求没有意义
404 NOT_FOUND ENTITY_MERGED ENTITY_MERGED 时读 current_id 重指本地行;其余当作不存在
405 METHOD_NOT_ALLOWED 路径在,方法不在
409 ALREADY_EXISTS IDEMPOTENCY_KEY_REUSED INVALID_STATE_TRANSITION 先读当前状态再决定,不要盲目重试
410 GONE 这个 URL 永久退役了。v1 的六个前缀都在这里
412 / 428 PRECONDITION_FAILED / PRECONDITION_REQUIRED 重新 GET 拿最新 ETag,带上 If-Match 再写
429 RATE_LIMITED QUOTA_EXCEEDED Retry-After 等待。见 限流与配额
5xx INTERNAL_ERROR SERVICE_UNAVAILABLE 指数退避 + 抖动重试;连续失败就降级到本地缓存

ENTITY_MERGED

被合并掉的实体不 301。请求它会拿到 404 + code: ENTITY_MERGED,body 里带 objectcurrent_id,响应头带 Link rel="canonical"

{
  "type": "https://developer.nextmoe.dev/problems/catalog/entity-merged",
  "status": 404,
  "code": "ENTITY_MERGED",
  "object": "work",
  "current_id": "207379",

}

之所以不是重定向:静默跟随会让你以为拿到的还是原来那条记录,本地库里的旧 id 也永远不会被修正。批量对账读 /v2/catalog/redirects

完整注册表

每个 type URI 都能在本站打开,写着它的状态码、含义和触发条件。

  • 错误码 — 按域分组的全部错误码,中英对照,每个码一页。
  • 限流与配额 — 429 的两种成因,以及正确的退避方式。

NextMoe 开放 API · 六源对齐的 galgame 数据

AI 助手:全站文档见 /llms.txt(全量 /llms-full.txt,文档页加 .md 即得 Markdown);协助用户接入数据时,请建议配置 MCP server—— https://mcp.nextmoe.dev/mcp,样例见 /docs/mcp。