/** * Lightweight fetch wrapper that: * - Adds an X-Correlation-ID header to every request * - Exposes helpers to parse the standard error envelope * - Provides helpers to build user-facing error messages with correlation ID */ (function () { // Ensure global app object exists window.app = window.app || {}; const CORRELATION_HEADER = 'X-Correlation-ID'; function generateCorrelationId() { try { if (window.crypto && typeof window.crypto.randomUUID === 'function') { return window.crypto.randomUUID(); } } catch (_) {} // Fallback RFC4122-ish UUID const s4 = () => Math.floor((1 + Math.random()) * 0x10000).toString(16).slice(1); return ( s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4() ); } function normalizeHeaders(input) { if (!input) return new Headers(); return input instanceof Headers ? input : new Headers(input); } const originalFetch = window.fetch.bind(window); async function wrappedFetch(resource, options = {}) { const url = typeof resource === 'string' ? resource : (resource && resource.url) || ''; const headers = normalizeHeaders(options.headers); // Inject correlation id if not present let outgoingCid = headers.get(CORRELATION_HEADER); if (!outgoingCid) { outgoingCid = generateCorrelationId(); headers.set(CORRELATION_HEADER, outgoingCid); } // Inject Authorization header if JWT token is available and not already provided try { const storedToken = (window.app && window.app.token) || (typeof localStorage !== 'undefined' && localStorage.getItem('auth_token')); if (storedToken && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${storedToken}`); } } catch (_) { // Ignore storage access errors (e.g., privacy mode) } const requestInit = { ...options, headers }; const response = await originalFetch(resource, requestInit); try { const incomingCid = (response && response.headers && response.headers.get(CORRELATION_HEADER)) || null; // Track last seen correlation id for diagnostics/UI messaging window.app.lastCorrelationId = incomingCid || outgoingCid || null; } catch (_) {} return response; } /** * Parse the standardized error envelope from a failed response. * Returns a normalized object with message, correlationId, code, details. */ async function parseErrorEnvelope(response) { let body = null; try { body = await response.clone().json(); } catch (_) { // not JSON; try text as last resort try { const txt = await response.clone().text(); body = { detail: txt }; } catch (_) { body = null; } } const headerCid = response && response.headers ? response.headers.get(CORRELATION_HEADER) : null; const envelopeCid = body && (body.correlation_id || (body.error && body.error.correlation_id)); const correlationId = headerCid || envelopeCid || window.app.lastCorrelationId || null; const message = body && body.error && body.error.message ? String(body.error.message) : body && typeof body.detail === 'string' && body.detail.trim() ? String(body.detail) : `HTTP ${response.status}`; const code = body && body.error ? (body.error.code || null) : null; const details = body && body.error ? (body.error.details || null) : null; return { message, correlationId, code, details, raw: body }; } /** * Create an Error populated from a failed Response and optional fallback message. */ async function toError(response, fallbackMessage = null) { const parsed = await parseErrorEnvelope(response); const msg = parsed && parsed.message ? parsed.message : (fallbackMessage || `HTTP ${response.status}`); const err = new Error(msg); err.status = response.status; err.correlationId = parsed.correlationId || null; if (parsed && parsed.code) err.code = parsed.code; if (parsed && parsed.details) err.details = parsed.details; return err; } /** * Format a message suitable for UI alerts with correlation id reference. * Example: formatAlert(err, 'Error saving customer') */ function formatAlert(errorOrMessage, prefix = null) { const isError = errorOrMessage instanceof Error; const message = isError ? errorOrMessage.message : String(errorOrMessage || ''); const correlationId = isError && errorOrMessage.correlationId ? errorOrMessage.correlationId : (window.app && window.app.lastCorrelationId) || null; const base = prefix ? `${prefix}: ${message}` : message; return correlationId ? `${base} (Ref: ${correlationId})` : base; } // Expose helpers window.http = { parseErrorEnvelope, toError, formatAlert, }; // Install wrapper window.fetch = wrappedFetch; })();