5.1 状态码、头部与主体控制


5.1 状态码、头部与主体控制

出层半程从三个赋值开始:ctx.statusctx.setctx.body。它们语法简单到不值得讲,但组合语义与生效时机有一堆暗规则——赋值顺序影响类型推断、头有最后修改窗口、204 不许带 body。本节把这些暗规则摊开,让你写出的每个响应都出自明确意图而不是碰运气。

body 赋值的类型语义

ctx.body 是出层机制的核心枢纽:给什么类型,内核就按什么方式序列化。规则要背下来:

ctx.body = { id: 1 }; // 对象或数组 → JSON 序列化,content-type: application/json ctx.body = '<h1>ok</h1>'; // 字符串 → 原样输出,content-type: text/html(含标签时) ctx.body = Buffer.from([1, 2, 3]); // Buffer → 二进制流,application/octet-stream ctx.body = fs.createReadStream(p); // 可读流 → 管道接管(4.4 节的下载写法) ctx.body = null; // 置空 + 204/304 场景,配合 status 使用

两条联动规则藏在里面。其一,给 body 赋值会把 status 从 404 拉回 200——第 1.3 节讲过"无人写 body 则 404",反过来写了 body 就不再是 404,除非你显式再赋 status。其二,显式 status 要写在 body 之后或独立声明,否则可能被联动覆盖。最稳的顺序是先想清语义,两个都显式写:

ctx.status = 201; ctx.set('Location', `/orders/${id}`); ctx.body = order; // 显式声明的 201 不会被默认联动拉回 200

状态码的实用分组

不用背全部状态码,接口开发真正高频的是这几组,按"谁的问题"分:2xx 表示成功且分形态——200 常规成功带 body、201 已创建(配合 Location)、204 成功无内容(删除、无需返回体的更新)。4xx 表示客户端的问题——400 参数不合法、401 未认证(凭证缺失或失效)、403 已认证但无权、404 资源不存在、405 方法不支持、409 状态冲突、413 请求体过大、422 语义校验不过、429 触发限流。5xx 表示服务端的问题——500 未捕获异常、502/504 网关与上游超时。

一个常被问的争议:登录失败该回 401 还是 403?我的口径:没登录(或凭证失效)401,登录了但没权限 403——401 的语义里含"请重新提供凭证",与登录场景天然吻合;把"密码错误"也归 401 没问题,但把"没权限看这个资源"归 401 会误导客户端反复重登。

图 5-1:出层状态码决策树

图 5-1:出层状态码决策树

头的时间窗:出层阶段是最后修改机会

响应头与 body 一样走"声明制",真正的发送发生在内核出层阶段。这意味着出层路径上的每一层都能改头——这正是第 3.2 节把压缩层、安全头层放外侧的原因。反过来,一旦内核开始写 socket,一切修改都太晚了:

// 能改:出层阶段统一补追踪头 app.use(async (ctx, next) => { await next(); ctx.set('X-Request-Id', ctx.state.reqId); ctx.set('X-Response-Time', `${Date.now() - ctx.state.start}ms`); }); // 不能改:响应已经发出后(如 body 是流、且流已开始写入)再 set,直接抛错

实用推论:想在响应里带任何动态信息(追踪号、限流余量、灰度标记),统一起 X- 前缀在出层补,别指望处理器里记得每个接口都 set 一遍——约定胜过自觉。

高频误用对照:错的状态码长什么样

把联调中最常撞见的状态码误用摆成对照表,code review 时可以直接引用:

误用写法 问题 正确口径
参数错误回 500 客户端的锅记到服务端头上,告警噪音暴增 400 或 422
未登录回 403 客户端误判为"无权限",触发不该有的重登引导 401
资源不存在回 400 探测者借此区分"格式错"与"没这个资源" 404
一切失败回 200 加 code 字段 HTTP 层监控、网关重试、浏览器缓存全部失灵 语义状态码,body 只做补充
删除成功回 200 带整个对象 传输浪费,语义含糊 204 无内容

其中第四行值得多说一句:把所有响应都包成 200 的"伪 REST"风格在早期移动端流行过,理由是"方便统一解析"。代价是 HTTP 协议层的一切机制——网关按状态码熔断、监控系统按状态码算错误率、CDN 按状态码决定缓存——全部需要额外改造才能识别你的业务语义。协议提供了语义,就用协议的语义;body 里的 code 字段负责业务细分,两层各司其职。

特殊响应:重定向与附件

两类响应自带固定套路。重定向用 ctx.redirect,默认 302;语义明确"永久搬家"时显式给 308(保留方法与体,比老的 301 更适合 API):

router.get('/v1/users/:id', async (ctx) => { ctx.status = 308; // 永久迁移,保留方法 ctx.redirect(`/v2/users/${ctx.params.id}`); });

附件下载在 4.4 节已完整实现,这里补一个内容协商之外的便利方法——ctx.attachment(filename) 等价于手工设置 Content-Disposition,还会处理 UTF-8 文件名的兼容编码,中文文件名场景直接用它。

⚠️ 常见坑:给 204 或 304 响应赋 body。规范上这两个状态码不允许携带主体,Koa 会忽略甚至因头部已发送而抛错。需要"成功但无内容"时,ctx.status = 204 即可,不要画蛇添足。

本节要点回顾

  • body 类型决定序列化:对象出 JSON、字符串出文本、Buffer 出二进制、流出管道;
  • 两条联动规则:赋 body 会从 404 回到 200;显式 status 要显式声明,别依赖默认;
  • 状态码按"谁的锅"分组:2xx 分形态、4xx 客户端问题、5xx 服务端问题,401 与 403 的边界是"要不要重新提供凭证";
  • 头有最后修改窗口:出层阶段的层都能改头,内核开始写 socket 后一切免谈;
  • 特殊响应有固定套路:重定向 API 场景优先 308,附件用 ctx.attachment 处理中文名。

状态码与头的骨架定了,下一节填充血肉:同一资源怎么按客户端口味输出不同格式,以及错误响应的统一形态怎么设计。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U