This commit is contained in:
natib21
2026-07-14 08:47:46 +00:00
parent 22cf8eabd1
commit e63e6a2fc0
6 changed files with 92 additions and 16 deletions

View File

@@ -16,6 +16,67 @@ interface ErrorResponseBody {
path: string;
}
interface PgDriverError {
code?: string;
detail?: string;
column?: string;
message?: string;
}
const toSentenceCase = (snake: string): string =>
snake.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
/**
* Translate common Postgres constraint failures (surfaced by TypeORM as
* QueryFailedError, which is NOT an HttpException and would otherwise fall
* through as an opaque 500) into actionable 400s. Clients rely on the
* "still referenced" wording to show friendly cannot-delete messages.
*/
const translatePgError = (
driver: PgDriverError,
): { status: number; message: string } | null => {
const detail = driver.detail ?? "";
switch (driver.code) {
case "23503": {
const stillReferenced = detail.match(
/is still referenced from table "(.*?)"/,
);
if (stillReferenced) {
const table = toSentenceCase(stillReferenced[1]);
return {
status: HttpStatus.BAD_REQUEST,
message: `Cannot delete: this record is still referenced by ${table}.`,
};
}
const notPresent = detail.match(/is not present in table "(.*?)"/);
const entity = toSentenceCase(notPresent?.[1] ?? "referenced entity");
return {
status: HttpStatus.BAD_REQUEST,
message: `The specified ${entity} does not exist.`,
};
}
case "23505": {
const pair = detail.match(/\((.*?)\)=\((.*?)\)/);
return {
status: HttpStatus.BAD_REQUEST,
message: `Duplicate entry: '${pair?.[2] ?? "value"}' already exists for '${toSentenceCase(pair?.[1] ?? "field")}'.`,
};
}
case "23502":
return {
status: HttpStatus.BAD_REQUEST,
message: `Missing required field: ${toSentenceCase(driver.column ?? "field")}.`,
};
case "22P02":
return {
status: HttpStatus.BAD_REQUEST,
message: "Invalid input format (e.g., wrong UUID or number).",
};
default:
return null;
}
};
@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(HttpExceptionFilter.name);
@@ -25,13 +86,23 @@ export class HttpExceptionFilter implements ExceptionFilter {
const response = ctx.getResponse();
const request = ctx.getRequest();
const status =
exception instanceof HttpException
const pgTranslated =
(exception as Error)?.name === "QueryFailedError"
? translatePgError(
((exception as { driverError?: PgDriverError }).driverError ??
{}) as PgDriverError,
)
: null;
const status = pgTranslated
? pgTranslated.status
: exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;
const messageRaw =
exception instanceof HttpException
const messageRaw = pgTranslated
? pgTranslated.message
: exception instanceof HttpException
? exception.getResponse()
: "Internal server error";