流式请求可能在三个阶段失败:建立连接前的 fetch() 异常、HTTP 非成功响应,以及已经开始读取后的流中断。这三类错误必须统一进入可测试的错误协议,不能把浏览器产生的 Failed to fetch 等原始文案直接展示给用户。
统一错误码
网络层应该保留用户主动取消,同时把其他连接与读取异常归一化:
const signal = options.signal
try {
const response = await fetch(url, options)
if (!response.ok) throw await parseStructuredError(response)
if (!response.body) {
throw new AppError('响应不包含可读数据流', 'INVALID_STREAM')
}
await consumeStream(response.body)
} catch (error) {
if (signal?.aborted) throw error
if (error instanceof AppError) throw error
throw new AppError('服务连接中断', 'NETWORK_ERROR')
}
检查 signal.aborted 而不只匹配 AbortError 名称,可以保留 abort(reason) 传入的自定义取消原因。Response.body 的类型允许为 null,流式端点还应把“成功状态但没有流”归为独立协议错误。界面层只根据稳定的 code 映射可读原因,例如网络中断、限频、额度不足、上游不可用、超时、上下文过长和鉴权失败。后端返回的结构化错误码应原样保留;浏览器或代理的实现细节不应成为用户文案。相关底层语义见 Fetch Standard。
明确失败对象与备用项
如果请求可能在服务端被路由到备用资源,界面需要分别记录:
- 用户发起请求时选择的资源。
- 流中已经确认的实际资源。
错误发生在解析实际资源之前时,提示所选资源;之后发生时,提示实际资源。备用建议应来自同一批服务端发布的可用列表,从失败位置向后查找,跳过冷却或禁用项,并允许从列表末尾回绕。
只有资源相关错误才建议切换。上下文过长、内容拒绝、鉴权失败或本地持久化失败不能通过切换资源解决,不应显示误导性的切换按钮。
不要盲目自动重试流式 POST
连接在客户端看来失败时,服务端可能已经完成生成或持久化。自动重放非幂等 POST 可能造成重复消息、重复扣费或重复副作用。更安全的默认交互是:
- 明确显示失败资源与可读原因。
- 提供一键切换到建议资源。
- 由用户确认后重新发送。
真正需要自动重试时,应先为请求增加稳定的幂等键,并让服务端能够查询、复用或拒绝重复请求。
验证
至少覆盖以下测试:
fetch()直接拒绝时不泄漏浏览器原始文案。- HTTP 非成功响应保留后端结构化错误码。
- 流读取中断映射为同一个网络错误码。
- 备用选择跳过冷却项并能回绕。
- 鉴权、上下文等非资源错误不提供切换建议。
- 用户主动取消不会显示成服务故障。
这套边界可通过聚焦单元测试验证错误映射与备用选择,再用真实流式接口验证首包、降级解析和连接中断行为。