122 lines
3.9 KiB
TypeScript
122 lines
3.9 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);
|
|
|
|
// 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('<!DOCTYPE') || errorText.trim().startsWith('<html')) {
|
|
throw new Error(`HTTP ${response.status} ${response.statusText}: Server returned HTML page instead of JSON. This usually means authentication failed or URL is incorrect. Preview: ${errorText.substring(0, 300)}`);
|
|
}
|
|
|
|
throw new Error(
|
|
`HTTP ${response.status} ${response.statusText}: ${errorText.substring(0, 200)}`
|
|
);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
|
|
// Check if response is JSON
|
|
if (!contentType.includes('application/json')) {
|
|
// Read the response text to see what we got
|
|
const responseText = await clonedResponse.text().catch(() => 'Unable to read response');
|
|
const preview = responseText.substring(0, 500);
|
|
|
|
// If it looks like HTML, provide a more helpful error
|
|
if (responseText.trim().startsWith('<!DOCTYPE') || responseText.trim().startsWith('<html')) {
|
|
throw new Error(`Expected JSON response, got text/html. Server may be returning an error page or login page. URL: ${url}. Preview: ${preview}`);
|
|
}
|
|
|
|
throw new Error(`Expected JSON response, got ${contentType || 'unknown'}. URL: ${url}. Preview: ${preview}`);
|
|
}
|
|
|
|
try {
|
|
return await response.json();
|
|
} catch (jsonError) {
|
|
// If JSON parsing fails, try to read the text to see what we got
|
|
const responseText = await clonedResponse.text().catch(() => 'Unable to read response');
|
|
throw new Error(`Failed to parse JSON response from ${url}. Content: ${responseText.substring(0, 500)}`);
|
|
}
|
|
}
|