setu.js
Version:
A lightweight HTTP client for Node.js and the browser, with smart adapter selection.
172 lines • 6.5 kB
JavaScript
import http from 'http';
import https from 'https';
import { URL } from 'url';
import FormData from 'form-data';
import { defaults } from './defaults.js';
import { buildQuery, mergeHeaders } from './utils.js';
function delay(ms) {
return new Promise((res) => setTimeout(res, ms));
}
function isBinaryContentType(contentType) {
return /(application|image|audio|video|octet-stream)/i.test(contentType) &&
!/json|text|xml/i.test(contentType);
}
export async function coreRequest(url, config = {}) {
const attempts = config.retry ?? 0;
const delayMs = config.retryDelay ?? 300;
let lastError;
for (let i = 0; i <= attempts; i++) {
try {
return await makeRequest(url, config);
}
catch (err) {
lastError = err;
if (i < attempts)
await delay(delayMs);
}
}
throw lastError;
}
function makeRequest(url, config) {
return new Promise((resolve, reject) => {
const finalUrl = new URL(url.startsWith('http') ? url : defaults.baseURL + url);
if (config.params)
finalUrl.search = buildQuery(config.params);
const lib = finalUrl.protocol === 'https:' ? https : http;
const headers = mergeHeaders(defaults.headers, config.headers);
const method = config.method || 'GET';
let body = config.body;
let isForm = false;
if (body instanceof FormData) {
isForm = true;
Object.assign(headers, body.getHeaders());
}
else if (body && typeof body === 'object' && !(body instanceof Buffer)) {
body = JSON.stringify(body);
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
headers['Content-Length'] = Buffer.byteLength(body).toString();
}
const reqOptions = {
method,
headers,
timeout: config.timeout || defaults.timeout,
};
const req = lib.request(finalUrl, reqOptions, (res) => {
const total = parseInt(res.headers['content-length'] || '0');
let downloaded = 0;
if (config.responseType === 'stream') {
return resolve({
status: res.statusCode || 200,
headers: res.headers,
data: res,
});
}
const chunks = [];
res.setEncoding(null);
res.on('data', (chunk) => {
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
chunks.push(bufferChunk);
downloaded += bufferChunk.length;
if (config.onDownloadProgress && total > 0) {
config.onDownloadProgress({
loaded: downloaded,
total,
percent: (downloaded / total) * 100,
});
}
});
res.on('end', () => {
const buffer = Buffer.concat(chunks);
const contentType = res.headers['content-type'] || '';
const status = res.statusCode || 200;
let parsed;
const isBinary = config.responseType === 'blob' ||
config.responseType === 'arraybuffer' ||
isBinaryContentType(contentType);
try {
if (isBinary) {
parsed = buffer;
}
else if (contentType.includes('application/json')) {
parsed = JSON.parse(buffer.toString('utf-8'));
}
else {
parsed = buffer.toString('utf-8');
}
}
catch (err) {
return reject(buildError('Failed to parse response', config, null, req, {
status,
headers: res.headers,
data: buffer.toString('utf-8'),
}, status));
}
const response = {
status,
headers: res.headers,
data: parsed,
};
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, req, response, status));
}
});
res.on('error', (err) => {
reject(buildError('Response stream error', config, 'ERR_RESPONSE', req, null));
});
});
req.on('error', (err) => {
reject(buildError(err.message, config, 'ERR_NETWORK', req));
});
const timeout = reqOptions.timeout || defaults.timeout;
req.on('timeout', () => {
req.destroy();
reject(buildError(`Timeout of ${timeout}ms exceeded`, config, 'ECONNABORTED', req));
});
if (config.signal) {
config.signal.addEventListener('abort', () => {
req.destroy();
reject(buildError('Request aborted', config, 'ECONNABORTED', req));
});
}
// Upload body
if (isForm) {
let uploaded = 0;
body.on('data', (chunk) => {
uploaded += chunk.length;
config.onUploadProgress?.({
loaded: uploaded,
total: undefined,
percent: uploaded / (body.getLengthSync() || 1) * 100,
});
});
body.on('error', (err) => {
reject(buildError(err.message, config, 'ERR_UPLOAD_STREAM', req));
});
body.pipe(req);
}
else if (body) {
req.write(body);
req.end();
}
else {
req.end();
}
});
}
// ✅ Structured error generator
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;
}
//# sourceMappingURL=nodeAdapter.js.map