Files
suaveplan/packages/api-stack/api/api-plugin-export/src/routes/problem-json/problem-json.ts
2026-05-27 23:29:53 +01:00

73 lines
2.3 KiB
TypeScript

import { SuaveplanError } from "@suaveplan/error";
/** Standard RFC 7807 content-type. */
export const PROBLEM_JSON_CONTENT_TYPE = "application/problem+json";
/**
* Minimal structural Hono context surface relied on by
* {@link respondProblemJson}. Declared as an interface so the renderer
* does not couple to a specific Hono version.
*/
export interface HonoLike {
header(name: string, value: string): void;
body(data: string, status: number): Response;
}
/** RFC 7807 problem-details body. */
export interface ProblemDetails {
readonly type: string;
readonly title: string;
readonly status: number;
readonly detail: string;
readonly code?: string;
readonly [key: string]: unknown;
}
/**
* Convert an arbitrary error into an RFC 7807 problem-details body and
* write it to the supplied Hono context. `SuaveplanError` subclasses
* surface their `code` and read `context.statusCode` for the HTTP code;
* plain `Error` and unknown throwables fall through to a generic
* `500 Internal Server Error`.
*/
export function respondProblemJson(c: HonoLike, err: unknown): Response {
const body = renderProblem(err);
c.header("content-type", PROBLEM_JSON_CONTENT_TYPE);
return c.body(JSON.stringify(body), body.status);
}
/**
* Pure render — kept separate from {@link respondProblemJson} so the
* shape can be asserted in tests that do not construct a Hono context.
*/
export function renderProblem(err: unknown): ProblemDetails {
if (err instanceof SuaveplanError) {
const ctx = err.context as { statusCode?: number } | undefined;
const status =
typeof ctx?.statusCode === "number" && ctx.statusCode >= 400
? ctx.statusCode
: 500;
return {
type: `urn:suaveplan:error:${err.code.toLowerCase()}`,
title: err.name,
status,
detail: err.message,
code: err.code,
};
}
if (err instanceof Error) {
return {
type: "urn:suaveplan:error:internal",
title: err.name || "InternalError",
status: 500,
detail: err.message || "Internal Server Error",
};
}
return {
type: "urn:suaveplan:error:internal",
title: "InternalError",
status: 500,
detail: "Internal Server Error",
};
}