首页
/ RealWorld API 错误处理契约详解:422 / 401 / 403 / 404 响应格式与 Hurl 测试实证

RealWorld API 错误处理契约详解:422 / 401 / 403 / 404 响应格式与 Hurl 测试实证

2026-09-04 12:02:18作者:温玫谨Lighthearted

RealWorld("The mother of all demo apps")为所有后端实现规定了统一的错误响应契约:当请求校验失败时返回 422 及固定结构的 errors 对象,认证缺失返回 401,越权操作返回 403,资源不存在返回 404。本文以 error-handling.md 为核心骨架,结合 OpenAPI 规范 与仓库中的 Hurl 测试套件(specs/api/hurl/),完整拆解每一类错误码的响应体结构、错误键的命名规则,以及可直接复制运行的端到端验证脚本,帮助你在实现或校验任意后端时精确对齐这份跨技术栈契约。

错误响应体:errors 对象的统一格式

规范原文给出的核心约定是:任何请求只要有一项校验失败,就应返回 HTTP 422,并且错误体采用如下格式:

{
  "errors":{
    "body": [
      "can't be empty"
    ]
  }
}

其结构在 OpenAPI 规范 中被固化为 GenericErrorModel(约第 637–647 行):

GenericErrorModel:
  required:
    - errors
  type: object
  properties:
    errors:
      type: object
      additionalProperties:
        type: array
        items:
          type: string

从源码结构看,这份模型可以归纳出三条规则:

  1. 顶层只有一个键 errors,且它是必需字段;
  2. errors 是一个对象,键为出错字段的名称(如 bodytitletokenarticle),值统一是字符串数组——同一字段可以有多个错误信息;
  3. 错误消息本身是纯字符串,由实现方给出,契约只约束"字段名 + 数组"这一外层形状。

