138 lines
3.4 KiB
JavaScript
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,
|
|
};
|