38 lines
1.3 KiB
TypeScript
38 lines
1.3 KiB
TypeScript
/**
|
|
* RFC 9457 Problem Details for HTTP APIs — the governed contract
|
|
* (docs/api/worker.openapi.json) specifies `application/problem+json` for all
|
|
* error responses (400/401/404/502/503).
|
|
*
|
|
* We omit the `type` member: per RFC 9457 §3.1.1 an absent `type` is
|
|
* equivalent to "about:blank", in which case `title` mirrors the HTTP status
|
|
* phrase — which is exactly what we send.
|
|
*/
|
|
|
|
import type { FastifyReply } from 'fastify';
|
|
|
|
export const PROBLEM_CONTENT_TYPE = 'application/problem+json';
|
|
|
|
export interface ProblemDetails {
|
|
title: string;
|
|
status: number;
|
|
detail?: string;
|
|
}
|
|
|
|
export function sendProblem(
|
|
reply: FastifyReply,
|
|
status: number,
|
|
title: string,
|
|
detail?: string,
|
|
/** Extra response headers (e.g. `Retry-After` on 503). */
|
|
headers: Record<string, string> = {},
|
|
): FastifyReply {
|
|
const body: ProblemDetails = detail === undefined ? { title, status } : { title, status, detail };
|
|
// Serialize explicitly: Fastify's default JSON serializer targets
|
|
// application/json, so send a pre-serialized string under the problem type.
|
|
reply.code(status).header('content-type', `${PROBLEM_CONTENT_TYPE}; charset=utf-8`);
|
|
for (const [name, value] of Object.entries(headers)) {
|
|
reply.header(name, value);
|
|
}
|
|
return reply.send(JSON.stringify(body));
|
|
}
|