Log which fields break an RPC output contract (#207)
An 'Output validation failed' response was swallowed by the RPC error logger because it is an ORPCError, so the failing procedure and fields never reached the logs. Log those cases with the procedure and the field paths, without the payload. Claude-Session: https://claude.ai/code/session_01LFxDxzmAy9mJnKWoBrCtRB Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
557f8486cd
commit
20421e6d58
@ -1,5 +1,5 @@
|
|||||||
import { randomUUID } from "node:crypto";
|
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 { RPCHandler } from "@orpc/server/fetch";
|
||||||
import type {
|
import type {
|
||||||
AgentRuntime,
|
AgentRuntime,
|
||||||
@ -1048,7 +1048,18 @@ function sessionHeaders(request: Request) {
|
|||||||
* "fetch failed" and keep the host and errno one level down.
|
* "fetch failed" and keep the host and errno one level down.
|
||||||
*/
|
*/
|
||||||
export function logUnexpectedRpcError(error: unknown, path: readonly string[]): void {
|
export function logUnexpectedRpcError(error: unknown, path: readonly string[]): void {
|
||||||
if (error instanceof ORPCError) return;
|
|
||||||
const where = `rpc ${path.join("/")} failed`;
|
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);
|
getLogger().error(where, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { ORPCError } from "@orpc/server";
|
import { ORPCError, ValidationError } from "@orpc/server";
|
||||||
import { createLogger, createTestSink, installLogger } from "@rakazo/logging";
|
import { createLogger, createTestSink, installLogger } from "@rakazo/logging";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { logUnexpectedRpcError } from "./app.js";
|
import { logUnexpectedRpcError } from "./app.js";
|
||||||
@ -20,6 +20,28 @@ describe("logUnexpectedRpcError", () => {
|
|||||||
expect(sink.events).toEqual([]);
|
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", () => {
|
it("names the procedure and every cause behind an opaque failure", () => {
|
||||||
const sink = createTestSink();
|
const sink = createTestSink();
|
||||||
installLogger(createLogger({ service: "rakazo-api", sinks: [sink] }));
|
installLogger(createLogger({ service: "rakazo-api", sinks: [sink] }));
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user