11/**
2- * Wrapper to fix OpenAI SDK connection issues with Node.js 20+ and localhost
3- *
4- * Problem: Node.js 20+ uses undici for fetch(), which has bugs with localhost connections.
5- * Both native fetch() and OpenAI SDK fail silently on localhost in streaming mode.
6- *
7- * Solution: Use undici.Client directly instead of fetch(), which works correctly.
8- * This wrapper replaces global fetch with an undici-based implementation.
2+ * Undici-based fetch wrapper for fixing Node.js 20+ localhost streaming issues
93 */
104
11- import * as undici from "undici"
5+ import { Client , Dispatcher } from "undici"
126
13- const clientCache = new Map < string , undici . Client > ( )
7+ const clientCache = new Map < string , Client > ( )
148
15- function getOrCreateClient ( baseURL : string ) : undici . Client {
9+ function getOrCreateClient ( baseURL : string ) : Client {
1610 if ( ! clientCache . has ( baseURL ) ) {
17- clientCache . set ( baseURL , new undici . Client ( baseURL ) )
11+ clientCache . set ( baseURL , new Client ( baseURL ) )
1812 }
1913 return clientCache . get ( baseURL ) !
2014}
2115
2216interface FetchHeaders {
23- [ key : string ] : string | string [ ]
17+ [ key : string ] : string | string [ ] | undefined
2418}
2519
26- /**
27- * Wrapper for headers that implements the Headers interface
28- */
2920class HeadersWrapper {
30- private headersMap : Map < string , string >
21+ private headersMap = new Map < string , string > ( )
3122
3223 constructor ( headers : FetchHeaders ) {
33- this . headersMap = new Map ( )
3424 for ( const [ key , value ] of Object . entries ( headers ) ) {
25+ if ( value === undefined ) continue
3526 const headerValue = Array . isArray ( value ) ? value . join ( "," ) : String ( value )
3627 this . headersMap . set ( key . toLowerCase ( ) , headerValue )
3728 }
3829 }
3930
4031 get ( name : string ) : string | null {
41- return this . headersMap . get ( name . toLowerCase ( ) ) || null
32+ return this . headersMap . get ( name . toLowerCase ( ) ) ?? null
4233 }
4334
4435 has ( name : string ) : boolean {
@@ -49,19 +40,11 @@ class HeadersWrapper {
4940 return this . headersMap . entries ( )
5041 }
5142
52- keys ( ) : IterableIterator < string > {
53- return this . headersMap . keys ( )
54- }
55-
56- values ( ) : IterableIterator < string > {
57- return this . headersMap . values ( )
58- }
59-
6043 [ Symbol . iterator ] ( ) : IterableIterator < [ string , string ] > {
6144 return this . headersMap . entries ( )
6245 }
6346
64- forEach ( callback : ( value : string , key : string , parent : HeadersWrapper ) => void , thisArg ?: any ) : void {
47+ forEach ( callback : ( value : string , key : string , parent : HeadersWrapper ) => void , thisArg ?: unknown ) : void {
6548 this . headersMap . forEach ( ( value , key ) => {
6649 callback . call ( thisArg , value , key , this )
6750 } )
@@ -74,7 +57,7 @@ class FetchResponse {
7457 statusText : string
7558 headers : HeadersWrapper
7659 body : AsyncIterable < Buffer >
77- bodyUsed : boolean = false
60+ bodyUsed = false
7861
7962 constructor ( statusCode : number , headers : FetchHeaders , body : AsyncIterable < Buffer > ) {
8063 this . status = statusCode
@@ -84,88 +67,94 @@ class FetchResponse {
8467 this . body = body
8568 }
8669
87- async json ( ) {
88- let data = ""
89- for await ( const chunk of this . body ) {
90- data += chunk . toString ( )
91- }
92- return JSON . parse ( data )
70+ async json ( ) : Promise < any > {
71+ const text = await this . text ( )
72+ return JSON . parse ( text )
9373 }
9474
95- async text ( ) {
75+ async text ( ) : Promise < string > {
9676 let data = ""
9777 for await ( const chunk of this . body ) {
9878 data += chunk . toString ( )
9979 }
10080 return data
10181 }
10282
103- async blob ( ) {
83+ async arrayBuffer ( ) : Promise < ArrayBuffer > {
84+ const buffer = await this . blob ( )
85+ return buffer . buffer . slice ( buffer . byteOffset , buffer . byteOffset + buffer . byteLength )
86+ }
87+
88+ async blob ( ) : Promise < Buffer > {
10489 let data = Buffer . alloc ( 0 )
10590 for await ( const chunk of this . body ) {
10691 data = Buffer . concat ( [ data , chunk ] )
10792 }
10893 return data
10994 }
11095
111- async arrayBuffer ( ) {
112- const blob = await this . blob ( )
113- return blob . buffer . slice ( blob . byteOffset , blob . byteOffset + blob . byteLength )
114- }
115-
116- clone ( ) {
117- throw new Error ( "Response.clone() not implemented in undici wrapper" )
96+ clone ( ) : never {
97+ throw new Error ( "Response.clone() not implemented" )
11898 }
11999}
120100
121101/**
122- * Undici -based fetch wrapper that works with OpenAI SDK
102+ * Create undici -based fetch
123103 */
124- export function createUndicsiFetch ( ) {
125- return async function fetch ( url : string | URL , options ?: RequestInit & { timeout ?: number } ) : Promise < Response > {
104+ export function createUndiciFetch ( ) {
105+ return async function fetch ( url : string | URL , options ?: RequestInit & { timeout ?: number } ) : Promise < any > {
126106 const urlObj = new URL ( url )
127107 const baseURL = `${ urlObj . protocol } //${ urlObj . host } `
128- const path = urlObj . pathname + ( urlObj . search || "" )
108+ const path = urlObj . pathname + ( urlObj . search ?? "" )
129109
130110 const client = getOrCreateClient ( baseURL )
131111
112+ const method : Dispatcher . HttpMethod = ( options ?. method ?. toUpperCase ( ) ?? "GET" ) as Dispatcher . HttpMethod
113+
114+ const headers = ( options ?. headers as Record < string , string > ) ?? undefined
115+
116+ // Narrow body for undici
117+ let body : string | Buffer | Uint8Array | null | undefined = undefined
118+
119+ if ( typeof options ?. body === "string" ) body = options . body
120+ else if ( options ?. body instanceof Buffer ) body = options . body
121+ else if ( options ?. body instanceof Uint8Array ) body = options . body
122+ else if ( options ?. body == null ) body = undefined
123+ else body = String ( options . body )
124+
132125 try {
133- const response = await client . request ( {
126+ const response : Dispatcher . ResponseData = await client . request ( {
134127 path,
135- method : options ?. method || "GET" ,
136- headers : options ?. headers as Record < string , string > ,
137- body : options ?. body ,
128+ method,
129+ headers,
130+ body,
138131 } )
139132
140- return new FetchResponse ( response . statusCode , response . headers as FetchHeaders , response . body ) as any
133+ return new FetchResponse ( response . statusCode , response . headers as FetchHeaders , response . body )
141134 } catch ( error ) {
142135 throw new Error ( `Fetch failed: ${ error instanceof Error ? error . message : String ( error ) } ` )
143136 }
144137 }
145138}
146139
147140/**
148- * Install the undici-based fetch wrapper as global fetch
149- * Call this at the top of your application initialization
141+ * Install global fetch override
150142 */
151- export function installUndisciFetchWrapper ( ) {
152- if ( typeof globalThis !== "undefined" ) {
153- // Store original fetch for debugging/fallback
154- const originalFetch = ( globalThis as any ) . fetch
155-
156- // Override global fetch
157- ; ( globalThis as any ) . fetch = createUndicsiFetch ( )
158-
159- console . log ( "[undici-fetch-wrapper] Global fetch replaced with undici-based implementation" )
160-
161- // Return cleanup function
162- return ( ) => {
163- ; ( globalThis as any ) . fetch = originalFetch
164- // Close all cached clients
165- for ( const client of clientCache . values ( ) ) {
166- client . close ( ) . catch ( ( ) => { } )
167- }
168- clientCache . clear ( )
143+ export function installUndiciFetchWrapper ( ) : ( ( ) => void ) | void {
144+ if ( typeof globalThis === "undefined" ) {
145+ return
146+ }
147+
148+ const originalFetch = ( globalThis as any ) . fetch
149+ ; ( globalThis as any ) . fetch = createUndiciFetch ( )
150+
151+ console . log ( "[undici-fetch-wrapper] Global fetch replaced with undici Client implementation" )
152+
153+ return ( ) => {
154+ ; ( globalThis as any ) . fetch = originalFetch
155+ for ( const client of clientCache . values ( ) ) {
156+ client . close ( ) . catch ( ( ) => { } )
169157 }
158+ clientCache . clear ( )
170159 }
171160}
0 commit comments