UNPKG

laravel-precognition

Version:
194 lines (193 loc) 6.81 kB
import { HttpResponseError, HttpCancelledError, HttpNetworkError } from './errors.js'; import { buildUrl } from './url.js'; import { hasFiles } from '../form.js'; /** * Get the X-Requested-With header from Laravel's bootstrap config if available. */ function getAjaxHeader() { if (typeof window === 'undefined') { return null; } return window.axios?.defaults?.headers?.common?.['X-Requested-With'] ?? null; } /** * Convert data to FormData recursively. */ function toFormData(data, formData = new FormData(), parentKey = null) { for (const key in data) { if (Object.prototype.hasOwnProperty.call(data, key)) { appendToFormData(formData, parentKey ? `${parentKey}[${key}]` : key, data[key]); } } return formData; } function appendToFormData(formData, key, value) { if (Array.isArray(value)) { return value.forEach((val, index) => appendToFormData(formData, `${key}[${index}]`, val)); } else if (value instanceof Date) { return formData.append(key, value.toISOString()); } else if (typeof File !== 'undefined' && value instanceof File) { return formData.append(key, value, value.name); } else if (value instanceof Blob) { return formData.append(key, value); } else if (typeof value === 'boolean') { return formData.append(key, value ? '1' : '0'); } else if (typeof value === 'string') { return formData.append(key, value); } else if (typeof value === 'number') { return formData.append(key, `${value}`); } else if (value === null || value === undefined) { return formData.append(key, ''); } toFormData(value, formData, key); } /** * Prepare the request body. */ function prepareBody(data, headers) { if (data === undefined || data === null) { return undefined; } if (data instanceof FormData) { return data; } if (typeof data === 'object' && hasFiles(data)) { return toFormData(data); } if (typeof data === 'object' || headers['Content-Type']?.includes('application/json')) { return JSON.stringify(data); } return String(data); } /** * Parse response headers into a plain object. */ function parseHeaders(headers) { const result = {}; headers.forEach((value, key) => { result[key.toLowerCase()] = value; }); return result; } /** * Create a fetch-based HTTP client. */ export function createFetchClient(options = {}) { let xsrfCookieName = options.xsrfCookieName ?? 'XSRF-TOKEN'; let xsrfHeaderName = options.xsrfHeaderName ?? 'X-XSRF-TOKEN'; function getXsrfToken() { if (typeof document === 'undefined') { return null; } const match = document.cookie.match(new RegExp('(^|;\\s*)' + xsrfCookieName + '=([^;]*)')); return match ? decodeURIComponent(match[2]) : null; } return { setXsrfCookieName(name) { xsrfCookieName = name; }, setXsrfHeaderName(name) { xsrfHeaderName = name; }, async request(config) { const url = buildUrl(config.url, config.baseURL, config.params); const method = config.method.toUpperCase(); const headers = {}; // Inherit X-Requested-With from Laravel's bootstrap config if available const ajaxHeader = getAjaxHeader(); if (ajaxHeader) { headers['X-Requested-With'] = ajaxHeader; } // Set default Content-Type for non-GET/DELETE requests with data if (config.data !== undefined && !['GET', 'DELETE'].includes(method)) { if (!(config.data instanceof FormData) && !hasFiles(config.data)) { headers['Content-Type'] = 'application/json'; } } // Copy user headers if (config.headers) { Object.entries(config.headers).forEach(([key, value]) => { if (value !== undefined) { headers[key] = String(value); } }); } // Add XSRF token const xsrfToken = getXsrfToken(); if (xsrfToken && !['GET', 'HEAD', 'OPTIONS'].includes(method)) { headers[xsrfHeaderName] = xsrfToken; } // Handle timeout let signal = config.signal; let timeoutId; const timeout = config.timeout ?? 30000; if (timeout > 0 && !signal) { const controller = new AbortController(); signal = controller.signal; timeoutId = setTimeout(() => controller.abort(), timeout); } // Prepare body (only for non-GET/DELETE requests) const body = ['GET', 'DELETE'].includes(method) ? undefined : prepareBody(config.data, headers); if (body instanceof FormData) { delete headers['Content-Type']; } try { const response = await fetch(url, { method, headers, body, signal, credentials: config.credentials ?? 'same-origin', }); if (timeoutId) { clearTimeout(timeoutId); } let data; const contentType = response.headers.get('content-type'); if (contentType?.includes('application/json')) { data = await response.json(); } else { data = await response.text(); } const httpResponse = { status: response.status, data, headers: parseHeaders(response.headers), }; if (!response.ok) { throw new HttpResponseError(httpResponse); } return httpResponse; } catch (error) { if (timeoutId) { clearTimeout(timeoutId); } if (error instanceof HttpResponseError) { throw error; } if (error instanceof DOMException && error.name === 'AbortError') { throw new HttpCancelledError(); } if (error instanceof TypeError) { throw new HttpNetworkError(error.message); } throw error; } }, }; } /** * Default fetch HTTP client instance. */ export const fetchHttpClient = createFetchClient();