tRPC 在 AWS Lambda 上实现 API Gateway 响应流式传输:lambda-api-gateway-streaming 示例深度解析
本文以 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 名(如greet、iterable、iterable,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 不同:除了 event 和 context,还会额外收到一个可写流参数 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.ts 的createContext里apiVersion: event.version ?? '1.0'的来源——REST API 默认走 v1(APIGatewayProxyEvent),所以示例的泛型参数用的是APIGatewayProxyEvent而非 V2 版本。
3. 三个演示 procedure 各验证一种流式场景
iterable:async function*generator procedure,每 500ms 产出一个数字、共 10 个。这是 tRPC 的“流式 query”典型形态——客户端拿到的是一个可迭代的流式响应,而不是一次性数组;deferred:接收wait参数并 sleepwait * 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。
调用链小结与适用边界
把示例与源码拼起来,一次流式请求的完整链路是:
- 客户端
httpBatchStreamLink发起请求,命中 API Gateway 的/{proxy+}通配资源; - API Gateway 以 v1 payload 调用 Lambda;
awslambda.streamifyResponse让执行环境额外注入responseStream; awsLambdaStreamingRequestHandler通过getPlanner(event)把事件还原为标准Request,交给 tRPC 核心的resolveResponse得到标准Response;planner.toStream用awslambda.HttpResponseStream.from先写出状态码与响应头,再把Response.body通过pipeline逐块泵入 Lambda 响应流;- API Gateway 以
STREAM传输模式把 Lambda 的流式输出透传给客户端,客户端按 tRPC 协议逐条解析完成的 operation。
适用边界需要留意:
- 该模式必须部署在 AWS(Lambda Function URL 或 API Gateway REST API),且 REST API 场景必须在集成上显式配置
responseTransferMode: STREAM(本示例通过 serverless.yml 的response.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 适配器用法(普通
awsLambdaRequestHandler、maxBatchSize限制等)可继续参考 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 章节) |
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00