3
3
import cors from "cors" ;
4
4
import { parseArgs } from "node:util" ;
5
5
import { parse as shellParseArgs } from "shell-quote" ;
6
+ import nodeFetch , { Headers as NodeHeaders } from "node-fetch" ;
7
+
8
+ // Type-compatible wrappers for node-fetch to work with browser-style types
9
+ const fetch = nodeFetch ;
10
+ const Headers = NodeHeaders ;
6
11
7
12
import {
8
13
SSEClientTransport ,
@@ -231,13 +236,37 @@ const authMiddleware = (
231
236
next ( ) ;
232
237
} ;
233
238
239
+ /**
240
+ * Converts a Node.js ReadableStream to a web-compatible ReadableStream
241
+ * This is necessary for the EventSource polyfill which expects web streams
242
+ */
243
+ const createWebReadableStream = ( nodeStream : any ) : ReadableStream => {
244
+ return new ReadableStream ( {
245
+ start ( controller ) {
246
+ nodeStream . on ( "data" , ( chunk : any ) => {
247
+ controller . enqueue ( chunk ) ;
248
+ } ) ;
249
+ nodeStream . on ( "end" , ( ) => {
250
+ controller . close ( ) ;
251
+ } ) ;
252
+ nodeStream . on ( "error" , ( err : any ) => {
253
+ controller . error ( err ) ;
254
+ } ) ;
255
+ } ,
256
+ } ) ;
257
+ } ;
258
+
234
259
/**
235
260
* Creates a `fetch` function that merges dynamic session headers with the
236
261
* headers from the actual request, ensuring that request-specific headers like
237
- * `Content-Type` are preserved.
262
+ * `Content-Type` are preserved. For SSE requests, it also converts Node.js
263
+ * streams to web-compatible streams.
238
264
*/
239
265
const createCustomFetch = ( headerHolder : { headers : HeadersInit } ) => {
240
- return ( input : RequestInfo | URL , init ? : RequestInit ) : Promise < Response > => {
266
+ return async (
267
+ input : RequestInfo | URL ,
268
+ init ?: RequestInit ,
269
+ ) : Promise < Response > => {
241
270
// Determine the headers from the original request/init.
242
271
// The SDK may pass a Request object or a URL and an init object.
243
272
const originalHeaders =
@@ -252,8 +281,43 @@ const createCustomFetch = (headerHolder: { headers: HeadersInit }) => {
252
281
finalHeaders . set ( key , value ) ;
253
282
} ) ;
254
283
255
- // This works for both `fetch(url, init)` and `fetch(request)` style calls.
256
- return fetch ( input , { ...init , headers : finalHeaders } ) ;
284
+ // Convert Headers to a plain object for node-fetch compatibility
285
+ const headersObject : Record < string , string> = { } ;
286
+ finalHeaders . forEach ( ( value , key ) => {
287
+ headersObject [ key ] = value ;
288
+ } ) ;
289
+
290
+ // Get the response from node-fetch (cast input and init to handle type differences)
291
+ const response = await fetch (
292
+ input as any ,
293
+ { ...init , headers : headersObject } as any ,
294
+ ) ;
295
+
296
+ // Check if this is an SSE request by looking at the Accept header
297
+ const acceptHeader = finalHeaders . get ( "Accept" ) ;
298
+ const isSSE = acceptHeader ?. includes ( "text/event-stream" ) ;
299
+
300
+ if ( isSSE && response . body ) {
301
+ // For SSE requests, we need to convert the Node.js stream to a web ReadableStream
302
+ // because the EventSource polyfill expects web-compatible streams
303
+ const webStream = createWebReadableStream ( response . body ) ;
304
+
305
+ // Create a new response with the web-compatible stream
306
+ // Convert node-fetch headers to plain object for web Response compatibility
307
+ const responseHeaders : Record < string , string> = { } ;
308
+ response . headers . forEach ( ( value : string , key : string ) => {
309
+ responseHeaders [ key ] = value ;
310
+ } ) ;
311
+
312
+ return new Response ( webStream , {
313
+ status : response . status ,
314
+ statusText : response . statusText ,
315
+ headers : responseHeaders ,
316
+ } ) as Response ;
317
+ }
318
+
319
+ // For non-SSE requests, return the response as-is (cast to handle type differences)
320
+ return response as unknown as Response ;
257
321
} ;
258
322
} ;
259
323
0 commit comments