DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
OnlineSIM:在线接收短信,虚拟号码租用

Node TransformStream 背压恢复后丢失请求上下文

写入先因背压停住、读取再从请求作用域外开始时,TransformStream 的回调可能读不到 AsyncLocalStorage。应用拥有回调时,可在正确作用域内分别绑定 transform 与 flush。

流是在 AsyncLocalStorage.run() 内建立的,transform() 中的 getStore() 却返回 undefined。若第一笔写入先因背压停住,稍后才从请求作用域外开始读取,应检查 TransformStream 恢复写入时的上下文,而不只是确认创建流的位置。

具体版本表现有差别:24.21.0、26.9.0 可只在首块丢失;24.22.0、24.22.1、26.10.0、26.12.1 可在后续各块继续丢失。26.12.1 还出现 flush 丢失。这里列的是这些具体版本,不能据此补成未经确认的连续区间。正式 24.22.1、26.12.1 的实现仍有对应恢复路径。

创建位置没有固定所有回调的上下文

受影响的顺序是:内部 pipe 在请求作用域内写入,第一块因背压等待;消费者稍后在另一个作用域读取,解除背压。恢复逻辑由读取方注册写入续调,后续写入队列也可能沿这个上下文继续。

因此,删掉一次等待后现象消失,只能说明调度条件变了,不能证明请求状态已经可靠传播。提高队列水位、提前读完整条流也会改变内存与流控行为,不适合作为通用修复。

先用应用已有的请求标识核对写入、transform、flush 和读取位置。数据顺序正确并不意味着上下文正确;这也不能直接归为 Streams 规范违规,因为 Streams 本身没有定义 AsyncLocalStorage 的传播契约。

在请求作用域内捕获应用自己的回调

如果应用拥有转换回调,可以在 store 仍正确时,用稳定的 AsyncLocalStorage.bind() 分别包装。下面假定每个请求调用一次工厂,requestState 是该请求已有的状态:

import { AsyncLocalStorage } from 'node:async_hooks';

const requestContext = new AsyncLocalStorage();

function createRequestTransform(requestState) {
  return requestContext.run(requestState, () => new TransformStream({
    transform: AsyncLocalStorage.bind((chunk, controller) => {
      // 此处可通过 requestContext.getStore() 读取捕获的请求状态。
      controller.enqueue(chunk);
    }),
    flush: AsyncLocalStorage.bind(() => {
      // 将实际依赖请求状态的收尾工作放在这里。
    }),
  }));
}

bind 捕获的是调用它的那一刻。已经丢失上下文以后再包装,只会捕获错误的状态。不能把某个请求的包装回调保存成所有请求共用的全局转换器,否则会串用租户、鉴权或日志信息。

如果业务只需要少量值,也可在每条流的闭包或显式参数中保留这些值,并按原生命周期释放。不要长期保存整份请求来规避传播问题。

回调绑定只覆盖这些回调

只绑定 transform,不会保证 flush、取消回调、其他 pipe 内部任务都恢复上下文。哪些回调读取请求状态,就逐一核对其捕获点。框架 API 还可能限制调用位置和响应结束后的生命周期,不能因为 getStore 恢复,就认定框架的后置任务接口允许在这里调用。

验收保留“先背压等待、再从作用域外读取”的顺序,覆盖多块数据和收尾;再让两个请求交错消费,确认各自读取自己的状态,异常与取消仍沿原流程传播。普通无等待路径成功不足以覆盖这次故障。