@@ -6,6 +6,7 @@ import type {
6
6
} from 'openid-client' ;
7
7
import { MongoDBOIDCError , type OIDCAbortSignal } from './types' ;
8
8
import { createHash , randomBytes } from 'crypto' ;
9
+ import type { Readable } from 'stream' ;
9
10
10
11
class AbortError extends Error {
11
12
constructor ( ) {
@@ -236,44 +237,67 @@ export class TokenSet {
236
237
}
237
238
}
238
239
240
+ function getCause ( err : unknown ) : Record < string , unknown > | undefined {
241
+ if (
242
+ err &&
243
+ typeof err === 'object' &&
244
+ 'cause' in err &&
245
+ err . cause &&
246
+ typeof err . cause === 'object'
247
+ ) {
248
+ return err . cause as Record < string , unknown > ;
249
+ }
250
+ }
251
+
239
252
// [email protected] has reduced error messages for HTTP errors significantly, reducing e.g.
240
253
// an HTTP error to just a simple 'unexpect HTTP response status code' message, without
241
254
// further diagnostic information. So if the `cause` of an `err` object is a fetch `Response`
242
255
// object, we try to throw a more helpful error.
243
256
export async function improveHTTPResponseBasedError < T > (
244
257
err : T
245
258
) : Promise < T | MongoDBOIDCError > {
246
- if (
247
- err &&
248
- typeof err === 'object' &&
249
- 'cause' in err &&
250
- err . cause &&
251
- typeof err . cause === 'object' &&
252
- 'status' in err . cause &&
253
- 'statusText' in err . cause &&
254
- 'text' in err . cause &&
255
- typeof err . cause . text === 'function'
256
- ) {
259
+ // Note: `err.cause` can either be an `Error` object itself, or a `Response`, or a JSON HTTP response body
260
+ const cause = getCause ( err ) ;
261
+ if ( cause ) {
257
262
try {
263
+ const statusObject =
264
+ 'status' in cause ? cause : ( err as Record < string , unknown > ) ;
265
+ if ( ! statusObject . status ) return err ;
266
+
258
267
let body = '' ;
259
268
try {
260
- body = await err . cause . text ( ) ;
269
+ if ( 'text' in cause && typeof cause . text === 'function' )
270
+ body = await cause . text ( ) ; // Handle the `Response` case
261
271
} catch {
262
272
// ignore
263
273
}
264
274
let errorMessageFromBody = '' ;
265
275
try {
266
- const parsed = JSON . parse ( body ) ;
276
+ let parsed : Record < string , unknown > = cause ;
277
+ try {
278
+ parsed = JSON . parse ( body ) ;
279
+ } catch {
280
+ // ignore, and maybe `parsed` already contains the parsed JSON body anyway
281
+ }
267
282
errorMessageFromBody =
268
- ': ' + String ( parsed . error_description || parsed . error || '' ) ;
283
+ ': ' +
284
+ [ parsed . error , parsed . error_description ]
285
+ . filter ( Boolean )
286
+ . map ( String )
287
+ . join ( ', ' ) ;
269
288
} catch {
270
289
// ignore
271
290
}
272
291
if ( ! errorMessageFromBody ) errorMessageFromBody = `: ${ body } ` ;
292
+
293
+ const statusTextInsert =
294
+ 'statusText' in statusObject
295
+ ? `(${ String ( statusObject . statusText ) } )`
296
+ : '' ;
273
297
return new MongoDBOIDCError (
274
298
`${ errorString ( err ) } : caused by HTTP response ${ String (
275
- err . cause . status
276
- ) } ( ${ String ( err . cause . statusText ) } ) ${ errorMessageFromBody } `,
299
+ statusObject . status
300
+ ) } ${ statusTextInsert } ${ errorMessageFromBody } `,
277
301
{ codeName : 'HTTPResponseError' , cause : err }
278
302
) ;
279
303
} catch {
@@ -282,3 +306,76 @@ export async function improveHTTPResponseBasedError<T>(
282
306
}
283
307
return err ;
284
308
}
309
+
310
+ // Check whether converting a Node.js `Readable` stream to a web `ReadableStream`
311
+ // is possible. We use this for compatibility with fetch() implementations that
312
+ // return Node.js `Readable` streams like node-fetch.
313
+ export function streamIsNodeReadable ( stream : unknown ) : stream is Readable {
314
+ return ! ! (
315
+ stream &&
316
+ typeof stream === 'object' &&
317
+ 'pipe' in stream &&
318
+ typeof stream . pipe === 'function' &&
319
+ ( ! ( 'cancel' in stream ) || ! stream . cancel )
320
+ ) ;
321
+ }
322
+
323
+ export function nodeFetchCompat (
324
+ response : Response & { body : Readable | ReadableStream | null }
325
+ ) : Response {
326
+ const notImplemented = ( method : string ) =>
327
+ new MongoDBOIDCError ( `Not implemented: body.${ method } ` , {
328
+ codeName : 'HTTPBodyShimNotImplemented' ,
329
+ } ) ;
330
+ const { body, clone } = response ;
331
+ if ( streamIsNodeReadable ( body ) ) {
332
+ let webStream : ReadableStream | undefined ;
333
+ const toWeb = ( ) =>
334
+ webStream ?? ( body . constructor as typeof Readable ) . toWeb ?.( body ) ;
335
+ // Provide ReadableStream methods that may be used by openid-client
336
+ Object . assign (
337
+ body ,
338
+ {
339
+ locked : false ,
340
+ cancel ( ) {
341
+ if ( webStream ) return webStream . cancel ( ) ;
342
+ body . resume ( ) ;
343
+ } ,
344
+ getReader ( ...args : Parameters < ReadableStream [ 'getReader' ] > ) {
345
+ if ( ( webStream = toWeb ( ) ) ) return webStream . getReader ( ...args ) ;
346
+
347
+ throw notImplemented ( 'getReader' ) ;
348
+ } ,
349
+ pipeThrough ( ...args : Parameters < ReadableStream [ 'pipeThrough' ] > ) {
350
+ if ( ( webStream = toWeb ( ) ) ) return webStream . pipeThrough ( ...args ) ;
351
+ throw notImplemented ( 'pipeThrough' ) ;
352
+ } ,
353
+ pipeTo ( ...args : Parameters < ReadableStream [ 'pipeTo' ] > ) {
354
+ if ( ( webStream = toWeb ( ) ) ) return webStream . pipeTo ( ...args ) ;
355
+
356
+ throw notImplemented ( 'pipeTo' ) ;
357
+ } ,
358
+ tee ( ...args : Parameters < ReadableStream [ 'tee' ] > ) {
359
+ if ( ( webStream = toWeb ( ) ) ) return webStream . tee ( ...args ) ;
360
+ throw notImplemented ( 'tee' ) ;
361
+ } ,
362
+ values ( ...args : Parameters < ReadableStream [ 'values' ] > ) {
363
+ if ( ( webStream = toWeb ( ) ) ) return webStream . values ( ...args ) ;
364
+ throw notImplemented ( 'values' ) ;
365
+ } ,
366
+ } ,
367
+ body
368
+ ) ;
369
+ Object . assign ( response , {
370
+ clone : function ( this : Response ) : Response {
371
+ // node-fetch replaces `.body` on `.clone()` on *both*
372
+ // the original and the cloned Response objects
373
+ const cloned = clone . call ( this ) ;
374
+ nodeFetchCompat ( this ) ;
375
+ return nodeFetchCompat ( cloned ) ;
376
+ } ,
377
+ } ) ;
378
+ }
379
+
380
+ return response ;
381
+ }
0 commit comments