import type { FastifyError, FastifyInstance, FastifyRequest } from 'fastify'; import { AppError } from './errors.js'; import { getSessionUser } from './session.js'; interface ErrorBody { error: string; message: string; } const TOO_LARGE_MESSAGE = 'Le fichier dépasse la taille maximale autorisée'; /** Détermine le statut HTTP et le corps `{ error, message }` d'une erreur. */ function resolveError( error: FastifyError, request: FastifyRequest, ): { status: number; body: ErrorBody } { if (error instanceof AppError) { return { status: error.statusCode, body: { error: error.name, message: error.message } }; } switch (error.code) { case 'FST_REQ_FILE_TOO_LARGE': return { status: 413, body: { error: 'PayloadTooLargeError', message: TOO_LARGE_MESSAGE } }; case 'FST_FILES_LIMIT': return { status: 400, body: { error: 'ValidationError', message: 'Un seul fichier par requête' }, }; case 'FST_INVALID_MULTIPART_CONTENT_TYPE': return { status: 400, body: { error: 'ValidationError', message: 'Requête multipart/form-data attendue' }, }; default: break; } if (error.validation) { return { status: 400, body: { error: 'ValidationError', message: error.message } }; } request.log.error({ err: error }, 'Erreur non gérée'); return { status: 500, body: { error: 'InternalServerError', message: 'Erreur interne' } }; } /** Chemin de la requête sans la chaîne de query. */ function pathOf(request: FastifyRequest): string { return request.url.split('?', 1)[0] ?? request.url; } function requestedTarget(request: FastifyRequest): string | undefined { const query = request.query as { target?: unknown }; return typeof query.target === 'string' ? query.target : undefined; } function isUploadPath(path: string): boolean { return path === '/upload' || path.startsWith('/upload/'); } /** * Installe le gestionnaire d'erreurs global. * * - `AppError` -> `statusCode` + `{ error, message }`. * - Erreurs `@fastify/multipart` connues -> 4xx explicites. * - Erreurs de validation de schéma Fastify -> 400. * - Tout le reste -> 500 générique (détails seulement dans les logs). * * Toute réponse 4xx/5xx sur une route `/upload*` déclenche une notification * webhook `error`. */ export function registerErrorHandler(app: FastifyInstance): void { app.setErrorHandler((error: FastifyError, request, reply): void => { const { status, body } = resolveError(error, request); reply.status(status).send(body); const path = pathOf(request); if (status >= 400 && isUploadPath(path)) { app.notifier.uploadFailed({ message: body.message, statusCode: status, method: request.method, path, target: requestedTarget(request), user: getSessionUser(request)?.sub, }); } }); }