UNPKG

@lodestar/utils

Version:

Utilities required across multiple lodestar packages

189 lines (169 loc) 5.78 kB
/** * Native fetch with transparent and consistent error handling * * [MDN Reference](https://developer.mozilla.org/docs/Web/API/fetch) */ async function wrappedFetch(url: string | URL, init?: RequestInit): Promise<Response> { try { // This function wraps global `fetch` which should only be directly called here // biome-ignore lint/style/noRestrictedGlobals: We need to use global `fetch` return await fetch(url, init); } catch (e) { throw new FetchError(url, e); } } export {wrappedFetch as fetch}; export function isFetchError(e: unknown): e is FetchError { return e instanceof FetchError; } export type FetchErrorType = "failed" | "input" | "aborted" | "timeout" | "unknown"; type FetchErrorCause = NativeFetchFailedError["cause"] | NativeFetchInputError["cause"]; export class FetchError extends Error { type: FetchErrorType; code: string; cause?: FetchErrorCause; constructor(url: string | URL, e: unknown) { if (isNativeFetchFailedError(e)) { super(`Request to ${url.toString()} failed, reason: ${e.cause.message}`); this.type = "failed"; this.code = e.cause.code || "ERR_FETCH_FAILED"; this.cause = e.cause; } else if (isNativeFetchInputError(e)) { // For input errors the e.message is more detailed super(e.message); this.type = "input"; this.code = e.cause.code || "ERR_INVALID_INPUT"; this.cause = e.cause; } else if (isNativeFetchAbortError(e)) { super(`Request to ${url.toString()} was aborted`); this.type = "aborted"; this.code = "ERR_ABORTED"; } else if (isNativeFetchTimeoutError(e)) { super(`Request to ${url.toString()} timed out`); this.type = "timeout"; this.code = "ERR_TIMEOUT"; } // There are few incompatibilities related to `fetch` with NodeJS // So we have to wrap those cases here explicitly // https://github.com/oven-sh/bun/issues/20486 else if (isBunError(e) && e.code === "ConnectionRefused") { super("TypeError: fetch failed"); this.type = "failed"; this.code = "ENOTFOUND"; this.cause = e as unknown as FetchErrorCause; } else if (isBunError(e) && e.code === "ECONNRESET") { super("TypeError: fetch failed"); this.type = "failed"; this.code = "UND_ERR_SOCKET"; this.cause = e as unknown as FetchErrorCause; } else if (isBun && (e as Error).message.includes("protocol must be")) { super("fetch failed"); this.type = "failed"; this.code = "ERR_FETCH_FAILED"; this.cause = e as unknown as FetchErrorCause; } else if ((e as Error).message.includes("URL is invalid")) { super("Failed to parse URL from invalid-url"); this.type = "input"; this.code = "ERR_INVALID_URL"; this.cause = e as unknown as FetchErrorCause; } else { super((e as Error).message); this.type = "unknown"; this.code = "ERR_UNKNOWN"; } this.name = this.constructor.name; } } type NativeFetchError = TypeError & { cause: Error & { code?: string; }; }; /** * ``` * TypeError: fetch failed * cause: Error: connect ECONNREFUSED 127.0.0.1:9596 * errno: -111, * code: 'ECONNREFUSED', * syscall: 'connect', * address: '127.0.0.1', * port: 9596 * --------------------------- * TypeError: fetch failed * cause: Error: getaddrinfo ENOTFOUND non-existent-domain * errno: -3008, * code: 'ENOTFOUND', * syscall: 'getaddrinfo', * hostname: 'non-existent-domain' * --------------------------- * TypeError: fetch failed * cause: SocketError: other side closed * code: 'UND_ERR_SOCKET', * socket: {} * --------------------------- * TypeError: fetch failed * cause: Error: unknown scheme * [cause]: undefined * ``` */ type NativeFetchFailedError = NativeFetchError & { message: "fetch failed"; cause: { errno?: string; syscall?: string; address?: string; port?: string; hostname?: string; socket?: object; [prop: string]: unknown; }; }; /** * ``` * TypeError: Failed to parse URL from invalid-url * [cause]: TypeError [ERR_INVALID_URL]: Invalid URL * input: 'invalid-url', * code: 'ERR_INVALID_URL' * ``` */ type NativeFetchInputError = NativeFetchError & { cause: { input: unknown; }; }; /** * ``` * DOMException [AbortError]: This operation was aborted * ``` */ type NativeFetchAbortError = DOMException & { name: "AbortError"; }; /** * ``` * DOMException [TimeoutError]: The operation was aborted due to timeout * ``` */ type NativeFetchTimeoutError = DOMException & { name: "TimeoutError"; }; function isNativeFetchError(e: unknown): e is NativeFetchError { return e instanceof TypeError && (e as NativeFetchError).cause instanceof Error; } function isNativeFetchFailedError(e: unknown): e is NativeFetchFailedError { return isNativeFetchError(e) && (e as NativeFetchFailedError).message === "fetch failed"; } function isNativeFetchInputError(e: unknown): e is NativeFetchInputError { return isNativeFetchError(e) && (e as NativeFetchInputError).cause.input !== undefined; } function isNativeFetchAbortError(e: unknown): e is NativeFetchAbortError { return e instanceof DOMException && (e as NativeFetchAbortError).name === "AbortError"; } function isNativeFetchTimeoutError(e: unknown): e is NativeFetchTimeoutError { return e instanceof DOMException && (e as NativeFetchTimeoutError).name === "TimeoutError"; } const isBun = "bun" in process.versions; type BunError = {code: string; path: string; errno: number; message: string}; function isBunError(e: unknown): e is BunError { return isBun && typeof e === "object" && e !== null && "code" in e && "path" in e && "errno" in e && "message" in e; }