Files
HealthLog/src/lib/prisma-errors.ts
T
Marc-André Bombeck 47f20b773d fix(auth): answer 503 for every session lookup failure that is not about the value
The catch kept a denylist of "store unreachable" signals and folded
everything else into a missing session. The pool's own connect timeout
carries no code, and 53300, 53200 and 55P03 are about the store, so all
four answered 401 and signed the client out. The shape check already
refuses crafted values before the query; the catch now keeps only a
short allowlist of input refusals (SQLSTATE class 22, Prisma
validation) as null and raises 503 for the rest.
2026-10-20 10:36:16 +02:00

87 lines
3.1 KiB
TypeScript

/**
* Shared Prisma error narrowing helpers.
*
* The P2002 (unique-constraint violation) predicate was hand-copied into
* five call sites (rollup persist, intake-slot dedup, slot upsert, legacy
* step consolidation, dense-intraday retention). v1.11.2 folds them into
* this single helper so a future change to the P2002 semantics has one
* place to live instead of five.
*
* Deliberately structural (`typeof === "object"` + `"code" in err` +
* `code === "P2002"`) rather than `instanceof Prisma.PrismaClientKnownRequestError`
* so it survives client-bundling quirks while staying specific to the
* unique-constraint code.
*/
import { classifyRestoreFailure } from "@/lib/export/restore-failure-cause";
/** Narrow a thrown Prisma error to a specific known-request code. */
function hasPrismaCode(err: unknown, code: string): boolean {
return (
typeof err === "object" &&
err !== null &&
"code" in err &&
(err as { code: unknown }).code === code
);
}
/** Narrow a thrown Prisma error to the P2002 unique-constraint code. */
export function isP2002(err: unknown): boolean {
return hasPrismaCode(err, "P2002");
}
/**
* Narrow a thrown Prisma error to the P2025 "record not found" code — the
* error `delete` / `update` raise when the target row no longer exists. Lets
* an idempotent delete treat "already gone" as success while still surfacing a
* genuine failure (a real DB fault raises a different code and rethrows).
*/
export function isP2025(err: unknown): boolean {
return hasPrismaCode(err, "P2025");
}
/**
* Prisma's own codes for a query it refused because of the value it was given:
* a value too long for its column (P2000), an invalid value or type for a
* field (P2006, P2007, P2023), and a query that failed validation (P2009,
* P2012, P2019, P2020).
*/
const PRISMA_INPUT_CODES = new Set([
"P2000",
"P2006",
"P2007",
"P2009",
"P2012",
"P2019",
"P2020",
"P2023",
]);
/**
* Whether a query failed because of the value it was asked about, rather than
* because of the store it asked. Deliberately a short allowlist: Postgres'
* data-exception class (SQLSTATE 22, an encoding or a cast it will not accept)
* and Prisma's validation refusals. Everything else, including failures with
* no recognisable code at all, is not an answer about the input.
*
* The allowlist runs this way round because the other direction cannot be
* closed: the pool's own "timeout exceeded when trying to connect" carries no
* code, nor do half the ways a connection ends, and a resource SQLSTATE
* (53300 too many connections, 53200 out of memory, 55P03 a lock timeout) says
* nothing about the value either. A denylist of "the database is down" signals
* folded each of those into "this value matches nothing".
*/
export function isInputRefusal(err: unknown): boolean {
if (
typeof err === "object" &&
err !== null &&
(err as { name?: unknown }).name === "PrismaClientValidationError"
) {
return true;
}
const { code } = classifyRestoreFailure(err);
if (code === null) return false;
if (PRISMA_INPUT_CODES.has(code)) return true;
return /^22[0-9A-Z]{3}$/.test(code);
}