/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { getErrorMessage, isNodeError } from './errors.js'; import { URL } from 'node:url'; import { Agent, ProxyAgent, setGlobalDispatcher } from 'undici'; const DEFAULT_HEADERS_TIMEOUT = 60000; // 60 seconds const DEFAULT_BODY_TIMEOUT = 300000; // 5 minutes // Configure default global dispatcher with higher timeouts setGlobalDispatcher( new Agent({ headersTimeout: DEFAULT_HEADERS_TIMEOUT, bodyTimeout: DEFAULT_BODY_TIMEOUT, }), ); const PRIVATE_IP_RANGES = [ /^10\./, /^127\./, /^172\.(1[6-9]|2[0-9]|3[0-1])\./, /^192\.168\./, /^::1$/, /^fc00:/, /^fe80:/, ]; export class FetchError extends Error { constructor( message: string, public code?: string, options?: ErrorOptions, ) { super(message, options); this.name = 'FetchError'; } } export function isPrivateIp(url: string): boolean { try { const hostname = new URL(url).hostname; return PRIVATE_IP_RANGES.some((range) => range.test(hostname)); } catch (_e) { return false; } } export async function fetchWithTimeout( url: string, timeout: number, options?: RequestInit, ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); if (options?.signal) { if (options.signal.aborted) { controller.abort(); } else { options.signal.addEventListener('abort', () => controller.abort(), { once: true, }); } } try { const response = await fetch(url, { ...options, signal: controller.signal, }); return response; } catch (error) { if (isNodeError(error) && error.code === 'ABORT_ERR') { throw new FetchError(`Request timed out after ${timeout}ms`, 'ETIMEDOUT'); } throw new FetchError(getErrorMessage(error), undefined, { cause: error }); } finally { clearTimeout(timeoutId); } } export function setGlobalProxy(proxy: string) { setGlobalDispatcher( new ProxyAgent({ uri: proxy, headersTimeout: DEFAULT_HEADERS_TIMEOUT, bodyTimeout: DEFAULT_BODY_TIMEOUT, }), ); }