83 lines
2.8 KiB
JavaScript
83 lines
2.8 KiB
JavaScript
// 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})`;
|
|
}
|