File size: 2,809 Bytes
cd8bd0a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
 * 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: <string|object> }` 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;
}