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>
80 lines
3 KiB
TypeScript
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();
|