/** * One error envelope for every failure, because a client parses failures the same way it parses * successes. A route that answers with a bare string does not return the wrong shape — it breaks * the parse. `error` is always an object. */ export const ERROR_STATUS = { invalid_request: 400, unauthorized: 401, not_found: 404, payload_too_large: 413, unprocessable: 422, timeout: 504, server_error: 500, } as const; export type ErrorType = keyof typeof ERROR_STATUS; export class ApiError extends Error { constructor( readonly type: ErrorType, message: string, /** Dotted path to the offending field, e.g. `pipeline[1].resize.width`. */ readonly param?: string, ) { super(message); this.name = "ApiError"; } toResponse(): Response { return Response.json( { error: { message: this.message, type: this.type, param: this.param ?? null } }, { status: ERROR_STATUS[this.type] }, ); } } export const invalid = (message: string, param?: string) => new ApiError("invalid_request", message, param); export const notFound = (message = "No such asset") => new ApiError("not_found", message); export const unprocessable = (message: string, param?: string) => new ApiError("unprocessable", message, param); /** * Turn anything thrown into a response. * * An unrecognised error is logged and answered with a generic 500 — the message may name a path or * quote file contents, and neither belongs in a response body. */ export function toErrorResponse(error: unknown): Response { if (error instanceof ApiError) return error.toResponse(); console.error("[av-club] unhandled:", error); return Response.json( { error: { message: "Internal Server Error", type: "server_error", param: null } }, { status: 500 }, ); }