2026-09-22 12:33:19 -04:00
|
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
|
import type { Context } from "hono";
|
|
|
|
|
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
|
|
|
|
|
|
|
|
|
export type ErrorCode =
|
|
|
|
|
| "UNAUTHENTICATED"
|
|
|
|
|
| "FORBIDDEN"
|
|
|
|
|
| "NOT_FOUND"
|
|
|
|
|
| "VALIDATION_ERROR"
|
|
|
|
|
| "CONFLICT"
|
|
|
|
|
| "INTERNAL_ERROR"
|
|
|
|
|
| "DATABASE_UNAVAILABLE";
|
|
|
|
|
|
2026-09-25 16:37:06 -04:00
|
|
|
export class ApiError extends Error {
|
|
|
|
|
constructor(
|
|
|
|
|
readonly status: ContentfulStatusCode,
|
|
|
|
|
readonly code: ErrorCode,
|
|
|
|
|
message: string,
|
|
|
|
|
) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = "ApiError";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 12:33:19 -04:00
|
|
|
export const CORRELATION_HEADER = "x-correlation-id";
|
|
|
|
|
|
|
|
|
|
export type ErrorEnvelope = {
|
|
|
|
|
error: {
|
|
|
|
|
code: ErrorCode;
|
|
|
|
|
message: string;
|
|
|
|
|
correlationId: string;
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function correlationIdFrom(c: Context): string {
|
|
|
|
|
const raw = c.req.header(CORRELATION_HEADER)?.trim();
|
|
|
|
|
if (raw && raw.length <= 128) {
|
|
|
|
|
return raw;
|
|
|
|
|
}
|
|
|
|
|
return randomUUID();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function errorBody(code: ErrorCode, message: string, correlationId: string): ErrorEnvelope {
|
|
|
|
|
return { error: { code, message, correlationId } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function errorJson(
|
|
|
|
|
c: Context,
|
|
|
|
|
status: ContentfulStatusCode,
|
|
|
|
|
code: ErrorCode,
|
|
|
|
|
message: string,
|
|
|
|
|
correlationId?: string,
|
|
|
|
|
) {
|
|
|
|
|
const id = correlationId ?? correlationIdFrom(c);
|
|
|
|
|
c.header(CORRELATION_HEADER, id);
|
|
|
|
|
return c.json(errorBody(code, message, id), status);
|
|
|
|
|
}
|