142 lines
4.8 KiB
JavaScript
142 lines
4.8 KiB
JavaScript
/**
|
|
* 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;
|
|
})();
|
|
|
|
|