这里有一个值得注意的细节:规范文档示例中的消息文案是 "can't be empty",而仓库中全部 Hurl 测试断言的实际文案是 "can't be blank"(例如 errors_auth.hurljsonpath "$.errors.username[0]" == "can't be blank")。backend 文档的 Introduction 明确声明:"These pages summarize the contract in prose, but the OpenAPI spec and the Hurl suite are what actually define it. Where the prose and the tests disagree, the tests win"——即测试套件与 OpenAPI 是契约的最终事实来源。因此实现方应以 Hurl 断言中的消息文案(can't be blankis missingnot foundforbiddenhas already been takeninvalid)为准。

状态码总览:422、401、403、404 及 409

规范文档定义了四类错误状态码,配合 Hurl 错误测试套件 可归纳为如下对照表:

状态码 语义 触发条件 典型错误键 实测断言文案
422 校验失败 请求体字段为空、缺必填项、违反取值规则 字段名(username/email/password/title/description/body can't be blank
401 Unauthorized 需要认证但未提供 token token is missing
403 Forbidden 请求合法但用户无权操作该资源 资源类型(article/comment forbidden
404 Not Found 按 slug/用户名/ID 找不到资源 资源类型(article/profile/comment not found
409 Conflict 唯一性冲突(注册时用户名/邮箱已被占用) 冲突字段(username/email has already been taken

其中 409 一栏来自测试证据:errors_auth.hurl 第 53–77 行对重复用户名、重复邮箱的注册请求分别断言 HTTP 409"has already been taken"OpenAPI 规范 中也定义了 ConflictError 响应(示例 errors: {username: ["has already been taken"]})。这说明错误体格式(errors 对象 + 字符串数组)是所有错误状态码共用的,不只是 422。

401 Unauthorized:token 缺失的统一拒绝方式

规范定义:"401 for Unauthorized requests, when a request requires authentication but it isn't provided"。契约要求错误键为 token、消息为 is missing,即响应体形如:

{ "errors": { "token": ["is missing"] } }

Hurl 套件对这条规则做了穷举式覆盖。errors_articles.hurl 验证了所有需要认证的 articles 端点在缺 token 时都必须返回 401 + errors.token[0] == "is missing",包括:

  • POST /api/articles(创建文章,第 1–12 行)
  • PUT /api/articles/{slug}(更新,第 20–29 行)
  • DELETE /api/articles/{slug}(删除,第 31–35 行)
  • GET /api/articles/feed(信息流,第 37–41 行)
  • POST /api/articles/{slug}/favoriteDELETE .../favorite(收藏/取消收藏,第 43–53 行)

同样的断言也出现在 errors_comments.hurl(评论的创建与删除,第 1–16 行)、errors_profiles.hurl(关注/取消关注,第 7–17 行)以及 errors_auth.hurl 中的 GET /api/userPUT /api/user(第 115–130 行)。

一个容易踩坑的对照:密码错误不返回 422,而是 401errors_auth.hurl 第 103–113 行对 POST /api/users/login 传入错误密码的请求断言 HTTP 401errors.credentials[0] == "invalid"。也就是说,"凭证错误"被归类为认证失败(401 + credentials 键),而"字段为空"才走校验失败路径(422 + 字段键)。

403 Forbidden:有权限体系内的越权操作

规范定义:"403 for Forbidden requests, when a request may be valid but the user doesn't have permissions to perform the action"。与 401 的关键区别是:请求本身合法(token 有效、资源存在),但当前用户不是资源所有者

错误键的规则在 OpenAPI 规范Forbidden 响应描述中写明:"The error key identifies the resource type (article, comment, etc.)",即操作文章用 article,操作评论用 comment,消息固定为 forbidden

{ "errors": { "article": ["forbidden"] } }

errors_authorization.hurl 完整演绎了这条规则的测试流程:

  1. 注册用户 A 与用户 B(第 1–25 行),分别捕获 token_atoken_b
  2. 用户 A 创建文章(第 27–39 行),捕获 slug
  3. 用户 B 尝试 DELETE /api/articles/{slug} → 断言 HTTP 403errors.article[0] == "forbidden"(第 41–46 行);
  4. 用户 B 尝试 PUT 篡改正文为 "hijacked" → 同样 403(第 48–58 行);
  5. 用户 A 在文章下创建评论后,用户 B 尝试删除该评论 → 403 且 errors.comment[0] == "forbidden"(第 72–77 行);
  6. 验证失败的删除没有产生副作用——再次 GET 确认评论仍然存在(第 79–84 行)。

这套断言隐含了一个重要的实现要求:403 必须是"拒绝且无副作用"的——被拒绝的写操作不得修改任何数据,测试第 79–84 行专门验证了这一点。

404 Not Found:资源键与"not found"文案

规范定义:"404 for Not found requests, when a resource can't be found to fulfill the request"。与 403 类似,错误键标识未被找到的资源类型,消息为 not found。Hurl 套件覆盖了三种资源类型:

文章(article)——errors_articles.hurl 验证了所有以未知 slug 访问的端点都返回 404:

  • GET /api/articles/{slug}(第 14–18 行)
  • PUTDELETE 未知 slug(第 139–182 行)
  • POST/DELETE .../favorite 未知 slug(第 151–163 行)

评论(comment)——errors_comments.hurl 展示了两级 404:对未知文章的任何评论操作(POST/GET/DELETE)返回 errors.article[0] == "not found"(第 56–79 行);而"文章存在但评论 ID 不存在"则返回 errors.comment[0] == "not found"(第 81–86 行)。这要求实现方在报错时指明究竟是哪一层资源缺失

用户资料(profile)——errors_profiles.hurl 验证 GET /api/profiles/{username}POST/DELETE .../follow 对未知用户返回 404 且 errors.profile[0] == "not found"(第 1–5 行、第 32–43 行)。

422 校验失败:按字段定位错误的完整清单

422 是错误契约中覆盖面最广的一类。其核心原则是:错误键直接等于请求体中出错字段的名字,让前端能精确地把错误信息定位到对应输入框。以下是 Hurl 套件固化下来的全部 422 校验场景:

注册(POST /api/users)——errors_auth.hurl 第 1–38 行:

{ "user": { "username": "", "email": "x@test.com", "password": "password123" } }
HTTP 422  { "errors": { "username": ["can't be blank"] } }

usernameemailpassword 三个字段分别断言。

登录(POST /api/users/login)——email 或 password 为空均返回 422 + 对应字段键(第 79–101 行)。

更新用户(PUT /api/user)——除"字段为空"外,还有一批针对必填字段传入 null 的拒绝规则(第 132–194 行):emailusername 传空字符串或 nullpassword 传空字符串或 null,均应返回 422。

密码策略(NIST 800-63B)——errors_auth.hurl 第 172–224 行针对 PUT /user 的密码更新固化了如下策略(测试注释中引用了 NIST 800-63B 第 5.1.1.2 节):

输入 期望
少于 8 个字符(如 short7c 422 拒绝
8 个字符的简单词(如 bonjour1 200 接受
64 个字符 200 接受(必须能接受至少 64 字符,不得设更短上限)

创建文章(POST /api/articles)——errors_articles.hurl 第 68–108 行分别对空 title、空 description、空 body 断言 422 + can't be blank。同时第 110–137 行固化了一个反向规则:标题重复是允许的(每篇文章生成唯一 slug),不产生任何错误。

创建评论(POST /api/articles/{slug}/comments)——空 body 返回 422(errors_comments.hurl 第 44–54 行)。

与空值归一化的边界:哪些"空"是错误,哪些是合法的 null

错误处理契约与数据归一化规则在边界处紧密咬合。以 PUT /user 为例(auth.hurlerrors_auth.hurl 对照):

  • 可空字段 bioimage:传空字符串是合法的,且必须被归一化为 nullauth.hurl 第 81–98 行断言 "bio": "" 更新后返回 bio == null);传 null 也被接受(第 112–122 行);
  • 必填字段 usernameemailpassword:传空字符串或 null 一律 422errors_auth.hurl 第 132–194 行)。

实现方需要按"字段是否可空"区分这两条路径,这正是契约测试存在的原因。

运行错误处理测试套件:从脚本到断言

仓库提供了两套可直接运行的契约测试,均以"错误行为是否符合上表"为断言目标:

Hurl 套件(推荐)——run-hurl-tests.sh 的完整实现为:

#!/usr/bin/env bash
set -euo pipefail

DIR="$(cd "$(dirname "$0")" && pwd)"
HOST="${HOST:-http://localhost:8000}"
UID_VAL="${UID_VAL:-$(date +%s)$$}"

FILES=("$@")
if [ ${#FILES[@]} -eq 0 ]; then
  FILES=("$DIR"/*.hurl)
fi

hurl --test --jobs 1 --variable "host=$HOST" --variable "uid=$UID_VAL" "${FILES[@]}"

关键参数说明:

  • HOST 环境变量指定被测 API 地址,默认 http://localhost:8000
  • UID_VAL 为每轮测试注入唯一后缀(默认取时间戳+进程号),保证用户名字段、slug 等在全新数据库中不冲突;
  • 不带参数运行时执行全部 *.hurl 文件;只验证错误契约时可单独传入错误相关文件,例如 run-hurl-tests.sh errors_auth.hurl errors_authorization.hurl
  • --jobs 1 保证请求顺序执行,避免并行干扰有依赖关系的断言。

各文件与错误主题的对应对照如下:

文件 验证内容
errors_auth.hurl 注册/登录/更新用户的 422、401、409 与密码策略
errors_articles.hurl 文章的 401/404/422
errors_authorization.hurl 跨用户越权操作的 403
errors_comments.hurl 评论的两级 404 与 422
errors_profiles.hurl 资料/关注的 401/404

仓库同时提供 Bruno 集合specs/api/bruno/errors-articles/errors-auth/errors-authorization/errors-comments/errors-profiles/ 等目录),其中的 .bru 请求与上述 Hurl 断言一一对应(如 01-register-empty-username.bru),可供偏好用图形化工具调试的场景,并配合 run-api-tests-bruno.sh 批量执行。

实现要点小结

综合 error-handling.mdOpenAPI 规范 与 Hurl/Bruno 测试,实现或校验 RealWorld 后端时,错误处理必须满足:

  1. 所有错误共用 GenericErrorModel 结构:{ "errors": { "<key>": ["<message>", ...] } }
  2. 422 的键等于校验失败的请求字段名,文案以 can't be blank 系为准;
  3. 401 分两种:token 缺失用 token 键 + is missing;凭证错误用 credentials 键 + invalid
  4. 403 与 404 的键等于资源类型articlecommentprofile),文案分别为 forbiddennot found
  5. 唯一性冲突返回 409 + has already been taken
  6. 被 403 拒绝的写操作必须无任何副作用;
  7. 可空字段(bio/image)的空字符串应归一化为 null 而非报 422,必填字段的空值一律 422。

由于各技术栈实现(React、Angular、Node、Django 等)都以同一份契约对接前端,上述每一条都可以通过仓库内自带的 Hurl/Bruno 套件在本地直接复现验证——这也是 RealWorld 作为全栈示范项目的核心价值之一:错误行为不再依赖口头约定,而是由可执行测试精确锁定。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384