Files
freno-dev/src/server/fetch-utils.ts
Michael Freno 236555e41e zod fix
2025-12-23 10:23:43 -05:00

138 lines
3.1 KiB
TypeScript

export class NetworkError extends Error {
constructor(
message: string,
public readonly cause?: unknown
) {
super(message);
this.name = "NetworkError";
}
}
export class TimeoutError extends Error {
constructor(
message: string,
public readonly timeoutMs: number
) {
super(message);
this.name = "TimeoutError";
}
}
export class APIError extends Error {
constructor(
message: string,
public readonly status: number,
public readonly statusText: string
) {
super(message);
this.name = "APIError";
}
}
interface FetchWithTimeoutOptions extends RequestInit {
timeout?: number;
}
export async function fetchWithTimeout(
url: string,
options: FetchWithTimeoutOptions = {}
): Promise<Response> {
const { timeout = 10000, ...fetchOptions } = options;
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: unknown) {
clearTimeout(timeoutId);
if (error instanceof Error) {
if (error.name === "AbortError") {
throw new TimeoutError(
`Request to ${url} timed out after ${timeout}ms`,
timeout
);
}
if (
error.message.includes("fetch failed") ||
error.message.includes("ECONNREFUSED") ||
error.message.includes("ENOTFOUND") ||
error.message.includes("ETIMEDOUT") ||
error.message.includes("UND_ERR_CONNECT_TIMEOUT") ||
error.name === "FailedToOpenSocket" ||
error.message.includes("Was there a typo")
) {
throw new NetworkError(
`Failed to connect to ${url}: ${error.message}`,
error
);
}
}
throw error;
}
}
export async function checkResponse(response: Response): Promise<Response> {
if (!response.ok) {
throw new APIError(
`API request failed: ${response.statusText}`,
response.status,
response.statusText
);
}
return response;
}
export async function safeJsonParse<T>(response: Response): Promise<T | null> {
try {
return await response.json();
} catch (error) {
console.error("Failed to parse JSON response:", error);
return null;
}
}
export async function fetchWithRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries?: number;
retryDelay?: number;
retryableErrors?: (error: unknown) => boolean;
} = {}
): Promise<T> {
const {
maxRetries = 2,
retryDelay = 1000,
retryableErrors = (error) =>
error instanceof TimeoutError || error instanceof NetworkError
} = options;
let lastError: unknown;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt === maxRetries || !retryableErrors(error)) {
throw error;
}
const delay = retryDelay * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw lastError;
}