Duriin-API/src/http.js

138 lines
3.4 KiB
JavaScript

const DEFAULT_HEADERS = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
'Upgrade-Insecure-Requests': '1',
'sec-ch-ua': '"Google Chrome";v="135", "Chromium";v="135", "Not.A/Brand";v="24"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"macOS"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
};
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isRetryableStatus(status) {
return status === 429 || status >= 500;
}
function getRetryDelay(attempt, response) {
const retryAfter = response && response.headers ? response.headers.get('retry-after') : null;
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds) && seconds >= 0) {
return Math.min(seconds * 1000, 30000);
}
const retryDate = new Date(retryAfter).getTime();
if (!Number.isNaN(retryDate)) {
return Math.max(Math.min(retryDate - Date.now(), 30000), 0);
}
}
const baseDelay = Math.min(1000 * (2 ** attempt), 10000);
return baseDelay + Math.floor(Math.random() * 250);
}
function getErrorCode(error) {
return String(error?.code || error?.cause?.code || '').trim();
}
function isRetryableError(error) {
const code = getErrorCode(error);
const message = String(error?.message || '').toLowerCase();
if (code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
return false;
}
if (error?.name === 'TimeoutError') {
return true;
}
return [
'UND_ERR_SOCKET',
'UND_ERR_CONNECT_TIMEOUT',
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'EAI_AGAIN',
'ENETUNREACH',
].includes(code) || message.includes('other side closed');
}
async function fetchWithPolicy(url, options = {}) {
const {
timeout = 20000,
retries = 2,
headers = {},
...fetchOptions
} = options;
let lastError;
for (let attempt = 0; attempt <= retries; attempt += 1) {
let response;
try {
response = await fetch(url, {
...fetchOptions,
signal: fetchOptions.signal || AbortSignal.timeout(timeout),
headers: {
...DEFAULT_HEADERS,
...headers,
},
});
if (response.ok || !isRetryableStatus(response.status)) {
return response;
}
const error = new Error(`Request failed with ${response.status} for ${url}`);
error.status = response.status;
lastError = error;
} catch (error) {
lastError = error;
if (!isRetryableError(error)) {
throw error;
}
}
if (attempt < retries) {
await sleep(getRetryDelay(attempt, response));
}
}
throw lastError;
}
async function fetchJson(url, options = {}) {
const response = await fetchWithPolicy(url, {
...options,
headers: {
Accept: 'application/json',
...(options.headers || {}),
},
});
if (!response.ok) {
const error = new Error(`Request failed with ${response.status} for ${url}`);
error.status = response.status;
throw error;
}
return response.json();
}
module.exports = {
fetchJson,
fetchWithPolicy,
};