// src/auth/apiFetch.js // // A tiny wrapper around window.fetch. // - NEVER sets Authorization (shim does that). // - Smart JSON handling (auto stringify, auto parse in helpers). // - Optional timeout via AbortController. // - Optional shim bypass: { bypassAuth: true } adds X-Bypass-Auth: 1. // - Leaves error semantics up to caller or helper. // // Use: // const res = await apiFetch('/api/user-profile'); // const json = await res.json(); // // Or helpers: // const data = await apiGetJSON('/api/user-profile'); // const out = await apiPostJSON('/api/premium/thing', { foo: 'bar' }); const DEFAULT_TIMEOUT_MS = 25000; // 25s export async function apiFetch(url, options = {}) { const headers = new Headers(options.headers || {}); const init = { ...options, credentials: options.credentials || 'include' }; // If body is a plain object and no Content-Type set, send JSON if (!headers.has('Content-Type') && options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) { headers.set('Content-Type', 'application/json'); init.body = JSON.stringify(options.body); } if (headers.size) init.headers = headers; // This must always return a Response, never null return fetch(url, init); } export async function apiGetJSON(url) { const res = await apiFetch(url); if (!res.ok) throw new Error(`GET ${url} failed: ${res.status}`); return res.json(); } export async function apiPostJSON(url, payload) { const res = await apiFetch(url, { method: 'POST', body: payload }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); const msg = errBody?.error || `POST ${url} failed: ${res.status}`; throw new Error(msg); } return res.json().catch(() => ({})); } /** PUT JSON → parse JSON, throw on !ok. */ export async function apiPutJSON(url, payload, opts = {}) { const res = await apiFetch(url, { ...opts, method: 'PUT', body: payload }); const text = await res.text(); const data = safeJSON(text); if (!res.ok) throw new Error(errorFromServer(data, text, res.status)); return data; } /** DELETE → parse JSON (if any), throw on !ok. */ export async function apiDeleteJSON(url, opts = {}) { const res = await apiFetch(url, { ...opts, method: 'DELETE' }); const text = await res.text(); const data = safeJSON(text); if (!res.ok) throw new Error(errorFromServer(data, text, res.status)); return data || { ok: true }; } /* -------------------- utils -------------------- */ function safeJSON(text) { if (!text) return null; try { return JSON.parse(text); } catch { return null; } } function errorFromServer(json, text, status) { if (json && typeof json === 'object' && json.error) return json.error; if (text) return `Request failed (${status}): ${text.slice(0, 240)}`; return `Request failed (${status})`; }