From be155ca7373f00aaf196e190e8f07216b7c2ce9c Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 7 May 2026 16:40:20 +0200 Subject: [PATCH] fix(cards-server): error classes extend Hono HTTPException MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared-hono's serviceErrorHandler only translates HTTPException instances; anything else degrades to 500. Our custom Error subclasses were silently bypassing the translation layer, so a missing JWT came back as `500 Internal server error` instead of the expected `401 Unauthorized`. Confirmed in prod logs after the Phase-β deploy. Switching the error hierarchy to extend HTTPException directly. The JSON body now carries the right status code + the existing `cause` object surfaces our `code` discriminator + zod-style `details` for BadRequest. No call-site changes needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- services/cards-server/src/lib/errors.ts | 66 +++++++++++++++---------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/services/cards-server/src/lib/errors.ts b/services/cards-server/src/lib/errors.ts index cb4a26aee..f013b358e 100644 --- a/services/cards-server/src/lib/errors.ts +++ b/services/cards-server/src/lib/errors.ts @@ -1,51 +1,63 @@ /** - * Domain errors — caught by serviceErrorHandler from @mana/shared-hono - * and translated to JSON responses with the right status code. + * Domain errors — caught by `serviceErrorHandler` from @mana/shared-hono. + * + * The shared handler only translates Hono `HTTPException`s; anything + * else degrades to 500. So our errors extend HTTPException directly + * rather than maintaining a parallel hierarchy. + * + * `details` (e.g. zod issue tree) is passed via `cause` because the + * shared handler picks that up and surfaces it in the JSON body. */ -export class HttpError extends Error { - constructor( - public status: number, - message: string, - public code?: string, - public details?: unknown - ) { - super(message); - this.name = 'HttpError'; - } +import { HTTPException } from 'hono/http-exception'; +import type { ContentfulStatusCode } from 'hono/utils/http-status'; + +function makeException( + status: ContentfulStatusCode, + message: string, + code?: string, + details?: unknown +) { + return new HTTPException(status, { + message, + cause: details ? { code, details } : code ? { code } : undefined, + }); } -export class UnauthorizedError extends HttpError { +export class HttpError extends HTTPException {} + +export class UnauthorizedError extends HTTPException { constructor(message = 'Unauthorized') { - super(401, message, 'unauthorized'); - this.name = 'UnauthorizedError'; + super(401, { message, cause: { code: 'unauthorized' } }); } } -export class ForbiddenError extends HttpError { +export class ForbiddenError extends HTTPException { constructor(message = 'Forbidden') { - super(403, message, 'forbidden'); - this.name = 'ForbiddenError'; + super(403, { message, cause: { code: 'forbidden' } }); } } -export class NotFoundError extends HttpError { +export class NotFoundError extends HTTPException { constructor(message = 'Not found') { - super(404, message, 'not_found'); - this.name = 'NotFoundError'; + super(404, { message, cause: { code: 'not_found' } }); } } -export class ConflictError extends HttpError { +export class ConflictError extends HTTPException { constructor(message = 'Conflict') { - super(409, message, 'conflict'); - this.name = 'ConflictError'; + super(409, { message, cause: { code: 'conflict' } }); } } -export class BadRequestError extends HttpError { +export class BadRequestError extends HTTPException { constructor(message = 'Bad request', details?: unknown) { - super(400, message, 'bad_request', details); - this.name = 'BadRequestError'; + super(400, { + message, + cause: details ? { code: 'bad_request', details } : { code: 'bad_request' }, + }); } } + +// Keep makeException exported in case future code wants the raw factory. +export { makeException };