/** * 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 = {}, ): 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)); }