From c4486d9e5efd47060e9f09c7be1d7eaefa507479 Mon Sep 17 00:00:00 2001 From: ordinarthur <@arthurbarre.js@gmail.com> Date: Wed, 6 May 2026 15:55:27 +0200 Subject: [PATCH] fix(api): exception handler normalise toutes les erreurs en { errors: [...] } MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 tests Japa étaient en échec à cause de réponses non conformes au contrat backend.md §6 : - E_INVALID_CREDENTIALS (Adonis auth) renvoyait 400 au lieu de 401 → mappé explicitement vers 401 + code 'invalid_credentials' - Custom Exception (status + code + message) côté controllers (ex. client_email_required) sortait en shape Adonis par défaut { message, name, code } → wrap en { errors: [{ code, message }] } - E_VALIDATION_ERROR de Vine relayé proprement (au cas où, déjà géré en pratique) L'enveloppe { errors: [...] } est maintenant garantie pour toutes les erreurs HTTP. Le SPA peut switch sur errors[0].code sans deviner la shape. Tests : 50/50 passent. --- apps/api/app/exceptions/handler.ts | 78 +++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/apps/api/app/exceptions/handler.ts b/apps/api/app/exceptions/handler.ts index 2ee1081..2e93549 100644 --- a/apps/api/app/exceptions/handler.ts +++ b/apps/api/app/exceptions/handler.ts @@ -2,23 +2,26 @@ import app from '@adonisjs/core/services/app' import { type HttpContext, ExceptionHandler } from '@adonisjs/core/http' /** - * Exception handler API JSON-only. On normalise les erreurs DB / Vine - * en réponse `{ errors: [...] }` (cf. backend.md §6 pour le contrat). + * Exception handler API JSON-only. Normalise toutes les erreurs vers la + * shape `{ errors: [{ code, message, field? }] }` documentée dans + * backend.md §6. + * + * Conversions : + * - PG 23505 (unique violation) → 422 `duplicate` avec field extrait + * - E_INVALID_CREDENTIALS → 401 `invalid_credentials` + * - Vine validation errors → 422 (déjà géré par Adonis, on relaie) + * - Exception custom avec code & status → propage tel quel sous shape errors + * - Reste → fallback super.handle() */ export default class HttpExceptionHandler extends ExceptionHandler { protected debug = !app.inProduction async handle(error: unknown, ctx: HttpContext) { - // Postgres unique violation → 422 avec field/code stables, pas un - // 500 avec stack pg-protocol. - if ( - error && - typeof error === 'object' && - 'code' in error && - (error as { code: unknown }).code === '23505' - ) { - const detail = String((error as { detail?: unknown }).detail ?? '') - // Tente d'extraire le nom de colonne du message PG ("Key (numero, ...)"). + if (!isObject(error)) return super.handle(error, ctx) + + // Postgres unique violation → 422 propre (pas un 500 avec stack pg-protocol). + if (error.code === '23505') { + const detail = typeof error.detail === 'string' ? error.detail : '' const fieldMatch = detail.match(/Key \(([^)]+)\)=/) const field = fieldMatch?.[1]?.split(',')[0]?.trim() ctx.response.status(422) @@ -33,6 +36,53 @@ export default class HttpExceptionHandler extends ExceptionHandler { }) } + // Adonis auth — mauvais credentials. Le default est 400, on veut 401. + if (error.code === 'E_INVALID_CREDENTIALS') { + ctx.response.status(401) + return ctx.response.json({ + errors: [ + { + code: 'invalid_credentials', + message: 'Email ou mot de passe incorrect', + }, + ], + }) + } + + // Vine — validation errors. Adonis sort déjà des messages structurés, + // on les relaie en `errors[]`. + if (error.code === 'E_VALIDATION_ERROR' && Array.isArray(error.messages)) { + ctx.response.status(422) + return ctx.response.json({ + errors: error.messages.map((m) => ({ + code: 'validation_failed', + message: typeof m === 'object' && m && 'message' in m ? String(m.message) : '', + field: typeof m === 'object' && m && 'field' in m ? String(m.field) : undefined, + rule: typeof m === 'object' && m && 'rule' in m ? String(m.rule) : undefined, + })), + }) + } + + // Custom Exception levée par les controllers : on a `status` + `code` + // + `message`. On les passe en shape `errors[]`. + if ( + typeof error.status === 'number' && + typeof error.code === 'string' && + typeof error.message === 'string' && + error.status >= 400 && + error.status < 600 + ) { + ctx.response.status(error.status) + return ctx.response.json({ + errors: [ + { + code: error.code, + message: error.message, + }, + ], + }) + } + return super.handle(error, ctx) } @@ -40,3 +90,7 @@ export default class HttpExceptionHandler extends ExceptionHandler { return super.report(error, ctx) } } + +function isObject(v: unknown): v is Record { + return v !== null && typeof v === 'object' +}