首页
/ tRPC 在 AWS Lambda 上实现 API Gateway 响应流式传输:lambda-api-gateway-streaming 示例深度解析

tRPC 在 AWS Lambda 上实现 API Gateway 响应流式传输:lambda-api-gateway-streaming 示例深度解析

2026-09-05 10:04:23作者:裴麒琰

本文以 tRPC 仓库中的 examples/lambda-api-gateway-streaming 示例为核心,讲解如何利用 Lambda 响应流式传输(Response Streaming)配合 API Gateway REST API 的 STREAM 传输模式,把 tRPC 的响应体逐步推送给客户端以降低首字节时间(TTFB)。读完本文,你将掌握该示例的部署方式、服务端流式 handler 的正确写法(awslambda.streamifyResponse() + awsLambdaStreamingRequestHandler())、客户端 httpBatchStreamLink 的消费方式,以及适配器底层从 Lambda 事件到流式写回的完整调用链。

这个示例解决什么问题

普通 Lambda 请求-响应模式下,API Gateway 需要拿到 Lambda 的完整返回体后才能把响应交给客户端。当 tRPC procedure 执行时间较长(例如可迭代 generator procedure、大文件下载、逐步生成内容)时,客户端只能等到全部计算完成才收到第一个字节。

AWS 提供的 Lambda Response Streaming 能力允许 Lambda 边计算边写出响应体,API Gateway 在 REST API 场景下通过把集成配置为 responseTransferMode: STREAM 把这部分能力透传到客户端。tRPC 官方文档也明确了适用范围:

响应流式传输支持 Lambda Function URLs 和 API Gateway REST APIs。对于 API Gateway REST API,需要将集成配置为 responseTransferMode: STREAM(见 AWS Lambda 适配器文档 中的 “AWS Lambda Response Streaming Adapter” 一节)。

本示例演示的正是 API Gateway REST API + Response Streaming 这条链路。README 中有两点硬性前提:

  • 该示例必须部署到 AWS 才能工作,本地无法直接运行完整链路(awslambda 命名空间由 Lambda 执行环境提供);
  • 部署后需要把 src/client.ts 中的占位 URL 替换为部署输出的 API Gateway 端点地址。

部署方式与目录结构

示例目录结构非常精简:

examples/lambda-api-gateway-streaming/
├── src/
│   ├── client.ts    # 流式客户端调用脚本
│   └── server.ts    # tRPC Lambda 流式 handler
├── package.json
├── serverless.yml   # Serverless Framework 部署配置
├── tsconfig.json
└── README.md

README 给出的部署命令为:

pnpm install
pnpm build
pnpm deploy

package.json 可以看到各命令对应的实际行为:

脚本 实际命令 说明
deploy pnpx serverless@4.28.0 deploy 通过 Serverless Framework 4.28.0 部署 Lambda 函数与 API Gateway REST API
start tsx watch src/client.ts 部署完成后本地运行客户端脚本
typecheck tsc 类型检查

部署成功后会打印 API Gateway 端点 URL,形如 https://???????.execute-api.us-east-1.amazonaws.com/dev

serverless.yml 关键配置

serverless.yml 全文很短,但每一行都对应流式链路的一个必要环节:

service: trpc-hello-world
frameworkVersion: '4'

provider:
  name: aws
  runtime: nodejs20.x

functions:
  hello:
    handler: src/server.handler
    events:
      - http:
          path: /{proxy+}
          method: any
          response:
            transferMode: STREAM

逐项说明:

  • runtime: nodejs20.x:Lambda 运行时,需满足 tRPC v11 对 Node.js 版本的要求;
  • handler: src/server.handler:指向 src/server.ts 中导出的 handler
  • path: /{proxy+}method: any通配资源 + 任意方法 是 tRPC 与 API Gateway 配合的关键。tRPC 客户端(尤其是批量链接)会在 URL 路径里拼接 procedure 名(如 greetiterableiterable,batch),每个 procedure 单独建资源会导致批量路径 404;通配资源可以让所有 procedure 路径命中同一个 Lambda。这一点在 adapter-aws-lambda 技能文档 的 “Common Mistakes” 一节也被列为高频错误;
  • response.transferMode: STREAM:把 API Gateway 与 Lambda 之间的集成配置为流式传输,这是整个示例的“总开关”。缺失这一项时,即使 Lambda 侧用 streamifyResponse 包装了 handler,API Gateway 仍然会按缓冲模式处理响应。

服务端:流式 handler 的完整写法

下面是 src/server.ts 的核心代码:

import { initTRPC } from '@trpc/server';
import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';
import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda';
import type { APIGatewayProxyEvent } from 'aws-lambda';
import { z } from 'zod';

function createContext({
  event,
  context,
}: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>) {
  return {
    event: event,
    apiVersion: (event as { version?: string }).version ?? '1.0',
    user: event.headers['x-user'],
  };
}
type Context = Awaited<ReturnType<typeof createContext>>;

const t = initTRPC.context<Context>().create();

const publicProcedure = t.procedure;
const router = t.router;

