首页
/ tRPC × AWS Lambda URL:基于 Response Streaming 的流式 API 部署实战

tRPC × AWS Lambda URL:基于 Response Streaming 的流式 API 部署实战

2026-09-05 22:09:57作者:羿妍玫Ivan

在 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) 提供 initTRPCawsLambdaStreamingRequestHandler
@trpc/client(workspace) 客户端 createTRPCClienthttpBatchStreamLink
serverless + serverless-esbuild 部署框架与 ESM 打包
tsx 本地直接运行 TypeScript 客户端(pnpm starttsx 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,
  }),
);

要点拆解:

  1. createContext:接收 CreateAWSLambdaContextOptions<APIGatewayProxyEventV2>,从原始 Lambda 事件中取出 version(默认 '1.0')与自定义请求头 x-user,构造成 tRPC 的上下文对象。这说明在 Lambda 环境下,event.headers 中的自定义头可以直接映射到业务上下文;
  2. greet:最普通的同步查询,演示 Zod 输入校验与上下文读取;
  3. iterable:异步生成器过程,每 500ms yield 一个数字,共 10 个——这是流式响应最直观的用例,客户端可以逐个拿到 0..9
  4. deferred:模拟延迟任务(等待 wait * 10 ms),配合客户端的并发批量请求,验证批处理下各请求独立等待、按各自时序返回;
  5. 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);
  }
})();

客户端有三个要点:

  1. httpBatchStreamLink:这是专为「服务端支持流式 + 批处理」场景设计的 link,它把同一时刻的多个请求合并为一次 HTTP 调用,同时允许服务端在单次连接内流式地逐个返回各请求的结果;对响应流式 Lambda 而言是最匹配的传输层;
  2. YOUR_LAMBDA_URL 占位符:部署后必须替换为实际生成的 https://xxx.lambda-url.<region>.on.aws/ 地址,这是本地运行前唯一要改的地方;
  3. 三段演示调用分别覆盖:单请求(greet)、并发批处理(deferred 三个不同延迟的请求,验证批量流式返回的时序)、异步迭代器(iterablefor 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);
  };
}

其执行链路可以概括为:

  1. 事件归一化getPlanner(event) 位于 getPlanner.ts。它先用 determinePayloadFormat 判断事件的负载格式版本——从源码结构看,判定逻辑是:事件中带 version 属性则取之(HTTP API v2 为 '2.0'),否则按 '1.0' 处理,从而选择 v1Processorv2Processor
  2. 构造标准 Request:planner 将 Lambda 事件的 hostname/rawPath/rawQueryString、请求头(v1 还会合并 multiValueHeaders、v2 还会还原 cookies 头)、HTTP 方法以及请求体(支持 isBase64Encoded 解码)组装成一个符合 Fetch 规范的 Request 对象,并设置 duplex: 'half';tRPC 的核心 resolveResponse 因此只需面对标准 Request/Response,无需感知 Lambda 细节;
  3. 路径提取getTRPCPath 会尝试匹配 routeKey/resource 中的 {proxy+} 风格代理参数(如 ${proxy+}),命中时从 pathParameters 取 tRPC 路径,否则直接取 rawPath 去掉首字符——这就是为什么示例可以只暴露根路径;
  4. 响应回写流toStreamgetPlanner.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)全部完成。

对比同文件中的非流式 awsLambdaRequestHandlerindex.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.jsonserverless.yml 的相互约定);
  • 生成的 URL 含区域信息(README 示例为 us-east-1),客户端 url 配置需与部署区域一致;
  • 该示例未配置 VPC、日志或鉴权,x-user 头仅用于演示上下文传递;生产环境应在此层补充鉴权与区域策略。

至此,从 serverless.yml 的一行 invokeMode: RESPONSE_STREAM,到 streamifyResponse 包装的 tRPC 流式 handler,再到客户端 httpBatchStreamLink 的消费,构成了一条完整的「tRPC 全类型安全 API + Lambda 响应流式」部署链路,可作为在 AWS 上承载长任务、迭代器与批量请求场景的参考实现。

登录后查看全文
热门项目推荐
相关项目推荐