用户登录

技术文章

外部链接将在新标签页打开。

JSON、表单与图片接口的响应设计:先定契约,再定字段

一句话回答

响应设计应先固定媒体类型、状态码和错误体,再确定字段。JSON 接口应使用稳定的机器码和字段级错误;表单接口要区分浏览器导航与脚本调用,成功可 303,验证失败可 422;图片接口成功时返回二进制响应,并带上 Content-Type、ETag、Cache-Control,失败时按协商返回结构化错误,不要用 200 伪装成功。

核心要点 TL;DR
  • 先定契约:媒体类型、状态码语义、错误体形状、缓存策略,最后才定字段命名。
  • 图片接口要用 Vary: Accept 区分格式,随机图片要避免被共享缓存固定为同一张。
  • 示例均为演示结构,未实际执行;具体框架行为需自行核实。

为 JSON、表单、图片三类接口设计响应时,最容易犯的错误是先定义字段,后补状态码。更稳妥的顺序是:先确定契约,即媒体类型、状态码语义、错误体形状、缓存与协商策略;字段命名是最后一步。下面按三类接口分别说明,示例只用于演示结构,未实际执行。

三类接口共享的响应契约#

  • 一个端点只承诺一种主表示,用 Content-Type 声明实际类型,用 Accept 协商可选类型。
  • 失败要反映在 HTTP 状态码上,不要把业务失败写成 200 加 {"code":500}。否则缓存、网关、监控和客户端重试都会判断错误。
  • 错误体保持固定形状:稳定机器码、可读消息、可选字段级错误、请求标识。机器码面向程序,消息面向人。

JSON 接口:稳定错误体与明确的缺省语义#

JSON 响应建议区分“成功数据”和“错误”两种顶层结构,不要让客户端从字段是否存在中反推成败。错误体可以采用接近 application/problem+json 的形状:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://example.com/problems/validation-error",
  "title": "Validation failed",
  "status": 422,
  "code": "VALIDATION_ERROR",
  "errors": [
    { "field": "email", "code": "INVALID_FORMAT", "message": "邮箱格式不正确" }
  ],
  "request_id": "req_demo_0001"
}

其中 request_id 与 type 均为占位演示值。code 一旦发布就应视为契约,不随文案调整而改名。

另外三个边界条件值得提前规定:null 与字段缺省的区别(有值、无值、未请求);数组字段返回空数组还是 null;时间统一为带时区的字符串(例如 RFC 3339 形式)而非本地时间。分页建议同时给出数据和游标或总数,避免客户端靠“返回条数小于页大小”判断末页。

表单接口:区分浏览器导航与脚本调用#

表单提交有两种调用者:浏览器页面导航和脚本。它们的期望不同,响应设计要显式区分。

  1. 成功时,对浏览器导航使用 303 See Other 加 Location,让刷新不会重复提交。
  2. 验证失败时返回 422,并附带字段级错误;浏览器场景可重新渲染表单(不回填密码等敏感字段),脚本场景返回结构化错误体。
  3. 对会产生副作用的提交,用幂等键或一次性令牌防止重复执行。
POST /subscribe HTTP/1.1
Content-Type: application/x-www-form-urlencoded

HTTP/1.1 303 See Other
Location: /thanks

上述交互为演示,未在具体框架中验证。若项目采用前端渲染,则表单接口更接近 JSON 接口,此时应统一错误体,不要维护两套形状。

图片接口:二进制响应要带足元数据#

图片成功的响应本身就是二进制,字段说明只能放进响应头。建议至少覆盖媒体类型、缓存策略、实体标识和协商维度。

HTTP/1.1 200 OK
Content-Type: image/webp
ETag: "demo-etag-1"
Cache-Control: public, max-age=86400
Content-Length: 12345
Vary: Accept

以上数值均为占位示例,非实测结果。要点有两个:

  • 用 Accept 协商格式时,必须回 Vary: Accept,否则共享缓存可能把 WebP 响应发给只接受 JPEG 的客户端。
  • 随机图或按用户生成的图片,不建议用长 TTL 的 public 缓存,否则同一 URL 会被固化成一张图;可考虑短 TTL 或 no-store,具体取值按业务核实。

失败时不要返回 200 加一张“占位图”,除非接口文档明确承诺这一行为。更清晰的做法是按协商返回结构化错误体或 HTML 错误页,并在响应头声明真实媒体类型与状态码。

缓存与协商是响应设计的一部分#

接口类型成功状态错误体缓存关注点
JSON200 / 201problem+json 等结构化体私有数据用 private,公开数据按时效定 TTL
表单303 重定向或 200 重渲染422 + 字段级错误一般 no-store,避免缓存提交结果
图片200 二进制按协商返回结构化错误ETag、Vary、Cache-Control 三者配套

这张表是通用建议,不是某一框架或规范的要求。422 的具体语义与客户端兼容性,建议结合所用 HTTP 版本和客户端库核实后再写入契约。响应设计最终要落到一份可核对的文档上:状态码清单、错误码清单、示例响应、缓存规则,缺一项都会在联调时变成猜测。

常见问题(FAQ)

错误响应可以统一返回 200 再加 code 字段吗?
不建议。这样会让网关、缓存、监控和客户端重试逻辑无法区分成败;如确有历史包袱,至少要在文档和客户端层显式约定,并保留正确的 HTTP 状态码路径。
图片接口出错时该返回图片还是 JSON?
取决于协商结果。脚本调用通常期望结构化错误体,浏览器直接访问则可能期望 HTML 或占位图;关键是不要用 200 状态码掩盖失败,并在响应头里声明实际媒体类型。

作者:沐小悠

本文约 6 分钟读完 · 关键词:API 响应设计、JSON 错误体、表单接口 422、图片接口 Content-Type、内容协商 Vary Accept

更多文章 GET 示例接口

「」标签下的其他文章