Skip to content

Commit deed628

Browse files
authored
fix(fetch): implement fully read body algorithm (#1597)
1 parent 0d1419c commit deed628

3 files changed

Lines changed: 79 additions & 12 deletions

File tree

lib/fetch/index.js

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ const {
3434
sameOrigin,
3535
isCancelled,
3636
isAborted,
37-
isErrorLike
37+
isErrorLike,
38+
fullyReadBody
3839
} = require('./util')
3940
const { kState, kHeaders, kGuard, kRealm } = require('./symbols')
4041
const assert = require('assert')
@@ -738,11 +739,7 @@ async function mainFetch (fetchParams, recursive = false) {
738739
}
739740

740741
// 4. Fully read response’s body given processBody and processBodyError.
741-
try {
742-
processBody(await response.arrayBuffer())
743-
} catch (err) {
744-
processBodyError(err)
745-
}
742+
await fullyReadBody(response.body, processBody, processBodyError)
746743
} else {
747744
// 21. Otherwise, run fetch finale given fetchParams and response.
748745
fetchFinale(fetchParams, response)
@@ -974,11 +971,7 @@ async function fetchFinale (fetchParams, response) {
974971
} else {
975972
// 4. Otherwise, fully read response’s body given processBody, processBodyError,
976973
// and fetchParams’s task destination.
977-
try {
978-
processBody(await response.body.stream.arrayBuffer())
979-
} catch (err) {
980-
processBodyError(err)
981-
}
974+
await fullyReadBody(response.body, processBody, processBodyError)
982975
}
983976
}
984977
}

lib/fetch/util.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const { redirectStatus } = require('./constants')
44
const { performance } = require('perf_hooks')
55
const { isBlobLike, toUSVString, ReadableStreamFrom } = require('../core/util')
66
const assert = require('assert')
7+
const { isUint8Array } = require('util/types')
78

89
let File
910

@@ -438,6 +439,53 @@ function makeIterator (iterator, name) {
438439
return Object.setPrototypeOf({}, i)
439440
}
440441

442+
/**
443+
* @see https://fetch.spec.whatwg.org/#body-fully-read
444+
*/
445+
async function fullyReadBody (body, processBody, processBodyError) {
446+
// 1. If taskDestination is null, then set taskDestination to
447+
// the result of starting a new parallel queue.
448+
449+
// 2. Let promise be the result of fully reading body as promise
450+
// given body.
451+
try {
452+
/** @type {Uint8Array[]} */
453+
const chunks = []
454+
let length = 0
455+
456+
const reader = body.stream.getReader()
457+
458+
while (true) {
459+
const { done, value } = await reader.read()
460+
461+
if (done === true) {
462+
break
463+
}
464+
465+
// read-loop chunk steps
466+
assert(isUint8Array(value))
467+
468+
chunks.push(value)
469+
length += value.byteLength
470+
}
471+
472+
// 3. Let fulfilledSteps given a byte sequence bytes be to queue
473+
// a fetch task to run processBody given bytes, with
474+
// taskDestination.
475+
const fulfilledSteps = (bytes) => queueMicrotask(() => {
476+
processBody(bytes)
477+
})
478+
479+
fulfilledSteps(Buffer.concat(chunks, length))
480+
} catch (err) {
481+
// 4. Let rejectedSteps be to queue a fetch task to run
482+
// processBodyError, with taskDestination.
483+
queueMicrotask(() => processBodyError(err))
484+
}
485+
486+
// 5. React to promise with fulfilledSteps and rejectedSteps.
487+
}
488+
441489
/**
442490
* Fetch supports node >= 16.8.0, but Object.hasOwn was added in v16.9.0.
443491
*/
@@ -477,5 +525,6 @@ module.exports = {
477525
isValidHeaderName,
478526
isValidHeaderValue,
479527
hasOwn,
480-
isErrorLike
528+
isErrorLike,
529+
fullyReadBody
481530
}

test/fetch/client-fetch.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,3 +536,28 @@ test('Receiving non-Latin1 headers', async (t) => {
536536
t.same(lengths, [30, 34, 94, 104, 90])
537537
t.end()
538538
})
539+
540+
// https://github.com/nodejs/undici/issues/1594
541+
// TODO(@KhafraDev): this test fails because of an integrity-mismatch check
542+
// that hasn't been implemented when this comment was written. Enable this
543+
// check once a resource's integrity is checked.
544+
// test('with RequestInit.integrity set', async (t) => {
545+
// const body = 'Hello world'
546+
// const hash = require('crypto').createHash('sha256').update(body).digest('hex')
547+
//
548+
// const server = createServer((req, res) => {
549+
// res.write(body)
550+
// res.end()
551+
// }).listen(0)
552+
//
553+
// t.teardown(server.close.bind(server))
554+
// await once(server, 'listening')
555+
//
556+
// const response = await fetch(`http://localhost:${server.address().port}`, {
557+
// integrity: `sha256-${hash}`
558+
// })
559+
//
560+
// const ab = await response.arrayBuffer()
561+
//
562+
// t.same(new Uint8Array(ab), new TextEncoder().encode(body))
563+
// })

0 commit comments

Comments
 (0)