Skip to content

Commit 8ed89cf

Browse files
committed
fix(webapp): close replay-route infra leak and log infra errors once
- api.v1.runs.$runParam.replay.ts returned a raw error.message; route it through clientSafeErrorMessage so infra errors are obfuscated like the other patched routes. - Tag an infra error when the client extension logs it at the statement level, and skip it in the $transaction-boundary loggers, so a single failure is logged exactly once instead of twice inside a transaction.
1 parent f4916fb commit 8ed89cf

4 files changed

Lines changed: 41 additions & 5 deletions

File tree

apps/webapp/app/db.server.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { logger } from "./services/logger.server";
1414
import { isValidDatabaseUrl } from "./utils/db";
1515
import {
1616
captureInfrastructureErrors,
17+
infraErrorAlreadyLogged,
1718
logTransactionInfrastructureError,
1819
} from "./utils/prismaErrors";
1920
import { singleton } from "./utils/singleton";
@@ -87,6 +88,11 @@ async function $transactionInner<R>(
8788
prisma,
8889
(client) => fn(client, span),
8990
(error) => {
91+
// Skip if the client extension already logged this at the statement
92+
// level — only commit-time errors that bypass it are logged here.
93+
if (infraErrorAlreadyLogged(error)) {
94+
return;
95+
}
9096
logger.error("prisma.$transaction error", {
9197
code: error.code,
9298
meta: error.meta,

apps/webapp/app/routes/api.v1.runs.$runParam.replay.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { logger } from "~/services/logger.server";
88
import { ReplayTaskRunService } from "~/v3/services/replayTaskRun.server";
99
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
1010
import { sanitizeTriggerSource } from "~/utils/triggerSource";
11+
import { clientSafeErrorMessage } from "~/utils/prismaErrors";
1112

1213
const ParamsSchema = z.object({
1314
/* This is the run friendly ID */
@@ -145,7 +146,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
145146
},
146147
run: runParam,
147148
});
148-
return json({ error: error.message }, { status: 400 });
149+
return json({ error: clientSafeErrorMessage(error) }, { status: 400 });
149150
} else {
150151
logger.error("Failed to replay run", { error: JSON.stringify(error), run: runParam });
151152
return json({ error: JSON.stringify(error) }, { status: 400 });

apps/webapp/app/utils/prismaErrors.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ export function isInfrastructureError(error: unknown): boolean {
4343
return false;
4444
}
4545

46+
// One-shot marker so a single infra error is logged exactly once: the client
47+
// extension (statement level) tags it, and the $transaction-boundary loggers
48+
// skip a tagged error rather than logging the same failure a second time.
49+
const INFRA_ERROR_LOGGED: unique symbol = Symbol("prismaInfraErrorLogged");
50+
51+
function markInfraErrorLogged(error: unknown): void {
52+
if (typeof error === "object" && error !== null) {
53+
(error as Record<symbol, unknown>)[INFRA_ERROR_LOGGED] = true;
54+
}
55+
}
56+
57+
export function infraErrorAlreadyLogged(error: unknown): boolean {
58+
return (
59+
typeof error === "object" &&
60+
error !== null &&
61+
(error as Record<symbol, unknown>)[INFRA_ERROR_LOGGED] === true
62+
);
63+
}
64+
4665
// Logs infrastructure failures (P1xxx-class, see isInfrastructureError) and
4766
// rethrows the ORIGINAL error: callers branch on error.code, and this fires
4867
// per-statement inside transactions, so converting it would break that.
@@ -66,6 +85,7 @@ export function captureInfrastructureErrors<T extends PrismaClient>(
6685
message: error instanceof Error ? error.message : String(error),
6786
stack: error instanceof Error ? error.stack : undefined,
6887
});
88+
markInfraErrorLogged(error);
6989
}
7090

7191
throw error;
@@ -76,14 +96,15 @@ export function captureInfrastructureErrors<T extends PrismaClient>(
7696
}
7797

7898
// Logs infrastructure errors that reach the $transaction boundary WITHOUT a
79-
// Prisma error code (e.g. PrismaClientInitializationError). Coded errors are
80-
// already logged by transac()'s callback, so they are skipped here to avoid
81-
// double-logging. Returns whether it logged.
99+
// Prisma error code (e.g. PrismaClientInitializationError). Coded errors there
100+
// are already logged by transac()'s callback, and errors that bubbled up from a
101+
// statement were already logged (and tagged) by the client extension — both are
102+
// skipped here to avoid double-logging. Returns whether it logged.
82103
export function logTransactionInfrastructureError(
83104
error: unknown,
84105
log: ErrorLogger = logger
85106
): boolean {
86-
if (!isInfrastructureError(error) || isPrismaKnownError(error)) {
107+
if (!isInfrastructureError(error) || isPrismaKnownError(error) || infraErrorAlreadyLogged(error)) {
87108
return false;
88109
}
89110

apps/webapp/test/prismaInfrastructureErrorCapture.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Prisma, PrismaClient } from "@trigger.dev/database";
44
import {
55
captureInfrastructureErrors,
66
clientSafeErrorMessage,
7+
infraErrorAlreadyLogged,
78
logTransactionInfrastructureError,
89
} from "~/utils/prismaErrors";
910

@@ -117,6 +118,13 @@ describe("captureInfrastructureErrors", () => {
117118
expect(log.captured[0].message).toBe("prisma infrastructure error");
118119
expect(log.captured[0].fields?.operation).toBe("findFirst");
119120
expect(log.captured[0].fields?.model).toBe("SecretStore");
121+
122+
// Dedupe: the extension tagged it, so a $transaction-boundary logger
123+
// seeing the same error must NOT log it a second time.
124+
expect(infraErrorAlreadyLogged(error)).toBe(true);
125+
const boundaryLog = capturingLogger();
126+
expect(logTransactionInfrastructureError(error, boundaryLog)).toBe(false);
127+
expect(boundaryLog.captured).toHaveLength(0);
120128
} finally {
121129
await unreachable.$disconnect();
122130
}

0 commit comments

Comments
 (0)