ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

tRPC 服务端错误处理完全指南:TRPCError、错误响应结构与 onError 钩子实战

tRPC 服务端错误处理完全指南:TRPCError、错误响应结构与 onError 钩子实战 tRPC 服务端错误处理完全指南TRPCError、错误响应结构与 onError 钩子实战【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpctRPC 采用 JSON-RPC 风格的错误模型只要某个 procedure过程在执行中抛错服务端就会向客户端返回一个携带error字段的结构化响应对象客户端可据此精确区分输入校验失败、未授权、资源不存在等不同情形。本文以 tRPC 官方文档 error-handling.md 为核心结合trpc/server的源码实现错误类、错误码表、HTTP 状态映射与响应形状构建系统讲解错误响应的完整结构、TRPCError的抛出方式、isDev对栈追踪的控制以及通过onError做全局错误观测的实战配置。读完本文你将能在自己的 tRPC 服务端写出一致、可控且可被客户端精确消费的错误处理代码。错误响应结构客户端拿到的是什么当 procedure 中发生错误时tRPC 总是以同一个 JSON 结构向客户端返回错误。以下是官方文档给出的、由非法请求输入触发的响应示例例如 zod 校验password 长度至少 4 位失败{ id: null, error: { message: \password\ must be at least 4 characters, code: -32600, data: { code: BAD_REQUEST, httpStatus: 400, stack: ..., path: user.changepassword } } }这个顶层对象的含义如下id批处理batch请求中对应的调用序号。单次请求发生错误时通常为null批处理场景下用于把错误归位到对应调用error.message人类可读的错误描述例如校验器返回的原文error.codeJSON-RPC 2.0 数字错误码例如示例中-32600对应BAD_REQUESTerror.datatRPC 的标准化错误元数据data.code字符串形式的 tRPC 错误码如BAD_REQUESTdata.httpStatus对应的 HTTP 状态码如400data.stack堆栈信息仅开发环境返回详见下文data.path抛出错误的 procedure 路径如user.changepassword。从源码看这个形状由 getErrorShape.ts 在错误发生时统一构建message取error.message数字code由 codes.ts 中的TRPC_ERROR_CODES_BY_KEY键值表查出data.httpStatus则由错误码经 HTTP 映射得到。若当前配置为开发模式且错误带字符串栈才把stack写入data只有path确实存在类型为 string时才附加data.path。生产环境中的栈追踪理解并覆盖isDev默认情况下tRPC只在开发环境把error.data.stack返回给客户端——因为把完整调用栈暴露给公网客户端既无必要也容易泄露内部实现细节。控制这一行为的是initTRPC.create()的isDev配置项。官方文档指出initTRPC.create()默认将isDev设置为process.env.NODE_ENV ! production。这一默认值同样可以在源码 initTRPC.ts 中直接看到const config: RootConfig$Root { ...opts, isDev: opts?.isDev ?? // eslint-disable-next-line typescript-eslint/dot-notation globalThis.process?.env[NODE_ENV] ! production, // ... };也就是说只要NODE_ENV不是字符串production例如本地开发、测试环境isDev即为true错误响应就会携带stack。若你希望在跨运行时如 Deno、Bun、Edge Runtime或容器化部署中获得确定性的行为就应手动覆盖isDevimport { initTRPC } from trpc/server; const t initTRPC.create({ isDev: false });设置isDev: false后无论运行环境如何错误响应都不会再包含data.stack字段。若需要更精细地控制哪些错误字段返回给客户端、哪些字段附加额外信息则应转向自定义错误格式化error formatting其入口与参数格式见 error-formatting.md本文第五节也会说明它与onError的分工差异。值得一提的是当你用t.mergeRouters()合并多个 router 时合并结果的isDev会取所有被合并 router 配置的“与”运算见 router.ts 中isDev: routerList.every((r) r._def._config.isDev)这一点在多团队独立子 router 的场景下需要留意。错误码全集tRPC 错误码与 HTTP 状态码映射tRPC 预定义了一套错误码每个错误码代表一种错误类型并对应不同的 HTTP 状态码。下表是官方文档给出的完整清单CodeDescriptionHTTP codePARSE_ERROR服务端收到非法 JSON或在解析请求时发生错误400BAD_REQUEST服务端因请求本身属于客户端错误而无法或拒绝处理该请求400UNAUTHORIZED请求缺少针对所请求资源的有效认证凭据因而未完成401PAYMENT_REQUIRED访问请求的资源需要先完成支付402FORBIDDEN客户端未被授权访问所请求的资源403NOT_FOUND服务端无法找到所请求的资源404METHOD_NOT_SUPPORTED服务端认识该请求方法但目标资源不支持此方法405TIMEOUT服务端希望关闭这条不再使用的连接408CONFLICT请求与目标资源的当前状态相冲突409PRECONDITION_FAILED对目标资源的访问已被拒绝前置条件失败412PAYLOAD_TOO_LARGE请求实体大于服务端定义的体积限制413UNSUPPORTED_MEDIA_TYPE服务端拒绝接受该请求因为负载格式是不支持的格式415UNPROCESSABLE_CONTENT服务端理解请求方法与请求实体但无法处理它422PRECONDITION_REQUIRED服务端因缺少必需的前置条件请求头如If-Match而无法处理请求当前置条件头与服务端状态不一致时应返回412 Precondition Failed428TOO_MANY_REQUESTS超出限流阈值或向服务端发送了过多请求429CLIENT_CLOSED_REQUEST服务端响应完成前客户端已关闭连接499INTERNAL_SERVER_ERROR发生了未指明的服务端错误500NOT_IMPLEMENTED服务端不支持完成该请求所需的功能501BAD_GATEWAY服务端从上游服务器收到无效响应502SERVICE_UNAVAILABLE服务端当前尚未准备好处理该请求503GATEWAY_TIMEOUT服务端未能及时收到上游服务器的响应以完成请求504关于底层实现的两点关键佐证HTTP 映射真实存在于源码中。上面“Code → HTTP code”的对应关系由 getHTTPStatusCode.ts 中的JSONRPC2_TO_HTTP_CODE常量表驱动其中的CLIENT_CLOSED_REQUEST: 499与官方文档一致——499 并非标准 HTTP 码而是 Nginx 生态中“客户端提前断开”的约定用法tRPC 将其沿用。JSON-RPC 数字码有“共用”现象。数字码定义在 codes.tsPARSE_ERROR为 JSON-RPC 规范的-32700BAD_REQUEST为-32600规范中的 Invalid Request而INTERNAL_SERVER_ERROR、NOT_IMPLEMENTED、BAD_GATEWAY、SERVICE_UNAVAILABLE、GATEWAY_TIMEOUT在数字层面共用-326034xx 的 UNAUTHORIZED~CLIENT_CLOSED_REQUEST 则采用“复制 HTTP 4xx 末两位”的实现自定义区段-32001~-32099。因此消费端不要以数字码为唯一判据来判断 5xx 的具体类型而应优先读取data.code字符串键。这也解释了为什么示例响应中BAD_REQUEST的数字码是-32600而文档第六节中INTERNAL_SERVER_ERROR的数字码是-32603。用getHTTPStatusCodeFromError从错误对象提取 HTTP 状态码官方文档同时强调跨进程/跨框架边界时你并不总是拿到最终 JSON 响应而更可能拿到一个TRPCError实例。tRPC 为此暴露了辅助函数getHTTPStatusCodeFromError可直接把错误码换算成 HTTP 状态码。该函数在 getHTTPStatusCode.ts 中实现为对getStatusCodeFromKey(error.code)的一行转发并经由 http.ts 从trpc/server/http路径公开导出import { TRPCError } from trpc/server; // ---cut--- import { getHTTPStatusCodeFromError } from trpc/server/http; // 例输入校验失败时你可能得到的错误 const error: TRPCError { name: TRPCError, code: BAD_REQUEST, message: password must be at least 4 characters, }; if (error instanceof TRPCError) { const httpCode getHTTPStatusCodeFromError(error); console.log(httpCode); // 400 }抛出错误使用TRPCErrortRPC 提供了Error子类TRPCError用于在 procedure 内部表示一个“发生了错误”的事实。你可以像下面这样显式抛出import { initTRPC, TRPCError } from trpc/server; const t initTRPC.create(); const theError new Error(something went wrong); const appRouter t.router({ hello: t.procedure.query(() { throw new TRPCError({ code: INTERNAL_SERVER_ERROR, message: An unexpected error occurred, please try again later., // optional: pass the original error to retain stack trace cause: theError, }); }), }); // [...]这会让客户端收到如下响应{ id: null, error: { message: An unexpected error occurred, please try again later., code: -32603, data: { code: INTERNAL_SERVER_ERROR, httpStatus: 500, stack: ..., path: hello } } }对照源码 TRPCError.tsTRPCError构造函数行为如下理解它有助于写出信息量更大的错误构造函数签名是{ message?: string; code: TRPC_ERROR_CODE_KEY; cause?: unknown }其中code取值必须来自上文错误码表的字符串键message缺省时的回退顺序为opts.message ?? cause?.message ?? opts.code——即不传message时会自动沿用cause原始错误的 message或退化为错误码本身cause会被getCauseFromUnknown处理若传入普通Error透传为Error类型若传入普通对象则包成UnknownCauseError避免把任意对象当作 Error 塞进cause该类的name固定为TRPCError便于instanceof之外的跨 realm 判断。另一个重要边界是procedure 中抛出的非TRPCError错误会被自动包装。源码中的getTRPCErrorFromUnknown会把任何未知异常转换为code: INTERNAL_SERVER_ERROR的TRPCError同时尽量继承原始错误的stack。也就是说onError与客户端最终看到的error永远是一个规范的TRPCError你不需要为“原始错误会不会破坏错误协议”担心——但这同时意味着不要在业务代码里抛出裸Error来传达业务失败语义如“余额不足”否则一律会变成 500正确的做法是抛语义明确的TRPCError。统一处理错误onError钩子所有发生在 procedure 中的错误都会在返回给客户端之前先经过 HTTP 适配层的onError方法。你可以在这里统一处理错误——例如记录日志、把 500 类内部错误上报到 Sentry 之类的 bug 报告系统。官方文档给出的 standalone 适配器示例// filename: router.ts import { initTRPC } from trpc/server; const t initTRPC.create(); export const appRouter t.router({}); // filename: server.ts // ---cut--- import { createHTTPServer } from trpc/server/adapters/standalone; import { appRouter } from ./router; const server createHTTPServer({ router: appRouter, onError(opts) { const { error, type, path, input, ctx, req } opts; console.error(Error:, error); if (error.code INTERNAL_SERVER_ERROR) { // send to bug reporting } }, });其中onError收到的参数是一个对象包含该错误以及它发生时的全部上下文import { TRPCError } from trpc/server; // ---cut--- interface OnErrorOpts { error: TRPCError; type: query | mutation | subscription | unknown; path: string | undefined; input: unknown; ctx: unknown; req: Request; }字段解读error已标准化的TRPCError可直接读取.code、.message、.causetype错误的来源类型——query、mutation、subscription解析阶段失败或未知来源时为unknownpath发生错误的 procedure 路径如user.changepassword解析阶段失败时可能为undefinedinput本次调用的输入参数ctx本次请求的上下文对象req原始 HTTPRequest可用于读取请求头、方法等信息以增强日志。需要特别说明的是onError的能力边界从源码类型看HTTP 适配层的onError对应 types.ts 中的HTTPErrorHandler它的作用是对错误做侧效处理观测、上报你不能通过修改onError里的error对象来改变最终返回给客户端的错误内容。若希望定制返回给客户端的错误形状例如剥离敏感字段、添加自定义错误码正确入口是配置initTRPC.create()时的errorFormatter错误格式化器——两者的完整分工与写法见 error-formatting.md。另外服务端上下文server-side calls即在 Node 中直接调用 router同样支持onError源码中调用器允许传入{ onError, signal }选项且错误经getTRPCErrorFromUnknown归一化后再回调见 resolveResponse.ts 与 router.ts。这意味着你在 HTTP 服务与进程内调用两种形态下都能保持一致的错误观测行为。官方在 Server Side Calls 文档中提供了完整的服务端上下文错误处理示例见 server-side-calls.md。客户端视角错误如何被结构化消费文档开篇提到错误响应contains all the information that you need to handle the error in the client。在客户端tRPC 会把上述错误响应统一规整为TRPCClientError其源码位于 packages/client/src/TRPCClientError.ts实例上暴露shape即响应中的整个error对象与data即error.data内含code、httpStatus、path等因此你可以写出类型安全的判读代码例如err.data?.code UNAUTHORIZED时跳转登录页静态方法TRPCClientError.from()会识别三类输入已存在的TRPCClientError直接复用并补全meta、结构上满足error对象的 tRPC 错误响应、以及其他未知错误——未知错误会被兜底为message取原错误或字符串Unknown error。这意味着你在服务端抛出的TRPCError语义code、httpStatus、path会被端到端保留到客户端这正是 tRPC端到端类型安全体验在错误链路上的延伸两端共享同一套错误码字面量类型服务端抛NOT_FOUND客户端即可在编译期对err.data.code NOT_FOUND的分支做类型收窄。小结tRPC 的错误处理形成了一条完整链路procedure 内抛TRPCError→getTRPCErrorFromUnknown归一化 →getErrorShape按isDev组装响应 → 经onError做全局观测 → 返回结构化 JSON → 客户端由TRPCClientError消费。实践要点可归纳为四句话业务失败务必抛出语义明确的TRPCError别用裸Error表达业务语义否则一律 500判断错误类型时优先读data.code字符串键而不是 JSON-RPC 数字码5xx 系列共用-32603需要隐藏栈或统一环境行为时在initTRPC.create()里显式传isDevonError负责记录与上报errorFormatter负责改写发给客户端的错误形状两者不要混用。如需进一步深入本主题可继续阅读仓库内的相关文档与源码错误形状定制见 error-formatting.mdisDev等初始化选项见 routers.md进程内调用场景的错误处理见 server-side-calls.md错误码与 HTTP 映射的完整定义位于 codes.ts 与 getHTTPStatusCode.ts错误响应组装逻辑位于 getErrorShape.ts。【免费下载链接】trpc‍♀️ Move Fast and Break Nothing. End-to-end typesafe APIs made easy.项目地址: https://gitcode.com/GitHub_Trending/tr/trpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表