From 22be61c25ade71fb44ed46434e5b97d7cce5576e Mon Sep 17 00:00:00 2001 From: Scott Lovegrove Date: Wed, 3 Dec 2025 17:06:37 +0000 Subject: [PATCH] fix: resolve undici browser compatibility with dynamic imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace static undici import with dynamic import and environment detection to prevent 'process.versions.node' errors in browser environments. - Remove static import of undici Agent - Add dynamic import that only loads undici in Node.js environments - Implement environment detection using process.versions?.node - Cache agent instance to prevent multiple imports - Maintain full backward compatibility for Node.js users - Enable browser usage without undici import errors Fixes browser bundling issues while preserving Node.js performance benefits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/utils/fetch-with-retry.ts | 42 ++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 8 deletions(-) diff --git a/src/utils/fetch-with-retry.ts b/src/utils/fetch-with-retry.ts index dcc8c2a7..b17cc9a3 100644 --- a/src/utils/fetch-with-retry.ts +++ b/src/utils/fetch-with-retry.ts @@ -1,4 +1,3 @@ -import { Agent } from 'undici' import type { HttpResponse, RetryConfig, CustomFetch, CustomFetchResponse } from '../types/http' import { isNetworkError } from '../types/http' @@ -16,13 +15,40 @@ const DEFAULT_RETRY_CONFIG: RetryConfig = { } /** - * HTTP agent with keepAlive disabled to prevent hanging connections - * This ensures the process exits immediately after requests complete + * Type for the undici Agent - represents what we need from the Agent instance + * We don't need to match all properties, just what's required for the dispatcher */ -const httpAgent = new Agent({ - keepAliveTimeout: 1, // Close connections after 1ms of idle time - keepAliveMaxTimeout: 1, // Maximum time to keep connections alive -}) +type UndiciAgent = { + // This is an opaque type - we just need to be able to pass it as a dispatcher + readonly [key: string]: unknown +} + +/** + * Cached HTTP agent to prevent creating multiple agents + * null = not initialized, undefined = browser env, UndiciAgent = Node.js env + */ +let httpAgent: UndiciAgent | undefined | null = null + +/** + * Gets the HTTP agent for Node.js environments or undefined for browser environments. + * Uses dynamic import to avoid loading undici in browser environments. + */ +async function getHttpAgent(): Promise { + if (httpAgent === null) { + if (typeof process !== 'undefined' && process.versions?.node) { + // We're in Node.js - dynamically import undici + const { Agent } = await import('undici') + httpAgent = new Agent({ + keepAliveTimeout: 1, // Close connections after 1ms of idle time + keepAliveMaxTimeout: 1, // Maximum time to keep connections alive + }) as unknown as UndiciAgent + } else { + // We're in browser - no agent needed + httpAgent = undefined + } + } + return httpAgent +} /** * Converts Headers object to a plain object @@ -141,7 +167,7 @@ export async function fetchWithRetry(args: { ...fetchOptions, signal: requestSignal, // @ts-expect-error - dispatcher is a valid option for Node.js fetch but not in the TS types - dispatcher: httpAgent, + dispatcher: await getHttpAgent(), }) fetchResponse = convertResponseToCustomFetch(nativeResponse) }