Files
triggerdotdev--trigger.dev/apps/webapp/app/utils/prismaErrors.ts
T
2026-07-13 13:32:57 +08:00

143 lines
5.3 KiB
TypeScript

import { Prisma, type PrismaClient, isPrismaKnownError } from "@trigger.dev/database";
import { logger } from "~/services/logger.server";
// Minimal structural logger so this stays decoupled from the concrete Logger
// (and lets tests pass a capturing logger).
type ErrorLogger = { error: (message: string, fields?: Record<string, unknown>) => void };
// Prisma connectivity / infrastructure error codes — engine- and
// connection-level failures, not query- or validation-level ones. When the
// database is unreachable, Prisma 6.x throws a PrismaClientKnownRequestError
// carrying one of these codes (e.g. P1001 "Can't reach database server").
const INFRASTRUCTURE_PRISMA_CODES = new Set([
"P1001", // Can't reach database server
"P1002", // Database server reached but timed out
"P1008", // Operations timed out
"P1017", // Server has closed the connection
]);
/**
* True when `error` is a Prisma infrastructure/connectivity failure (DB
* unreachable, timed out, connection dropped) rather than a query- or
* validation-level error.
*
* These errors carry internal infrastructure detail (e.g. the database
* hostname) in their `.message`, so they must never be surfaced to API
* clients — callers should let them propagate to the generic 5xx handler
* (which both scrubs the message and is retryable by the SDK) instead of
* folding `.message` into a client-facing error.
*/
export function isInfrastructureError(error: unknown): boolean {
if (
error instanceof Prisma.PrismaClientInitializationError ||
error instanceof Prisma.PrismaClientRustPanicError ||
error instanceof Prisma.PrismaClientUnknownRequestError
) {
return true;
}
if (error instanceof Prisma.PrismaClientKnownRequestError) {
return INFRASTRUCTURE_PRISMA_CODES.has(error.code);
}
return false;
}
// One-shot marker so a single infra error is logged exactly once: the client
// extension (statement level) tags it, and the $transaction-boundary loggers
// skip a tagged error rather than logging the same failure a second time.
const INFRA_ERROR_LOGGED: unique symbol = Symbol("prismaInfraErrorLogged");
function markInfraErrorLogged(error: unknown): void {
if (typeof error !== "object" || error === null) {
return;
}
try {
// Non-enumerable so error-spreads/serializers can't copy the marker onto a
// different error; try/catch so a frozen error object can't make this throw
// and mask the original error as it propagates out of the catch.
Object.defineProperty(error, INFRA_ERROR_LOGGED, {
value: true,
enumerable: false,
configurable: true,
writable: true,
});
} catch {
// best-effort: a sealed/frozen error simply won't be deduped.
}
}
export function infraErrorAlreadyLogged(error: unknown): boolean {
return (
typeof error === "object" &&
error !== null &&
(error as Record<symbol, unknown>)[INFRA_ERROR_LOGGED] === true
);
}
// Logs infrastructure failures (P1xxx-class, see isInfrastructureError) and
// rethrows the ORIGINAL error: callers branch on error.code, and this fires
// per-statement inside transactions, so converting it would break that.
export function captureInfrastructureErrors<T extends PrismaClient>(
client: T,
log: ErrorLogger = logger
): T {
return client.$extends({
name: "infrastructure-error-capture",
query: {
$allOperations: async ({ model, operation, args, query }) => {
try {
return await query(args);
} catch (error) {
if (isInfrastructureError(error)) {
log.error("prisma infrastructure error", {
model,
operation,
code: error instanceof Prisma.PrismaClientKnownRequestError ? error.code : undefined,
meta: error instanceof Prisma.PrismaClientKnownRequestError ? error.meta : undefined,
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined,
});
markInfraErrorLogged(error);
}
throw error;
}
},
},
}) as unknown as T;
}
// Logs infrastructure errors that reach the $transaction boundary WITHOUT a
// Prisma error code (e.g. PrismaClientInitializationError). Coded errors there
// are already logged by transac()'s callback, and errors that bubbled up from a
// statement were already logged (and tagged) by the client extension — both are
// skipped here to avoid double-logging. Returns whether it logged.
export function logTransactionInfrastructureError(
error: unknown,
log: ErrorLogger = logger
): boolean {
if (
!isInfrastructureError(error) ||
isPrismaKnownError(error) ||
infraErrorAlreadyLogged(error)
) {
return false;
}
log.error("prisma.$transaction infrastructure error", {
message: error instanceof Error ? error.message : String(error),
name: error instanceof Error ? error.name : undefined,
stack: error instanceof Error ? error.stack : undefined,
});
return true;
}
// Replaces a Prisma infrastructure error's message (which carries the DB
// hostname) with a generic one before it reaches an API client. Any other
// error's message is returned unchanged. Status codes/headers are unaffected.
export function clientSafeErrorMessage(error: Error): string {
return isInfrastructureError(error) ? "Internal Server Error" : error.message;
}