// --------------------------------------------------------------------------- // Ordner: lib/api // Datei: errors.js // Relativer Pfad: lib/api/errors.js // --------------------------------------------------------------------------- /** * Central API response helpers for consistent JSON output and standardized errors. * * Why this exists: * - Route handlers should NOT each invent their own error JSON shape. * - The frontend (and tests/logging) should rely on a stable contract: * { error: { message, code, details? } } * - We want consistent HTTP status codes (400/401/403/404/500). * * This file intentionally uses the Web Fetch `Response` object (not NextResponse), * because Next.js route handlers can return any `Response` and it keeps tests simple. */ /** * Default JSON response headers for all API responses that return JSON. * * RHL-006 (Caching / Freshness): * - We explicitly disable HTTP caching for ALL JSON API responses by default. * - Reason #1: NAS filesystem contents can change at any time (new scans appear). * - Reason #2: Auth/RBAC-protected responses must never be cached/shared by accident * (e.g. via browser cache, proxies, reverse proxies, or intermediate gateways). * * If a future endpoint truly needs caching, it should override headers explicitly * at the call site via `json(..., status, { "Cache-Control": "..." })`. */ const JSON_HEADERS = { "Content-Type": "application/json", "Cache-Control": "no-store", }; /** * Create a JSON `Response` with the given payload. * * @param {any} data - The JSON-serializable payload. * @param {number} status - HTTP status code. * @param {Record} headers - Optional extra headers. * @returns {Response} */ export function json(data, status = 200, headers = {}) { return new Response(JSON.stringify(data), { status, headers: { ...JSON_HEADERS, ...headers }, }); } /** * Build the standardized error payload. * * Contract: * { * "error": { * "message": "Human readable message", * "code": "SOME_MACHINE_CODE", * "details": { ... } // optional * } * } * * IMPORTANT: * - message is for humans (UI/logs). * - code is for machines (frontend decisions, monitoring, tests). * - details is optional and should only include safe data. * * @param {{code:string, message:string, details?:any}} input * @returns {{error:{message:string, code:string, details?:any}}} */ export function buildErrorPayload({ code, message, details }) { const error = { message, code }; // Only include details when explicitly provided. // This keeps the error payload minimal and predictable. if (details !== undefined) error.details = details; return { error }; } /** * Create a standardized JSON error response. * * @param {number} status - HTTP status code (400/401/403/404/500). * @param {string} code - Stable machine-readable error code. * @param {string} message - Human-readable message (safe to expose). * @param {any=} details - Optional structured details (validation fields, params, etc.). * @returns {Response} */ export function jsonError(status, code, message, details) { return json(buildErrorPayload({ code, message, details }), status); } /** * Custom error type for “expected” API failures. * * We throw ApiError from route handlers instead of returning ad-hoc JSON. * The `withErrorHandling` wrapper converts it into a standardized response. */ export class ApiError extends Error { /** * @param {{ * status: number, * code: string, * message: string, * details?: any, * cause?: any * }} input */ constructor({ status, code, message, details, cause }) { // Modern Node supports `cause`, which preserves the original error chain. super(message, cause ? { cause } : undefined); this.name = "ApiError"; this.status = status; this.code = code; // Only attach details if explicitly provided. if (details !== undefined) this.details = details; } } /** * Convenience constructors for the common HTTP statuses used by the API. * This prevents magic numbers and keeps code readable in route handlers. */ export function badRequest(code, message, details) { return new ApiError({ status: 400, code, message, details }); } export function unauthorized(code, message, details) { return new ApiError({ status: 401, code, message, details }); } export function forbidden(code, message, details) { return new ApiError({ status: 403, code, message, details }); } export function notFound(code, message, details) { return new ApiError({ status: 404, code, message, details }); } /** * Convert a thrown error into a standardized API response. * * Policy: * - If it's an ApiError, we trust its status/code/message (they should be safe). * - Otherwise, we return a generic 500 without leaking internals. * * @param {unknown} err * @returns {Response} */ export function toApiErrorResponse(err) { if (err instanceof ApiError) { return jsonError(err.status, err.code, err.message, err.details); } // Never leak unexpected error details to clients. return jsonError(500, "INTERNAL_SERVER_ERROR", "Internal server error"); } /** * Wrap a Next.js route handler to enforce consistent error handling. * * Usage: * export const GET = withErrorHandling(async function GET(request, ctx) { ... }) * * Benefits: * - No repeated try/catch blocks in every route. * - All thrown ApiErrors become standardized JSON responses. * - Unknown errors become a safe, generic 500 response. * * @param {Function} handler * @param {{logPrefix?:string}=} options * @returns {Function} */ export function withErrorHandling(handler, { logPrefix = "[api]" } = {}) { return async (...args) => { try { return await handler(...args); } catch (err) { // Log unexpected errors always. // For ApiErrors, only log server-side failures (5xx) by default // to avoid noisy logs for client mistakes (4xx). if (err instanceof ApiError) { if (err.status >= 500) { console.error(`${logPrefix} ${err.code}:`, err.cause || err); } return toApiErrorResponse(err); } console.error(`${logPrefix} Unhandled error:`, err); return toApiErrorResponse(err); } }; } // ---------------------------------------------------------------------------