Skip to content

Commit 00aaf7a

Browse files
committed
fix(webapp): apply transaction dedupe guard to both $transaction overloads
Code review caught that the infraErrorAlreadyLogged guard was added to only the named-$transaction callback; the anonymous overload still double-logged statement-level infra errors. Extract one shared boundary callback so the guard can't drift between the two overloads. Also harden the dedupe marker: define it non-enumerable (so error-spreads can't copy the tag onto a different error) and best-effort (so a frozen error object can't make the assignment throw and mask the original error).
1 parent 8ed89cf commit 00aaf7a

2 files changed

Lines changed: 33 additions & 30 deletions

File tree

apps/webapp/app/db.server.ts

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,22 @@ export type {
2929
PrismaReplicaClient,
3030
};
3131

32+
// Boundary logger for transac(): skips an error the client extension already
33+
// logged (and tagged) at the statement level, so a single failure is logged
34+
// once. Shared by both $transaction overloads so the guard can't drift.
35+
function logTransactionPrismaError(error: Prisma.PrismaClientKnownRequestError) {
36+
if (infraErrorAlreadyLogged(error)) {
37+
return;
38+
}
39+
logger.error("prisma.$transaction error", {
40+
code: error.code,
41+
meta: error.meta,
42+
stack: error.stack,
43+
message: error.message,
44+
name: error.name,
45+
});
46+
}
47+
3248
export async function $transaction<R>(
3349
prisma: PrismaClientOrTransaction,
3450
name: string,
@@ -84,39 +100,13 @@ async function $transactionInner<R>(
84100

85101
const fn = fnOrOptions as (prisma: PrismaTransactionClient, span: Span) => Promise<R>;
86102

87-
return transac(
88-
prisma,
89-
(client) => fn(client, span),
90-
(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-
}
96-
logger.error("prisma.$transaction error", {
97-
code: error.code,
98-
meta: error.meta,
99-
stack: error.stack,
100-
message: error.message,
101-
name: error.name,
102-
});
103-
},
104-
options
105-
);
103+
return transac(prisma, (client) => fn(client, span), logTransactionPrismaError, options);
106104
});
107105
} else {
108106
return transac(
109107
prisma,
110108
fnOrName,
111-
(error) => {
112-
logger.error("prisma.$transaction error", {
113-
code: error.code,
114-
meta: error.meta,
115-
stack: error.stack,
116-
message: error.message,
117-
name: error.name,
118-
});
119-
},
109+
logTransactionPrismaError,
120110
typeof fnOrOptions === "function" ? undefined : fnOrOptions
121111
);
122112
}

apps/webapp/app/utils/prismaErrors.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,21 @@ export function isInfrastructureError(error: unknown): boolean {
4949
const INFRA_ERROR_LOGGED: unique symbol = Symbol("prismaInfraErrorLogged");
5050

5151
function markInfraErrorLogged(error: unknown): void {
52-
if (typeof error === "object" && error !== null) {
53-
(error as Record<symbol, unknown>)[INFRA_ERROR_LOGGED] = true;
52+
if (typeof error !== "object" || error === null) {
53+
return;
54+
}
55+
try {
56+
// Non-enumerable so error-spreads/serializers can't copy the marker onto a
57+
// different error; try/catch so a frozen error object can't make this throw
58+
// and mask the original error as it propagates out of the catch.
59+
Object.defineProperty(error, INFRA_ERROR_LOGGED, {
60+
value: true,
61+
enumerable: false,
62+
configurable: true,
63+
writable: true,
64+
});
65+
} catch {
66+
// best-effort: a sealed/frozen error simply won't be deduped.
5467
}
5568
}
5669

0 commit comments

Comments
 (0)