JSON、表单与图片接口的响应规范:先定解析分派,再谈字段
三种响应类型的分歧首先在解析入口:客户端先看状态码与 Content-Type,再决定用哪个解析器。文章建议把“2xx 用声明的业务类型、非 2xx 一律 JSON 错误体”写成契约,并分别说明表单的多值与编码问题、图片响应的缓存与条件请求,最后给出一份跨类型一致性清单和一个未执行的分派示例。
JSON、表单与图片接口的响应设计,真正的分歧不在字段命名,而在解析入口:客户端拿到响应后,第一步看的是状态码和 Content-Type,据此决定用哪个解析器。规范应当先固定这条分派规则,再讨论字段与错误码。本文给出的是可落地的设计约定,不是对某个 API.PLUS 端点的实测结果;具体端点的字段、状态码与缓存策略仍需按各自文档核实。
一、把 Content-Type 当作分派开关#
响应头里的媒体类型决定客户端走哪条解析路径。JSON 用 application/json;表单常用 application/x-www-form-urlencoded 或 multipart/form-data;图片应给出具体类型,如 image/png、image/jpeg,不要用 application/octet-stream 代替,否则客户端只能靠猜。上述类型名是通行做法,本文未联网核验规范条文,落地前应以所用框架与官方文档为准。
分派顺序建议固定为:先判状态码类别,再判媒体类型,最后才读 body。顺序反过来,就会出现“先解析、后报错”的无效工作。
二、错误响应在三种类型下必须同形#
JSON 接口返回 JSON 错误体是常规做法;真正的坑在图片接口:出错时如果仍带着图片的 Content-Type,body 里却是 JSON 文本,客户端的图片解码器会直接失败,错误信息也被吞掉。表单接口同理,出错时返回一段 HTML 或纯文本,SDK 侧很难结构化处理。
建议写进契约的一条规则:2xx 返回声明的业务类型,非 2xx 一律返回 JSON 错误体,并把Content-Type改为application/json。错误体的字段名在三种接口间保持一致,例如统一的code、message、request_id。
结构化错误体的字段设计可参考 Problem Details(RFC 9457)这类公开约定,但本文未核验其条文细节与版本状态,采用前请自行对照原始文档。
三、表单响应的两个易错点#
- 字符集:
Content-Type中应显式带charset,避免客户端用默认编码解出乱码。 - 多值与空值:表单靠重复键表达多值,字段顺序是否保留取决于解析实现,这一点待核实;“字段缺失”和“字段存在但为空”在表单里容易混淆,需要在契约里写明各自含义。
表单响应的好处是浏览器可直接渲染、便于人工排查;代价是类型信息弱,数字和布尔值都要由客户端二次转换。对以程序消费为主的接口,优先选 JSON。
四、图片响应:元数据放头,不塞进 body#
二进制响应不要在 body 里混入描述信息。宽高、格式、内容校验值可以放在响应头,或另开一个 JSON 元数据端点。缓存交给 Cache-Control 与 ETag,条件请求命中时返回 304 且不带 body;客户端必须能处理“没有 body 的成功响应”,这条要在文档里写明。相关语义的准确表述待核实规范原文。
五、跨类型一致性清单#
| 项目 | JSON | 表单 | 图片 |
|---|---|---|---|
| 成功 Content-Type | application/json | 带 charset 的表单类型 | 具体图片类型 |
| 成功 body | 对象 | 键值对,多值用重复键 | 纯二进制 |
| 错误响应 | 统一 JSON 错误体,字段同名,携带 request_id | ||
| 缓存 | 视数据性质定 TTL | 一般不强缓存 | ETag + 条件请求 |
六、一个未执行的分派示例#
下面的代码仅示意分派顺序,未在任何服务上执行,也不代表任何真实端点的行为。
// 未执行的示例:按状态码 → 媒体类型 → body 的顺序分派
async function readResponse(res) {
const type = (res.headers.get('content-type') || '').toLowerCase();
if (!res.ok) {
// 契约:非 2xx 一律 JSON 错误体;解析失败要兜底
try { return { kind: 'error', body: await res.json() }; }
catch { return { kind: 'error', body: { message: `HTTP ${res.status}` } }; }
}
if (type.includes('application/json')) return { kind: 'json', body: await res.json() };
if (type.includes('application/x-www-form-urlencoded')) {
return { kind: 'form', body: new URLSearchParams(await res.text()) };
}
if (type.startsWith('image/')) return { kind: 'image', body: await res.blob() };
return { kind: 'unknown', body: await res.arrayBuffer() };
}
注意 304 会进入成功分支但没有 body,调用方需要单独处理,否则 res.json() 会抛错。
七、适用范围#
本次材料只有主题标题,没有可核验的接口文档或测试记录。因此上文属于设计建议而非规范复述:状态码的具体取值、字段命名、缓存时长都应视为待核实,需以目标服务的实际文档与一次可复现的调用记录为准。