73 lines
2.3 KiB
TypeScript
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",
|
|
};
|
|
}
|