diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts index 8cfbd91..fa61f56 100644 --- a/apps/api/src/app.ts +++ b/apps/api/src/app.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { ORPCError, onError } from "@orpc/server"; +import { ORPCError, onError, ValidationError } from "@orpc/server"; import { RPCHandler } from "@orpc/server/fetch"; import type { AgentRuntime, @@ -1048,7 +1048,18 @@ function sessionHeaders(request: Request) { * "fetch failed" and keep the host and errno one level down. */ export function logUnexpectedRpcError(error: unknown, path: readonly string[]): void { - if (error instanceof ORPCError) return; const where = `rpc ${path.join("/")} failed`; + if (error instanceof ORPCError) { + // A handler result that breaks its output contract is a server bug, not a chosen error. + if (error.code !== "INTERNAL_SERVER_ERROR" || !(error.cause instanceof ValidationError)) return; + const issues = error.cause.issues.map((issue) => ({ + path: (issue.path ?? []) + .map((segment) => (typeof segment === "object" ? segment.key : segment)) + .join("."), + problem: issue.message, + })); + getLogger().error(where, error, { issues }); + return; + } getLogger().error(where, error); } diff --git a/apps/api/src/log-unexpected-rpc-error.test.ts b/apps/api/src/log-unexpected-rpc-error.test.ts index 2e154cf..417077f 100644 --- a/apps/api/src/log-unexpected-rpc-error.test.ts +++ b/apps/api/src/log-unexpected-rpc-error.test.ts @@ -1,4 +1,4 @@ -import { ORPCError } from "@orpc/server"; +import { ORPCError, ValidationError } from "@orpc/server"; import { createLogger, createTestSink, installLogger } from "@rakazo/logging"; import { afterEach, describe, expect, it } from "vitest"; import { logUnexpectedRpcError } from "./app.js"; @@ -20,6 +20,28 @@ describe("logUnexpectedRpcError", () => { expect(sink.events).toEqual([]); }); + it("names the procedure and the fields when a result breaks its output contract", () => { + const sink = createTestSink(); + installLogger(createLogger({ service: "rakazo-api", sinks: [sink] })); + const error = new ORPCError("INTERNAL_SERVER_ERROR", { + message: "Output validation failed", + cause: new ValidationError({ + message: "Output validation failed", + issues: [{ message: "Expected string", path: ["items", 0, { key: "title" }] }], + data: { items: [{ title: null, body: "private" }] }, + }), + }); + + logUnexpectedRpcError(error, ["inbox", "list"]); + + expect(sink.events).toHaveLength(1); + expect(sink.events[0]).toMatchObject({ + message: "rpc inbox/list failed", + issues: [{ path: "items.0.title", problem: "Expected string" }], + }); + expect(JSON.stringify(sink.events[0])).not.toContain("private"); + }); + it("names the procedure and every cause behind an opaque failure", () => { const sink = createTestSink(); installLogger(createLogger({ service: "rakazo-api", sinks: [sink] }));