/** * Combo API error helper (T-22.b). * * Standardizes the 400/404/409 response shape for `/api/combos/*` routes. * Every failure surfaces a stable machine-readable `code` token (from * `src/shared/constants/errorCodes.ts`), a human-readable `message`, an * optional `details` payload, and the current `requestId` for log * correlation. The prior shape returned `{ error: }` with * no `code` field, which forced clients to string-match English error * messages. See `plans/2026-06-23-omniroute-v3.8.34-deep-audit.md` (Bug #3). * * Usage: * return comboErrorResponse("COMBO_002", 400, { issues: validation.issues }); * return comboErrorResponse("COMBO_005", 400, { reason: "cycle-detected" }); * * The response is a plain `Response` (not `NextResponse.json`) so it can * be used from both Next.js route handlers and server-side callers. The * `x-request-id` header is also attached for downstream log correlation. */ import { ERROR_CODES } from "@/shared/constants/errorCodes"; import { attachRequestIdToResponse, getRequestId, } from "@/shared/utils/requestId"; export type ComboErrorCode = | "COMBO_001" // request body is not valid JSON | "COMBO_002" // zod schema failure | "COMBO_003" // composite tier config invalid | "COMBO_004" // name collision | "COMBO_005" // DAG cycle / depth overflow | "COMBO_006" // managed by Quota Share (409) | "COMBO_007" // not found (404) | "VALID_001" // generic invalid body | "VALID_002" // missing required field | "INTERNAL_001"; // fallback export interface ComboErrorBody { error: { code: ComboErrorCode; message: string; category: string; details?: unknown; requestId?: string; }; } export function buildComboErrorBody( code: ComboErrorCode, details?: unknown ): ComboErrorBody { const def = ERROR_CODES[code] ?? ERROR_CODES.INTERNAL_001; const requestId = getRequestId(); return { error: { code: def.code as ComboErrorCode, message: def.message, category: def.category, ...(details !== undefined ? { details } : {}), ...(requestId ? { requestId } : {}), }, }; } export function comboErrorResponse( code: ComboErrorCode, status?: number, details?: unknown, request?: Request ): Response { const def = ERROR_CODES[code] ?? ERROR_CODES.INTERNAL_001; const httpStatus = status ?? def.httpStatus; const body = buildComboErrorBody(code, details); const response = Response.json(body, { status: httpStatus }); // Attach x-request-id header for downstream consumers. If a request is // passed, prefer to derive its id (works outside withRequestId scope); // otherwise the response body already carries requestId when available. return request ? attachRequestIdToResponse(request, response) : response; }