JSON / 表单 / 图片接口的响应设计:让客户端靠 Content-Type 与状态码判断,而不是靠猜
三类接口的响应形态应由 Content-Type 加状态码共同决定:JSON 接口返回可机读的错误码信封,表单接口面向浏览器给出重定向或带错误的页面,图片接口直接返回二进制并用响应头携带元数据;三者共用请求 ID、正确状态码和 nosniff 等基础约定,具体平台的字段与错误码需查官方文档。
- 客户端应先看 Content-Type 再决定怎么解析,不要假设响应一定是 JSON。
- 表单接口失败时返回带错误提示的页面或 303 重定向,而不是把 JSON 直接吐给浏览器。
- 图片接口用响应头携带类型与缓存信息,避免把 base64 塞进 JSON 信封。
- 请求 ID 与稳定的机器可读错误码,是三类接口共同的排障基础。
结论是:三类接口的响应形态,应由 Content-Type 与 HTTP 状态码共同决定,不需要客户端凭经验和接口名去猜。JSON 接口返回结构化数据和机器可读的错误码;表单接口面向浏览器,成功时重定向,失败时返回带错误提示的页面;图片接口直接返回二进制内容,元数据放在响应头里。三者共用一套错误标识和请求 ID 约定。本文谈的是通用设计取舍,API.PLUS 平台自身的字段命名、错误码表和限流响应头,本次材料未提供,需以官方文档为准(待核实)。
JSON 接口:统一响应信封与可判断的错误#
JSON 接口最容易出问题的地方不是成功响应,而是失败响应形态不固定:有时是纯文本,有时是 HTML 错误页,有时是另一种结构的 JSON。客户端因此被迫写多层兜底判断。
建议的约束是:错误响应同样返回 JSON,并且带一个稳定的机器可读 code,人类可读的 message 只作辅助;request_id 用于和服务端日志对账。这不是标准强制要求,而是一种便于长期维护的设计选择。
// 未执行的示例:仅示意错误响应结构,不代表任何真实接口
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json; charset=utf-8
{
"error": {
"code": "INVALID_ARGUMENT",
"message": "size must be between 1 and 4096",
"field": "size",
"request_id": "req_placeholder_0001"
}
}
状态码要如实反映语义:参数校验失败用 4xx,服务端自身异常用 5xx。把错误包在 200 里会让缓存、监控和重试策略全部失效。
表单接口:以浏览器为真实客户端#
表单接口的使用者通常是浏览器,不是脚本。这意味着响应要符合浏览器行为:
- 成功后重定向:POST 处理完成后返回
303 See Other指向结果页,避免用户刷新触发重复提交。 - 失败后重渲染:校验失败直接返回带错误提示的 HTML 页面,并保留用户已填内容,而不是返回一段 JSON 文本让用户盯着看。
- 不要两种形态混用:如果同一端点既要服务浏览器又要服务脚本,用
Accept分流是一种做法,但维护成本更高;更简单的是拆成两个端点。
表单接口的响应形态一旦确定,就不要因为内部实现调整而改变——浏览器端的跳转和回填逻辑会随之失效。
图片接口:二进制回二进制,元数据放头部#
图片接口的响应体应当是图片字节本身,配合准确的 Content-Type(如 image/png)、明确的 Cache-Control 与 ETag。需要下载语义时用 Content-Disposition 指定文件名。
常见反例是把图片 base64 后塞进 JSON 信封:体积约增加三分之一,客户端还要先解码,浏览器也无法直接把它当作图片资源缓存。除非调用方确实需要“一次请求拿到元数据加缩略图”,否则不值得。
同样重要的是失败路径:图片接口出错时不应返回 200 状态码加一张占位图或一段 HTML。客户端按 image/* 解析会得到损坏的图片,问题却很难定位。建议返回 4xx/5xx,并配合 X-Content-Type-Options: nosniff,防止浏览器把错误响应误判成别的类型。
客户端侧的最小判断逻辑#
与之配套的客户端写法应当先读响应头,再决定如何解析。
// 未执行的示例:示意分支顺序,未在真实环境中运行
async function call(url) {
const res = await fetch(url);
const type = (res.headers.get('content-type') || '').toLowerCase();
if (!res.ok) {
// 不要假设错误一定是 JSON
const body = type.includes('application/json') ? await res.json() : null;
throw new Error(body?.error?.code ?? `HTTP ${res.status}`);
}
if (type.startsWith('image/')) return res.blob();
if (type.includes('application/json')) return res.json();
return res.text();
}
这段代码的要点在于“按 Content-Type 分支”,不要只按接口文档分支。文档描述的是约定,响应头描述的是这一次实际返回的形态,两者不一致时,后者更可信。
三类接口共用的边界#
- 请求 ID:三类响应都带上,排障时能直接定位到一条服务端日志。
- 错误可判断:状态码加稳定错误码,不依赖
message文本匹配——文案随时可能改动。 - 缓存语义显式:图片接口尤其要写清
Cache-Control,避免中间层按默认策略缓存错误响应。 - 待核实项:具体的错误码枚举、限流相关响应头和字段命名,需查阅 API.PLUS 官方文档后确认,本文不作推测。