NeahStable/lib/utils/fetch-with-timeout.ts
2026-01-16 18:22:20 +01:00

95 lines
2.4 KiB
TypeScript

/**
* 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<Response>
* @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<Response> {
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<T> - Parsed JSON response
*
* @example
* ```typescript
* const data = await fetchJsonWithTimeout('https://api.example.com/data', {
* method: 'GET',
* timeout: 10000,
* });
* ```
*/
export async function fetchJsonWithTimeout<T = any>(
url: string,
options: FetchWithTimeoutOptions = {}
): Promise<T> {
const response = await fetchWithTimeout(url, options);
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(
`HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}`
);
}
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
throw new Error(`Expected JSON response, got ${contentType}`);
}
return response.json();
}