@@ -13,6 +13,8 @@ const assert = require('assert')
13
13
const { isErrored } = require ( '../core/util' )
14
14
const { isUint8Array, isArrayBuffer } = require ( 'util/types' )
15
15
const { File } = require ( './file' )
16
+ const { StringDecoder } = require ( 'string_decoder' )
17
+ const { parseMIMEType } = require ( './dataURL' )
16
18
17
19
let ReadableStream
18
20
@@ -301,117 +303,28 @@ function throwIfAborted (state) {
301
303
302
304
function bodyMixinMethods ( instance ) {
303
305
const methods = {
304
- async blob ( ) {
305
- if ( ! ( this instanceof instance ) ) {
306
- throw new TypeError ( 'Illegal invocation' )
307
- }
308
-
309
- throwIfAborted ( this [ kState ] )
310
-
311
- const chunks = [ ]
312
-
313
- for await ( const chunk of consumeBody ( this [ kState ] . body ) ) {
314
- if ( ! isUint8Array ( chunk ) ) {
315
- throw new TypeError ( 'Expected Uint8Array chunk' )
316
- }
317
-
318
- // Assemble one final large blob with Uint8Array's can exhaust memory.
319
- // That's why we create create multiple blob's and using references
320
- chunks . push ( new Blob ( [ chunk ] ) )
321
- }
322
-
323
- return new Blob ( chunks , { type : this . headers . get ( 'Content-Type' ) || '' } )
306
+ blob ( ) {
307
+ // The blob() method steps are to return the result of
308
+ // running consume body with this and Blob.
309
+ return specConsumeBody ( this , 'Blob' , instance )
324
310
} ,
325
311
326
- async arrayBuffer ( ) {
327
- if ( ! ( this instanceof instance ) ) {
328
- throw new TypeError ( 'Illegal invocation' )
329
- }
330
-
331
- throwIfAborted ( this [ kState ] )
332
-
333
- const contentLength = this . headers . get ( 'content-length' )
334
- const encoded = this . headers . has ( 'content-encoding' )
335
-
336
- // if we have content length and no encoding, then we can
337
- // pre allocate the buffer and just read the data into it
338
- if ( ! encoded && contentLength ) {
339
- const buffer = new Uint8Array ( contentLength )
340
- let offset = 0
341
-
342
- for await ( const chunk of consumeBody ( this [ kState ] . body ) ) {
343
- if ( ! isUint8Array ( chunk ) ) {
344
- throw new TypeError ( 'Expected Uint8Array chunk' )
345
- }
346
-
347
- buffer . set ( chunk , offset )
348
- offset += chunk . length
349
- }
350
-
351
- return buffer . buffer
352
- }
353
-
354
- // if we don't have content length, then we have to allocate 2x the
355
- // size of the body, once for consumed data, and once for the final buffer
356
-
357
- // This could be optimized by using growable ArrayBuffer, but it's not
358
- // implemented yet. https://github.com/tc39/proposal-resizablearraybuffer
359
-
360
- const chunks = [ ]
361
- let size = 0
362
-
363
- for await ( const chunk of consumeBody ( this [ kState ] . body ) ) {
364
- if ( ! isUint8Array ( chunk ) ) {
365
- throw new TypeError ( 'Expected Uint8Array chunk' )
366
- }
367
-
368
- chunks . push ( chunk )
369
- size += chunk . byteLength
370
- }
371
-
372
- const buffer = new Uint8Array ( size )
373
- let offset = 0
374
-
375
- for ( const chunk of chunks ) {
376
- buffer . set ( chunk , offset )
377
- offset += chunk . byteLength
378
- }
379
-
380
- return buffer . buffer
312
+ arrayBuffer ( ) {
313
+ // The arrayBuffer() method steps are to return the
314
+ // result of running consume body with this and ArrayBuffer.
315
+ return specConsumeBody ( this , 'ArrayBuffer' , instance )
381
316
} ,
382
317
383
- async text ( ) {
384
- if ( ! ( this instanceof instance ) ) {
385
- throw new TypeError ( 'Illegal invocation' )
386
- }
387
-
388
- throwIfAborted ( this [ kState ] )
389
-
390
- let result = ''
391
- const textDecoder = new TextDecoder ( )
392
-
393
- for await ( const chunk of consumeBody ( this [ kState ] . body ) ) {
394
- if ( ! isUint8Array ( chunk ) ) {
395
- throw new TypeError ( 'Expected Uint8Array chunk' )
396
- }
397
-
398
- result += textDecoder . decode ( chunk , { stream : true } )
399
- }
400
-
401
- // flush
402
- result += textDecoder . decode ( )
403
-
404
- return result
318
+ text ( ) {
319
+ // The text() method steps are to return the result of
320
+ // running consume body with this and text.
321
+ return specConsumeBody ( this , 'text' , instance )
405
322
} ,
406
323
407
- async json ( ) {
408
- if ( ! ( this instanceof instance ) ) {
409
- throw new TypeError ( 'Illegal invocation' )
410
- }
411
-
412
- throwIfAborted ( this [ kState ] )
413
-
414
- return JSON . parse ( await this . text ( ) )
324
+ json ( ) {
325
+ // The json() method steps are to return the result of
326
+ // running consume body with this and JSON.
327
+ return specConsumeBody ( this , 'JSON' , instance )
415
328
} ,
416
329
417
330
async formData ( ) {
@@ -534,6 +447,172 @@ function mixinBody (prototype) {
534
447
Object . assign ( prototype . prototype , bodyMixinMethods ( prototype ) )
535
448
}
536
449
450
+ // https://fetch.spec.whatwg.org/#concept-body-consume-body
451
+ async function specConsumeBody ( object , type , instance ) {
452
+ if ( ! ( object instanceof instance ) ) {
453
+ throw new TypeError ( 'Illegal invocation' )
454
+ }
455
+
456
+ // TODO: why is this needed?
457
+ throwIfAborted ( object [ kState ] )
458
+
459
+ // 1. If object is unusable, then return a promise rejected
460
+ // with a TypeError.
461
+ if ( bodyUnusable ( object [ kState ] . body ) ) {
462
+ throw new TypeError ( 'Body is unusable' )
463
+ }
464
+
465
+ // 2. Let promise be a promise resolved with an empty byte
466
+ // sequence.
467
+ let promise
468
+
469
+ // 3. If object’s body is non-null, then set promise to the
470
+ // result of fully reading body as promise given object’s
471
+ // body.
472
+ if ( object [ kState ] . body != null ) {
473
+ promise = await fullyReadBodyAsPromise ( object [ kState ] . body )
474
+ } else {
475
+ // step #2
476
+ promise = { size : 0 , bytes : [ new Uint8Array ( ) ] }
477
+ }
478
+
479
+ // 4. Let steps be to return the result of package data with
480
+ // the first argument given, type, and object’s MIME type.
481
+ const mimeType = type === 'Blob' || type === 'FormData'
482
+ ? bodyMimeType ( object )
483
+ : undefined
484
+
485
+ // 5. Return the result of upon fulfillment of promise given
486
+ // steps.
487
+ return packageData ( promise , type , mimeType )
488
+ }
489
+
490
+ /**
491
+ * @see https://fetch.spec.whatwg.org/#concept-body-package-data
492
+ * @param {{ size: number, bytes: Uint8Array[] } } bytes
493
+ * @param {string } type
494
+ * @param {ReturnType<typeof parseMIMEType>|undefined } mimeType
495
+ */
496
+ function packageData ( { bytes, size } , type , mimeType ) {
497
+ switch ( type ) {
498
+ case 'ArrayBuffer' : {
499
+ // Return a new ArrayBuffer whose contents are bytes.
500
+ const uint8 = new Uint8Array ( size )
501
+ let offset = 0
502
+
503
+ for ( const chunk of bytes ) {
504
+ uint8 . set ( chunk , offset )
505
+ offset += chunk . byteLength
506
+ }
507
+
508
+ return uint8 . buffer
509
+ }
510
+ case 'Blob' : {
511
+ // Return a Blob whose contents are bytes and type attribute
512
+ // is mimeType.
513
+ return new Blob ( bytes , { type : mimeType ?. essence } )
514
+ }
515
+ case 'JSON' : {
516
+ // Return the result of running parse JSON from bytes on bytes.
517
+ return JSON . parse ( utf8DecodeBytes ( bytes ) )
518
+ }
519
+ case 'text' : {
520
+ // 1. Return the result of running UTF-8 decode on bytes.
521
+ return utf8DecodeBytes ( bytes )
522
+ }
523
+ }
524
+ }
525
+
526
+ // https://fetch.spec.whatwg.org/#body-unusable
527
+ function bodyUnusable ( body ) {
528
+ // An object including the Body interface mixin is
529
+ // said to be unusable if its body is non-null and
530
+ // its body’s stream is disturbed or locked.
531
+ return body != null && ( body . stream . locked || util . isDisturbed ( body . stream ) )
532
+ }
533
+
534
+ // https://fetch.spec.whatwg.org/#fully-reading-body-as-promise
535
+ async function fullyReadBodyAsPromise ( body ) {
536
+ // 1. Let reader be the result of getting a reader for body’s
537
+ // stream. If that threw an exception, then return a promise
538
+ // rejected with that exception.
539
+ const reader = body . stream . getReader ( )
540
+
541
+ // 2. Return the result of reading all bytes from reader.
542
+ /** @type {Uint8Array[] } */
543
+ const bytes = [ ]
544
+ let size = 0
545
+
546
+ while ( true ) {
547
+ const { done, value } = await reader . read ( )
548
+
549
+ if ( done ) {
550
+ break
551
+ }
552
+
553
+ // https://streams.spec.whatwg.org/#read-loop
554
+ // If chunk is not a Uint8Array object, reject promise with
555
+ // a TypeError and abort these steps.
556
+ if ( ! isUint8Array ( value ) ) {
557
+ throw new TypeError ( 'Value is not a Uint8Array.' )
558
+ }
559
+
560
+ bytes . push ( value )
561
+ size += value . byteLength
562
+ }
563
+
564
+ return { size, bytes }
565
+ }
566
+
567
+ /**
568
+ * @see https://encoding.spec.whatwg.org/#utf-8-decode
569
+ * @param {Uint8Array[] } ioQueue
570
+ */
571
+ function utf8DecodeBytes ( ioQueue ) {
572
+ if ( ioQueue . length === 0 ) {
573
+ return ''
574
+ }
575
+
576
+ // 1. Let buffer be the result of peeking three bytes
577
+ // from ioQueue, converted to a byte sequence.
578
+ const buffer = ioQueue [ 0 ]
579
+
580
+ // 2. If buffer is 0xEF 0xBB 0xBF, then read three
581
+ // bytes from ioQueue. (Do nothing with those bytes.)
582
+ if ( buffer [ 0 ] === 0xEF && buffer [ 1 ] === 0xBB && buffer [ 2 ] === 0xBF ) {
583
+ ioQueue [ 0 ] = ioQueue [ 0 ] . subarray ( 3 )
584
+ }
585
+
586
+ // 3. Process a queue with an instance of UTF-8’s
587
+ // decoder, ioQueue, output, and "replacement".
588
+ const decoder = new StringDecoder ( 'utf-8' )
589
+ let output = ''
590
+
591
+ for ( const chunk of ioQueue ) {
592
+ output += decoder . write ( chunk )
593
+ }
594
+
595
+ output += decoder . end ( )
596
+
597
+ // 4. Return output.
598
+ return output
599
+ }
600
+
601
+ /**
602
+ * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
603
+ * @param {import('./response').Response|import('./request').Request } object
604
+ */
605
+ function bodyMimeType ( object ) {
606
+ const { headersList } = object [ kState ]
607
+ const contentType = headersList . get ( 'content-type' )
608
+
609
+ if ( contentType === null ) {
610
+ return 'failure'
611
+ }
612
+
613
+ return parseMIMEType ( contentType )
614
+ }
615
+
537
616
module . exports = {
538
617
extractBody,
539
618
safelyExtractBody,
0 commit comments