tRPC × AWS Lambda URL:基于 Response Streaming 的流式 API 部署实战
在 AWS Lambda URL(RESPONSE_STREAM 调用模式)上部署 tRPC 服务器,可以利用 Lambda 的响应流式能力把长任务、迭代器和批量请求的响应逐块推送给客户端,而不是等待整个响应生成完毕。本篇以仓库中的 lambda-url 示例 为主体,完整讲解从 serverless.yml 部署配置、awslambda.streamifyResponse() 接入方式,到 tRPC 客户端 httpBatchStreamLink 调用的全过程,并结合 tRPC 官方 AWS Lambda 适配器源码 剖析请求是如何被转换为标准 Request 对象、响应又是如何写回 HttpResponseStream 的底层链路。
一、示例定位:Lambda URL 与传统 API Gateway 的区别
该示例位于 examples/lambda-url,官方文档明确指出:本示例必须部署到 AWS 才能运行,无法在本地模拟 Lambda URL 环境。
它展示的核心能力是 Lambda Response Streaming(AWS 于 2024 年推出的响应流式特性):
- 传统 Lambda 通过 API Gateway 代理集成时,响应必须等 handler 整体返回后才能下发;
- Lambda URL 支持
RESPONSE_STREAM调用模式,Lambda 运行时直接暴露一个 HTTPS 端点,函数内部可以拿到一个可写流(responseStream),边生成边推送; - tRPC 的
awsLambdaStreamingRequestHandler正是为这种签名设计的适配器。
部署完成后会得到形如 https://<your-lambda-url>.lambda-url.us-east-1.on.aws/ 的端点(README 中的示例为 us-east-1 区域,实际区域取决于你的部署配置),tRPC 客户端直接把这个 URL 作为 url 传入即可。
二、部署流程
按照 examples/lambda-url/README.md 的说明,在示例目录下执行:
pnpm install
pnpm build
pnpm deploy
其中 pnpm deploy 对应 package.json 中的脚本 deploy: serverless deploy,即通过 Serverless Framework 完成 Lambda 函数部署与 Lambda URL 创建。示例依赖的关键包:
| 依赖 | 作用 |
|---|---|
@trpc/server(workspace) |
提供 initTRPC、awsLambdaStreamingRequestHandler |
@trpc/client(workspace) |
客户端 createTRPCClient 与 httpBatchStreamLink |
serverless + serverless-esbuild |
部署框架与 ESM 打包 |
tsx |
本地直接运行 TypeScript 客户端(pnpm start 即 tsx watch src/client.ts) |
zod |
过程输入校验 |
部署成功后,把生成的 Lambda URL 填入客户端代码(见下文第五节),然后运行:
pnpm start
三、服务端实现:src/server.ts
完整服务端代码如下,这是一个典型的「上下文构造 + 三个演示过程 + 流式 handler」结构:
import { Writable } from 'node:stream';
import { initTRPC } from '@trpc/server';
import type { CreateAWSLambdaContextOptions } from '@trpc/server/adapters/aws-lambda';
import { awsLambdaStreamingRequestHandler } from '@trpc/server/adapters/aws-lambda';
import type { APIGatewayProxyEventV2 } from 'aws-lambda';
import { z } from 'zod';
function createContext({
event,
context,
}: CreateAWSLambdaContextOptions<APIGatewayProxyEventV2>) {
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,
}),
);
要点拆解:
createContext:接收CreateAWSLambdaContextOptions<APIGatewayProxyEventV2>,从原始 Lambda 事件中取出version(默认'1.0')与自定义请求头x-user,构造成 tRPC 的上下文对象。这说明在 Lambda 环境下,event.headers中的自定义头可以直接映射到业务上下文;greet:最普通的同步查询,演示 Zod 输入校验与上下文读取;iterable:异步生成器过程,每 500msyield一个数字,共 10 个——这是流式响应最直观的用例,客户端可以逐个拿到0..9;deferred:模拟延迟任务(等待wait * 10ms),配合客户端的并发批量请求,验证批处理下各请求独立等待、按各自时序返回;handler的导出方式是关键:先用awsLambdaStreamingRequestHandler({ router, createContext })生成一个签名形如(event, responseStream, context) => Promise<void>的函数,再交给 Lambda 运行时的awslambda.streamifyResponse()包装成可部署的handler。这个包装告诉运行时:本函数将使用流式响应。
四、serverless.yml:启用 RESPONSE_STREAM 的关键配置
service: trpc-hello-world
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs20.x
functions:
hello:
handler: src/server.handler
url:
invokeMode: RESPONSE_STREAM
plugins:
- serverless-esbuild
custom:
esbuild:
platform: node
format: esm
target: node20
outExtension:
'.js': '.mjs'
逐项说明:
url.invokeMode: RESPONSE_STREAM:整个示例的开关。声明该函数需要创建 Lambda URL 并使用响应流式调用模式;若使用默认的 buffered 模式,streamifyResponse的流式链路不会生效;runtime: nodejs20.x:与 esbuild 的target: node20保持一致;Lambda Response Streaming 要求 Node.js 18+ 运行时;handler: src/server.handler:对应server.ts导出的handler;- esbuild 配置:
platform: node+format: esm+target: node20,并把产物后缀从.js改写为.mjs——因为 package.json 声明了"type": "module",函数入口必须使用 ESM 格式,这与nodejs20.x运行时的 ESM 支持相配合。
五、客户端实现:src/client.ts
import {
createTRPCClient,
httpBatchStreamLink,
loggerLink,
} from '@trpc/client';
import type { AppRouter } from './server';
const client = createTRPCClient<AppRouter>({
links: [
loggerLink({
enabled: (opts) => opts.direction === 'down',
}),
httpBatchStreamLink({
url: 'YOUR_LAMBDA_URL', // Insert your Lambda URL after deploying the serverless app
}),
],
});
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:这是专为「服务端支持流式 + 批处理」场景设计的 link,它把同一时刻的多个请求合并为一次 HTTP 调用,同时允许服务端在单次连接内流式地逐个返回各请求的结果;对响应流式 Lambda 而言是最匹配的传输层;YOUR_LAMBDA_URL占位符:部署后必须替换为实际生成的https://xxx.lambda-url.<region>.on.aws/地址,这是本地运行前唯一要改的地方;- 三段演示调用分别覆盖:单请求(
greet)、并发批处理(deferred三个不同延迟的请求,验证批量流式返回的时序)、异步迭代器(iterable用for await逐个消费,演示流式下发)。
六、源码原理:awsLambdaStreamingRequestHandler 如何工作
上面的示例行为可以在 tRPC 源码中得到印证。packages/server/src/adapters/aws-lambda/index.ts 中定义了流式 handler:
export function awsLambdaStreamingRequestHandler<
TRouter extends AnyRouter,
TEvent extends LambdaEvent,
>(opts: AWSLambdaOptions<TRouter, TEvent>): StreamifyHandler<TEvent> {
return async (event, responseStream, context) => {
const planner = getPlanner(event);
// ...
const response = await resolveResponse({
...opts,
createContext,
req: planner.request,
path: planner.path,
error: null,
onError(o) {
opts?.onError?.({ ...o, req: event });
},
});
await planner.toStream(response, responseStream);
};
}
其执行链路可以概括为:
- 事件归一化:
getPlanner(event)位于 getPlanner.ts。它先用determinePayloadFormat判断事件的负载格式版本——从源码结构看,判定逻辑是:事件中带version属性则取之(HTTP API v2 为'2.0'),否则按'1.0'处理,从而选择v1Processor或v2Processor; - 构造标准
Request:planner 将 Lambda 事件的hostname/rawPath/rawQueryString、请求头(v1 还会合并multiValueHeaders、v2 还会还原cookies头)、HTTP 方法以及请求体(支持isBase64Encoded解码)组装成一个符合 Fetch 规范的Request对象,并设置duplex: 'half';tRPC 的核心resolveResponse因此只需面对标准Request/Response,无需感知 Lambda 细节; - 路径提取:
getTRPCPath会尝试匹配routeKey/resource中的{proxy+}风格代理参数(如$、{proxy+}),命中时从pathParameters取 tRPC 路径,否则直接取rawPath去掉首字符——这就是为什么示例可以只暴露根路径; - 响应回写流:
toStream(getPlanner.ts)把 tRPC 响应拆为「元数据(statusCode/headers/cookies)」与「body」两部分,用awslambda.HttpResponseStream.from(stream, metadata)包装运行时注入的responseStream,再通过pipeline(Readable.fromWeb(response.body), responseStream)把标准Response的 Web 流逐块泵入 Lambda 的写流。对iterable这类异步迭代器过程,正是这一步让每个yield的值能立即推给客户端,而不必等到 5 秒(10 × 500ms)全部完成。
对比同文件中的非流式 awsLambdaRequestHandler(index.ts),两者共享同一套 planner/resolveResponse 逻辑,唯一差别在结尾:前者调用 planner.toResult(response) 把响应序列化为整体 JSON 返回给 API Gateway,后者调用 planner.toStream 写回流——这解释了为什么 Lambda URL 示例必须用 streamifyResponse 包装,而不是普通 handler 返回对象。
七、适用前提与注意事项
- 本方案必须部署到 AWS 才能验证,Lambda URL 的
RESPONSE_STREAM模式是托管环境特性,本地pnpm start只能跑客户端; - 运行时需 Node.js 18 及以上(示例使用
nodejs20.x),且 ESM 函数要配合format: esm+.mjs产物(见 tsconfig.json 与serverless.yml的相互约定); - 生成的 URL 含区域信息(README 示例为
us-east-1),客户端url配置需与部署区域一致; - 该示例未配置 VPC、日志或鉴权,
x-user头仅用于演示上下文传递;生产环境应在此层补充鉴权与区域策略。
至此,从 serverless.yml 的一行 invokeMode: RESPONSE_STREAM,到 streamifyResponse 包装的 tRPC 流式 handler,再到客户端 httpBatchStreamLink 的消费,构成了一条完整的「tRPC 全类型安全 API + Lambda 响应流式」部署链路,可作为在 AWS 上承载长任务、迭代器与批量请求场景的参考实现。
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 StartedRust0623
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