const appRouter = router({
  greet: publicProcedure.input(z.object({ name: z.string() })).query((opts) => {
    return `Greetings, ${opts.input.name}. x-user?: ${opts.ctx.user}.`;
  }),
  iterable: publicProcedure.query(async function* () {
    for (let i = 0; i < 10; i++) {
      await new Promise((resolve) => setTimeout(resolve, 500));
      yield i;
    }
  }),
  deferred: publicProcedure
    .input(z.object({ wait: z.number() }))
    .query(async (opts) => {
      await new Promise<void>((resolve) =>
        setTimeout(resolve, opts.input.wait * 10),
      );
      return opts.input.wait;
    }),
});

export type AppRouter = typeof appRouter;

export const handler = awslambda.streamifyResponse(
  awsLambdaStreamingRequestHandler({
    router: appRouter,
    createContext,
  }),
);

这段代码里有三个值得展开的技术点。

1. awslambda.streamifyResponse() 包装不可省略

流式 handler 的签名与普通 Lambda handler 不同:除了 eventcontext,还会额外收到一个可写流参数 responseStream(见 AWS Lambda 适配器文档 中的 “Response Streaming” 一节)。要让 Lambda 执行环境识别这一点,必须用 awslambda.streamifyResponse() 装饰 handler。

从源码结构看,awsLambdaStreamingRequestHandler() 返回的就是一个 StreamifyHandler<TEvent> 类型函数(packages/server/src/adapters/aws-lambda/index.ts#L86-L115):

export function awsLambdaStreamingRequestHandler<
  TRouter extends AnyRouter,
  TEvent extends LambdaEvent,
>(opts: AWSLambdaOptions<TRouter, TEvent>): StreamifyHandler<TEvent> {
  return async (event, responseStream, context) => {
    const planner = getPlanner(event);
    // ... 构建 createContext 并调用 resolveResponse 得到标准 Response
    const response = await resolveResponse({ /* ... */ });
    await planner.toStream(response, responseStream);
  };
}

如果忘记包 streamifyResponse,Lambda 会把它当普通缓冲式 handler 处理,流式能力完全失效。adapter-aws-lambda 技能文档 也把“忘记 streamifyResponse 包装”列为 HIGH 级别常见错误。

另外注意:示例中使用了 /// <reference types="aws-lambda" /> 依赖的全局 awslambda 命名空间——该命名空间由 Lambda 执行环境自动提供,类型通过 package.json 中 devDependencies 的 @types/aws-lambda 补全;本地开发环境下 awslambda 并不存在,这也是示例必须部署到 AWS 的原因之一。

2. toStream:从标准 Response 到流式写回

awsLambdaStreamingRequestHandler 与普通版 awsLambdaRequestHandler 的差别仅在最后一步:前者调用 planner.toStream(response, responseStream),后者调用 planner.toResult(response) 返回完整 JSON。

流式写回的实现位于 packages/server/src/adapters/aws-lambda/getPlanner.ts#L198-L215(以 v2 processor 为例,v1 实现与之对称):

toStream: async (response, stream) => {
  const { headers, cookies } = getHeadersAndCookiesFromResponse(response);

  const metadata = {
    statusCode: response.status,
    headers,
    cookies,
  };

  const responseStream = awslambda.HttpResponseStream.from(stream, metadata);

  if (response.body) {
    await pipeline(Readable.fromWeb(response.body as any), responseStream);
  } else {
    responseStream.end();
  }
},

可以确认的实现细节:

  • 先用 awslambda.HttpResponseStream.from(stream, metadata) 把 Lambda 传入的可写流升级为携带状态码/响应头/cookie 的 HTTP 响应流,响应元数据随第一帧先发出,这正是 TTFB 改善的来源;
  • 然后把 tRPC 内部标准 Response 的 body 用 Node 的 pipeline 逐块泵入该流——tRPC 协议层(resolveResponse)完全不需要感知“流式”这一概念,缓冲或流式只是适配器的出口差异;
  • 请求侧的转换同样在这里完成:getPlanner(event) 会依据事件的 version 字段判定 payload format(1.0 / 2.0,缺省视为 1.0,见 getPlanner.ts#L17-L27),把 API Gateway 事件还原成标准 Request 对象再交给 tRPC 核心。这也是 src/server.tscreateContextapiVersion: event.version ?? '1.0' 的来源——REST API 默认走 v1(APIGatewayProxyEvent),所以示例的泛型参数用的是 APIGatewayProxyEvent 而非 V2 版本。

3. 三个演示 procedure 各验证一种流式场景

  • iterableasync function* generator procedure,每 500ms 产出一个数字、共 10 个。这是 tRPC 的“流式 query”典型形态——客户端拿到的是一个可迭代的流式响应,而不是一次性数组;
  • deferred:接收 wait 参数并 sleep wait * 10 毫秒后返回原值,用于直观对比“响应在多少毫秒后开始到达”;
  • greet:带 zod 输入校验的普通 query,用于验证非流式请求在同一条流式链路上也正常工作。

客户端:httpBatchStreamLink 消费流式响应

客户端脚本位于 src/client.ts,按 README 要求,需先把部署输出的端点地址替换进去:

import {
  createTRPCClient,
  httpBatchStreamLink,
  loggerLink,
} from '@trpc/client';
import type { AppRouter } from './server';

const client = createTRPCClient<AppRouter>({
  links: [
    loggerLink({
      enabled: (opts) => opts.direction === 'down',
    }),
    httpBatchStreamLink({
      // Insert your API Gateway URL after deploying the serverless app
      url: 'https://???????.execute-api.us-east-1.amazonaws.com/dev',
    }),
  ],
});

void (async () => {
  try {
    const q = await client.greet.query({ name: 'Erik' });
    console.log(q);

    const deferred = await Promise.all([
      client.deferred.query({ wait: 3 }),
      client.deferred.query({ wait: 1 }),
      client.deferred.query({ wait: 2 }),
    ]);
    console.log('Deferred:', deferred);

    const iterable = await client.iterable.query();
    for await (const i of iterable) {
      console.log('Iterable:', i);
    }
  } catch (error) {
    console.log('error', error);
  }
})();

要点说明:

  • httpBatchStreamLink 是流式客户端的关键。它基于 Fetch 读取响应流并按 tRPC 协议解析逐条完成的操作;使用普通 httpBatchLink 时响应体需整体到达后才解析,流式链路中“边到边解析”的优势无法体现(adapter-aws-lambda 技能文档 中 “Streaming async generator procedure” 一节也建议 generator procedure 搭配 httpBatchStreamLink 使用);
  • loggerLink({ enabled: (opts) => opts.direction === 'down' }) 只打印响应方向日志,方便观察响应何时开始回流;
  • Promise.all([deferred(3), deferred(1), deferred(2)]) 会体现 tRPC 的批处理语义:三个不同延迟的请求被合并进一个 HTTP 请求,客户端的 Promise 按各自的真实完成时间分别 resolve,而不是统一等到最慢的那个。在 transferMode: STREAM 链路下,每个 operation 完成时它的响应块就先流出 Lambda,这正是流式模式相对缓冲模式的体感差异所在;
  • for await (const i of iterable) 遍历 generator procedure 的流式输出,数字每 500ms 逐个打印。

运行方式:

pnpm start

对应 tsx watch src/client.ts

调用链小结与适用边界

把示例与源码拼起来,一次流式请求的完整链路是:

  1. 客户端 httpBatchStreamLink 发起请求,命中 API Gateway 的 /{proxy+} 通配资源;
  2. API Gateway 以 v1 payload 调用 Lambda;awslambda.streamifyResponse 让执行环境额外注入 responseStream
  3. awsLambdaStreamingRequestHandler 通过 getPlanner(event) 把事件还原为标准 Request,交给 tRPC 核心的 resolveResponse 得到标准 Response
  4. planner.toStreamawslambda.HttpResponseStream.from 先写出状态码与响应头,再把 Response.body 通过 pipeline 逐块泵入 Lambda 响应流;
  5. API Gateway 以 STREAM 传输模式把 Lambda 的流式输出透传给客户端,客户端按 tRPC 协议逐条解析完成的 operation。

适用边界需要留意:

  • 该模式必须部署在 AWS(Lambda Function URL 或 API Gateway REST API),且 REST API 场景必须在集成上显式配置 responseTransferMode: STREAM(本示例通过 serverless.ymlresponse.transferMode: STREAM 声明);
  • REST API 事件为 v1 payload 格式,故 context 泛型应使用 APIGatewayProxyEvent;若使用 API Gateway HTTP API(v2),则改用 APIGatewayProxyEventV2,adapter 的 getPlanner 会自动识别两种格式(getPlanner.ts#L217-L254);
  • 批量链接依赖通配资源路由,per-procedure 资源 + httpBatchLink 的组合会 404,这是 技能文档 明确警示的坑;
  • 完整的 AWS Lambda 适配器用法(普通 awsLambdaRequestHandlermaxBatchSize 限制等)可继续参考 AWS Lambda 适配器文档,本示例是其“响应流式”分支的最小可运行实现。

参考文件索引

文件 作用
examples/lambda-api-gateway-streaming/README.md 示例说明:部署命令、STREAM 配置要点、客户端 URL 替换
examples/lambda-api-gateway-streaming/src/server.ts tRPC 流式 Lambda handler 与三个演示 procedure
examples/lambda-api-gateway-streaming/src/client.ts httpBatchStreamLink 客户端:批量 deferred 与 generator 流式消费
examples/lambda-api-gateway-streaming/serverless.yml Serverless 部署配置:nodejs20.x、/{proxy+} 通配、transferMode: STREAM
packages/server/src/adapters/aws-lambda/index.ts awsLambdaRequestHandler / awsLambdaStreamingRequestHandler 实现
packages/server/src/adapters/aws-lambda/getPlanner.ts v1/v2 事件→Request 转换与 toStream/toResult 出口实现
www/docs/server/adapters/aws-lambda.md AWS Lambda 适配器官方文档(含 Response Streaming 章节)
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
983
503
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384