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, };