UNPKG

setu.js

Version:

A lightweight HTTP client for Node.js and the browser, with smart adapter selection.

172 lines 6.61 kB
import { defaults } from './defaults.js'; export function browserAdapter(url, config = {}) { const makeRequest = (attempt) => { return new Promise((resolve, reject) => { const method = (config.method || 'GET').toUpperCase(); const isGetLike = ['GET', 'HEAD'].includes(method); // Merge query params const [baseUrl, existingQuery] = url.split('?'); const combinedParams = new URLSearchParams(existingQuery || ''); if (config.params) { Object.entries(config.params).forEach(([key, value]) => { combinedParams.set(key, value); }); } const finalUrl = (baseUrl.startsWith('http') ? baseUrl : defaults.baseURL + baseUrl) + (combinedParams.toString() ? '?' + combinedParams.toString() : ''); const xhr = new XMLHttpRequest(); xhr.open(method, finalUrl, true); if (config.timeout) xhr.timeout = config.timeout; if (config.responseType === 'blob') xhr.responseType = 'blob'; if (config.signal) { config.signal.addEventListener('abort', () => { xhr.abort(); reject(buildError('Request aborted', config, 'ECONNABORTED', xhr)); }); } const isFormData = config.body instanceof FormData; const isJSON = typeof config.body === 'object' && !isFormData; // Set headers if (config.headers) { for (const key in config.headers) { xhr.setRequestHeader(key, config.headers[key]); } } if (!isFormData && isJSON && !config.headers?.['Content-Type']) { xhr.setRequestHeader('Content-Type', 'application/json'); } // Upload progress if (xhr.upload && config.onUploadProgress) { xhr.upload.onprogress = (event) => { const percent = event.lengthComputable ? (event.loaded / event.total) * 100 : 0; config.onUploadProgress?.({ loaded: event.loaded, total: event.lengthComputable ? event.total : undefined, percent, }); }; } // Download progress if (config.onDownloadProgress) { xhr.onprogress = (event) => { if (event.lengthComputable) { config.onDownloadProgress?.({ loaded: event.loaded, total: event.total, percent: (event.loaded / event.total) * 100, }); } }; } // Success xhr.onload = () => { const headers = parseHeaders(xhr.getAllResponseHeaders()); const contentType = xhr.getResponseHeader('Content-Type') || ''; const status = xhr.status; const response = { status, headers, data: parseResponse(xhr, contentType), ...(safeExtractFilename(xhr) ? { filename: safeExtractFilename(xhr) } : {}), }; const validateStatus = config.validateStatus || ((status) => status >= 200 && status < 300); if (validateStatus(status)) { resolve(response); } else { reject(buildError(`Request failed with status code ${status}`, config, null, xhr, response, status)); } }; // Network error xhr.onerror = () => { if (attempt < (config.retries || 0)) { return retryRequest(); } reject(buildError('Network error', config, 'ERR_NETWORK', xhr)); }; // Timeout xhr.ontimeout = () => { if (attempt < (config.retries || 0)) { return retryRequest(); } reject(buildError(`Timeout of ${xhr.timeout}ms exceeded`, config, 'ECONNABORTED', xhr)); }; // Send body if (isGetLike) { xhr.send(); } else if (isFormData) { xhr.send(config.body); } else if (isJSON && config.body) { xhr.send(JSON.stringify(config.body)); } else { xhr.send(); } function retryRequest() { setTimeout(() => { makeRequest(attempt + 1).then(resolve).catch(reject); }, config.retryDelay || 500); } }); }; return makeRequest(0); } // ---------------------- // ✅ Utilities // ---------------------- function buildError(message, config, code, request, response, status) { const error = new Error(message); error.config = config; error.code = code; error.request = request; if (response) error.response = response; if (status) error.status = status; return error; } function parseHeaders(headerStr) { const headers = {}; headerStr.trim().split(/[\r\n]+/).forEach(line => { const [key, ...rest] = line.split(': '); if (key) headers[key.toLowerCase()] = rest.join(': '); }); return headers; } function parseResponse(xhr, contentType) { if (xhr.responseType === 'blob' || contentType.includes('application/octet-stream')) { return xhr.response; } else if (contentType.includes('application/json')) { try { return JSON.parse(xhr.responseText); } catch { throw new Error('Failed to parse JSON response'); } } else { return xhr.responseText; } } function safeExtractFilename(xhr) { try { const disposition = xhr.getResponseHeader('Content-Disposition'); if (!disposition) return undefined; const match = /filename[^;=\n]*=(['"]?)([^'"\n]*)\1/.exec(disposition); return match ? decodeURIComponent(match[2]) : undefined; } catch { // Access will throw in browser unless the server exposes this header return undefined; } } //# sourceMappingURL=browserAdapter.js.map