/** * Utility function for making HTTP requests with timeout * * This is a critical production utility to prevent hanging requests * that can exhaust server resources. */ export interface FetchWithTimeoutOptions extends RequestInit { timeout?: number; // Timeout in milliseconds (default: 30000) } /** * Fetch with automatic timeout handling * * @param url - The URL to fetch * @param options - Fetch options including optional timeout * @returns Promise * @throws Error if timeout is exceeded or request fails * * @example * ```typescript * const response = await fetchWithTimeout('https://api.example.com/data', { * method: 'GET', * timeout: 10000, // 10 seconds * headers: { 'Authorization': 'Bearer token' } * }); * ``` */ export async function fetchWithTimeout( url: string, options: FetchWithTimeoutOptions = {} ): Promise { const { timeout = 30000, ...fetchOptions } = options; // Create AbortController for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); }, timeout); try { const response = await fetch(url, { ...fetchOptions, signal: controller.signal, }); clearTimeout(timeoutId); return response; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms: ${url}`); } throw error; } } /** * Fetch with timeout and automatic JSON parsing * * @param url - The URL to fetch * @param options - Fetch options including optional timeout * @returns Promise - Parsed JSON response * * @example * ```typescript * const data = await fetchJsonWithTimeout('https://api.example.com/data', { * method: 'GET', * timeout: 10000, * }); * ``` */ export async function fetchJsonWithTimeout( url: string, options: FetchWithTimeoutOptions = {} ): Promise { const response = await fetchWithTimeout(url, options); // Clone response early so we can read it multiple times if needed const clonedResponse = response.clone(); if (!response.ok) { const errorText = await clonedResponse.text().catch(() => 'Unknown error'); const contentType = response.headers.get('content-type') || ''; // If it's HTML, provide more context if (contentType.includes('text/html') || errorText.trim().startsWith(' 'Unable to read response'); const preview = responseText.substring(0, 500); // If it looks like HTML, provide a more helpful error if (responseText.trim().startsWith(' 'Unable to read response'); throw new Error(`Failed to parse JSON response from ${url}. Content: ${responseText.substring(0, 500)}`); } }