batchCooking/packages/error-tools/src/error-handler.service.ts
Nicolas c5282ba4a7 style: préfixe tous les membres private/protected par _
Convention demandée par l'utilisateur : `emit` -> `_emit`, sur toutes les
classes du repo, pas seulement le nouveau code. `public` reste sans
préfixe.

- LoggerService (apps/api) : _minSeverity, _emit.
- ApiClient (apps/web) : _request (39 sites d'appel mis à jour).
- ErrorHandlerService (packages/error-tools) : _fromZodError,
  _fromHttpError, _fromUnknownError.
- ExpressServer (packages/express-tools) : _app, _registeredRoutes.

Aucun changement de comportement — pur renommage interne, aucune méthode
private/protected n'était appelée depuis l'extérieur de sa classe.

Vérifié : pnpm --filter api test (303/303), pnpm lint/build clean sur
tout le repo (apps/api, apps/web, packages/*).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-21 10:32:52 +02:00

80 lines
3 KiB
TypeScript

import { type ApiErrorResponse, ErrorCode } from "@batch-cooking/shared";
import { ZodError } from "zod";
import { HttpError } from "./http-error.js";
/** Return value of {@link ErrorHandlerService.handle}: everything a caller needs to send an HTTP response. */
export interface ErrorHandlingResult {
/** HTTP status code to respond with. */
status: number;
/** JSON body to respond with — matches the shared {@link ApiErrorResponse} contract. */
body: ApiErrorResponse;
}
/**
* Centralizes every "how do we turn a thrown error into an HTTP response"
* decision in one place, so route handlers and framework-specific
* middleware (e.g. `createErrorMiddleware` in `@batch-cooking/express-tools`)
* never duplicate this logic. Framework-agnostic on purpose — it only maps
* an error to `{ status, body }` and has zero dependency on Express or any
* other HTTP framework.
*
* Recognizes three error shapes today (zod validation failures, our own
* `HttpError`, and anything else) and always falls back to a safe, generic
* 500 for the unknown case — a caller of `handle()` never needs its own
* fallback branch.
*/
export class ErrorHandlerService {
/**
* Maps any thrown value into a status + body pair ready to send to the
* client. Always succeeds — an error that doesn't match a known shape
* becomes a generic {@link ErrorCode.INTERNAL_ERROR} and is logged.
*/
public handle(error: unknown): ErrorHandlingResult {
if (error instanceof ZodError) {
return this._fromZodError(error);
}
if (error instanceof HttpError) {
return this._fromHttpError(error);
}
return this._fromUnknownError(error);
}
/** Request body/query failed schema validation — always a 400. */
private _fromZodError(error: ZodError): ErrorHandlingResult {
return {
status: 400,
body: {
code: ErrorCode.VALIDATION_ERROR,
message: "Validation error",
details: error.flatten().fieldErrors,
},
};
}
/** Our own typed error — status/code were decided by whoever threw it. */
private _fromHttpError(error: HttpError): ErrorHandlingResult {
return {
status: error.status,
body: { code: error.code, message: error.message },
};
}
/**
* Anything unrecognized: never leaks internal details to the client —
* always a generic 500. Doesn't log the error itself — this class only
* maps an error to `{ status, body }` (see the class doc comment); a
* caller wanting this logged server-side does so on its own before/
* around calling `handle()` (see `apps/api`'s `errorLogger` middleware,
* which sees every error — including this exact "unrecognized" case —
* before it ever reaches here).
*/
private _fromUnknownError(_error: unknown): ErrorHandlingResult {
return {
status: 500,
body: { code: ErrorCode.INTERNAL_ERROR, message: "Internal server error" },
};
}
}
/** Single shared instance — this service is stateless, no need for one per request. */
export const errorHandlerService = new ErrorHandlerService();