题干与适用场景
你要在浏览器或边缘运行时处理二进制请求体。平台新增 Request.bytes(),但请求体只能消费一次。请说明何时使用它、如何避免重复读取、如何限制内存并设计回退。
MDN 说明 Request.bytes() 返回 Promise,成功值是请求体的 Uint8Array;请求体读取属于消费 body 的操作。面试重点是 Fetch body 生命周期、内存边界与错误处理,不是把新方法当成任意场景的默认选择。
面试官考察点
- 能否解释 body 已使用状态,以及
bytes()、arrayBuffer()、json()、text()的互斥关系。 - 能否根据请求大小和处理目标选择完整读取或流式读取。
- 能否在重试、日志、签名验证和业务解析之间安排唯一读取点。
- 能否设计能力检测、限制、取消、超时和错误映射。
- 能否避免把原始请求体写入日志、缓存或跨租户共享对象。
需要澄清的问题
- 请求体大小、来源、Content-Type、是否需要签名验证和目标运行时是什么?
- 业务需要完整字节数组、增量哈希、分片上传还是解码后的结构?
- 失败重试由客户端、网关还是业务服务负责?请求是否可安全重放?
- 请求体是否包含个人数据、凭证或租户隔离信息?
Body 生命周期与 API 选择
Request.bytes() 一旦成功消费 body,后续再调用 json() 或 text() 会失败;反过来也一样。若同一请求需要多个消费者,应在唯一入口读取一次,再把受控结果传递给解析、签名和业务层。不要把 Request 对象到处传递,让多个模块竞争读取权。
完整读取适合受限的小请求或需要一次性校验的协议。大请求应优先使用 request.body 的流式读取,在读取过程中做增量哈希和大小检查。bytes() 返回 Uint8Array 方便字节处理,但不代表读取过程没有内存峰值。
内存、大小限制与背压
入口先检查 Content-Length,但不能只信任它;分块传输需要在读取时累计实际字节数并超过上限立即取消。为完整读取设置租户、路由和全局上限,避免多个并发请求同时分配不受控数组。流式路径使用背压,消费者处理不过来时暂停读取。
对于需要转发的请求,不要为了记录或重试而复制多个完整数组。若必须重放,先把经过大小限制的字节写入受控临时存储,并绑定租户、过期时间和摘要;原始请求体不得进入普通日志。
签名验证、解析与错误契约
需要验证签名时,先确定签名覆盖的是原始字节还是规范化结构。原始字节应在一次读取后同时送入验证器和解析器,避免 JSON 重序列化改变签名输入。解析失败、签名失败、超限、取消和超时应映射为不同的业务错误,不能统一返回“格式错误”。
边缘函数可能有不同的 body API;用能力检测选择 bytes()、arrayBuffer() 或流式路径,并在启动时记录运行时能力版本。回退必须保持同样的大小限制、摘要和错误语义,不得因为旧平台而静默放宽安全策略。
取消、超时与重放
把 AbortSignal 传给读取和下游操作,客户端断开或超过截止时间时立即停止读取并释放引用。对于带副作用的请求,超时后不能自动重放;需要幂等键、服务端去重和明确的重试窗口。纯校验或查询请求也要验证重放是否会泄露敏感数据。
网关重试可能产生多个请求副本,因此签名验证、幂等键和审计事件应关联同一请求 ID。失败响应应告诉调用方是否可以安全重试,不泄露内部读取状态或密钥信息。
安全与可观测性
限制 Content-Type、请求大小、读取时长和并发;对压缩请求考虑解压后的大小,防止压缩炸弹。按租户隔离缓存与临时对象,敏感字节只在必要范围内存在。日志记录字节数、耗时、取消原因、错误类别和请求摘要,不记录内容。
监控 body 消费失败率、超限率、p95 读取时间、峰值内存、下游背压和重试率。若 bytes() 在某运行时的失败率升高,切换到已验证的回退并保留告警;不能通过捕获异常后再次读取同一 body 来“修复”。
验证清单与追问
测试空 body、单字节、接近上限、超过上限、分块传输、慢速客户端、客户端中断、重复读取、签名不匹配、压缩炸弹、并发请求、旧运行时回退和下游超时。验证每条路径都只消费一次 body,并检查取消后没有继续读取。
为什么不能先 json() 再用 bytes() 验签?
第一次读取已经消费 body,且 JSON 解析与重新序列化可能改变空白、顺序或编码。应先读取原始字节,再按同一字节输入验签和解析。
什么时候 bytes() 比流式读取合适?
请求很小、内存上限明确、协议需要完整字节校验时可用。大文件、持续上传或高并发场景应采用流式读取与增量处理。
如何证明回退没有降低安全性?
对每个运行时强制覆盖能力路径,比较大小限制、摘要、签名、错误码、取消与审计事件;回退不能改变安全策略或重试边界。