fix: translate duplicate-code errors into clean 409 responses

No code anywhere caught Prisma's P2002 (unique constraint violation).
Colony/apiary/queen codes, product SKUs, batch numbers, and equipment
inventory numbers are all @unique DB columns with no application-level
duplicate check ahead of the insert, relying on the constraint itself
- so a beekeeper reusing a code by mistake got a bare 500 "Internal
server error" with zero indication what went wrong, and the incident
polluted server logs as a fake error rather than a routine 4xx.

AllExceptionsFilter now recognizes P2002, returns 409 with a specific
German message naming the conflicting field when it's a single known
column (falling back to a generic duplicate message otherwise), and
logs it at warn level like any other client-side rejection.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-09 11:47:28 +02:00
1 parent dfb4de81f8
commit 961ca04a40
2 files changed
+100 -1

No files matched your search

@@ -1,4 +1,5 @@
import { ArgumentsHost, BadRequestException, Logger } from '@nestjs/common';
import { Prisma } from '../../../generated/prisma';
import { AllExceptionsFilter } from './all-exceptions.filter';
describe('AllExceptionsFilter', () => {
@@ -67,4 +68,59 @@ describe('AllExceptionsFilter', () => {
expect(warnSpy).toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
});
it('translates a P2002 unique-constraint error on a known field to a 409 with a specific message', () => {
jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
const exception = new Prisma.PrismaClientKnownRequestError(
'Unique constraint failed on the fields: (`code`)',
{ code: 'P2002', clientVersion: '6.0.0', meta: { target: ['code'] } },
);
filter.catch(exception, host);
expect(statusMock).toHaveBeenCalledWith(409);
expect(jsonMock).toHaveBeenCalledWith({
statusCode: 409,
message: 'Code wird bereits verwendet - bitte einen anderen Wert wählen',
});
});
it('falls back to a generic duplicate message for a P2002 on an unmapped or compound field', () => {
jest.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
const exception = new Prisma.PrismaClientKnownRequestError(
'Unique constraint failed on the fields: (`sourceType`,`sourceId`)',
{
code: 'P2002',
clientVersion: '6.0.0',
meta: { target: ['sourceType', 'sourceId'] },
},
);
filter.catch(exception, host);
expect(statusMock).toHaveBeenCalledWith(409);
expect(jsonMock).toHaveBeenCalledWith({
statusCode: 409,
message:
'Dieser Wert wird bereits verwendet - bitte einen anderen Wert wählen',
});
});
it('logs a P2002 error at warn level, not error level, since it is a user input problem', () => {
const errorSpy = jest
.spyOn(Logger.prototype, 'error')
.mockImplementation(() => undefined);
const warnSpy = jest
.spyOn(Logger.prototype, 'warn')
.mockImplementation(() => undefined);
const exception = new Prisma.PrismaClientKnownRequestError(
'Unique constraint failed on the fields: (`code`)',
{ code: 'P2002', clientVersion: '6.0.0', meta: { target: ['code'] } },
);
filter.catch(exception, host);
expect(warnSpy).toHaveBeenCalled();
expect(errorSpy).not.toHaveBeenCalled();
});
});
@@ -7,6 +7,32 @@ import {
Logger,
} from '@nestjs/common';
import type { Request, Response } from 'express';
import { Prisma } from '../../../generated/prisma';
// Unique-constraint fields a beekeeper might plausibly retype by mistake
// (a hive code, a batch number) - translated so the P2002 branch below can
// name the actual field instead of a raw column identifier.
const UNIQUE_FIELD_LABELS: Record<string, string> = {
code: 'Code',
email: 'E-Mail-Adresse',
sku: 'Artikelnummer (SKU)',
batchNumber: 'Chargennummer',
lotNumber: 'Los-/Chargennummer',
inventoryNumber: 'Inventarnummer',
};
function duplicateFieldMessage(target: unknown): string {
const fields = Array.isArray(target)
? target.filter((value): value is string => typeof value === 'string')
: typeof target === 'string'
? [target]
: [];
const label =
fields.length === 1 ? UNIQUE_FIELD_LABELS[fields[0]] : undefined;
return label
? `${label} wird bereits verwendet - bitte einen anderen Wert wählen`
: 'Dieser Wert wird bereits verwendet - bitte einen anderen Wert wählen';
}
/**
* Replaces Nest's default exception handling so EVERY error - not just
@@ -20,7 +46,14 @@ import type { Request, Response } from 'express';
* Client-facing behaviour is unchanged: HttpExceptions still return
* exactly the body/status Nest would have produced. Non-HttpException
* errors still collapse to a generic 500 body - the real message and
* stack go to the log, never to the client.
* stack go to the log, never to the client. The one exception is Prisma's
* P2002 (unique constraint violation): services create/update records
* straight off a DB `@unique` column (colony/apiary/queen codes, SKUs,
* batch numbers) with no application-level duplicate check, relying on
* the constraint itself - so without this branch, a beekeeper reusing a
* code by mistake saw a bare 500 "Internal server error" instead of a
* clean, specific message, and the log filled with fake server errors for
* what is really a user input problem.
*/
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
@@ -32,11 +65,21 @@ export class AllExceptionsFilter implements ExceptionFilter {
const request = ctx.getRequest<Request>();
const isHttpException = exception instanceof HttpException;
const isUniqueConstraintError =
exception instanceof Prisma.PrismaClientKnownRequestError &&
exception.code === 'P2002';
const status = isHttpException
? exception.getStatus()
: isUniqueConstraintError
? HttpStatus.CONFLICT
: HttpStatus.INTERNAL_SERVER_ERROR;
const body = isHttpException
? exception.getResponse()
: isUniqueConstraintError
? {
statusCode: status,
message: duplicateFieldMessage(exception.meta?.target),
}
: { statusCode: status, message: 'Internal server error' };
const message =
exception instanceof Error ? exception.message : 'Unknown error';