diff --git a/lib/api/readable.js b/lib/api/readable.js index a04ca8edca2..a2af6ae9bcd 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -124,6 +124,11 @@ class BodyReadable extends Readable { return consume(this, 'blob') } + // https://fetch.spec.whatwg.org/#dom-body-bytes + async bytes () { + return consume(this, 'bytes') + } + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer async arrayBuffer () { return consume(this, 'arrayBuffer') @@ -308,6 +313,31 @@ function chunksDecode (chunks, length) { return buffer.utf8Slice(start, bufferLength) } +/** + * @param {Buffer[]} chunks + * @param {number} length + * @returns {Uint8Array} + */ +function chunksConcat (chunks, length) { + if (chunks.length === 0 || length === 0) { + return new Uint8Array(0) + } + if (chunks.length === 1) { + // fast-path + return new Uint8Array(chunks[1]) + } + const buffer = new Uint8Array(length) + + let offset = 0 + for (let i = 0; i < chunks.length; ++i) { + const chunk = chunks[i] + buffer.set(chunk, offset) + offset += chunk.length + } + + return buffer +} + function consumeEnd (consume) { const { type, body, resolve, stream, length } = consume @@ -317,17 +347,11 @@ function consumeEnd (consume) { } else if (type === 'json') { resolve(JSON.parse(chunksDecode(body, length))) } else if (type === 'arrayBuffer') { - const dst = new Uint8Array(length) - - let pos = 0 - for (const buf of body) { - dst.set(buf, pos) - pos += buf.byteLength - } - - resolve(dst.buffer) + resolve(chunksConcat(body, length).buffer) } else if (type === 'blob') { resolve(new Blob(body, { type: stream[kContentType] })) + } else if (type === 'bytes') { + resolve(chunksConcat(body, length)) } consumeFinish(consume) diff --git a/test/client-request.js b/test/client-request.js index 8cbad5ccb48..b015a6408a2 100644 --- a/test/client-request.js +++ b/test/client-request.js @@ -655,6 +655,32 @@ test('request arrayBuffer', async (t) => { await t.completed }) +test('request bytes', async (t) => { + t = tspl(t, { plan: 2 }) + + const obj = { asd: true } + const server = createServer((req, res) => { + res.end(JSON.stringify(obj)) + }) + after(() => server.close()) + + server.listen(0, async () => { + const client = new Client(`http://localhost:${server.address().port}`) + after(() => client.destroy()) + + const { body } = await client.request({ + path: '/', + method: 'GET' + }) + const bytes = await body.bytes() + + t.deepStrictEqual(Buffer.from(JSON.stringify(obj)), bytes) + t.ok(bytes instanceof Uint8Array) + }) + + await t.completed +}) + test('request body', async (t) => { t = tspl(t, { plan: 1 }) diff --git a/test/types/readable.test-d.ts b/test/types/readable.test-d.ts index d004b706569..b5d32f6c221 100644 --- a/test/types/readable.test-d.ts +++ b/test/types/readable.test-d.ts @@ -20,6 +20,9 @@ expectAssignable(new BodyReadable()) // blob expectAssignable>(readable.blob()) + // bytes + expectAssignable>(readable.bytes()) + // arrayBuffer expectAssignable>(readable.arrayBuffer()) diff --git a/types/dispatcher.d.ts b/types/dispatcher.d.ts index 05f0093c3fd..89350159eab 100644 --- a/types/dispatcher.d.ts +++ b/types/dispatcher.d.ts @@ -244,6 +244,7 @@ declare namespace Dispatcher { readonly bodyUsed: boolean; arrayBuffer(): Promise; blob(): Promise; + bytes(): Promise; formData(): Promise; json(): Promise; text(): Promise; diff --git a/types/readable.d.ts b/types/readable.d.ts index a5fce8a20d3..c4f052af05e 100644 --- a/types/readable.d.ts +++ b/types/readable.d.ts @@ -25,6 +25,11 @@ declare class BodyReadable extends Readable { */ blob(): Promise + /** Consumes and returns the body as an Uint8Array + * https://fetch.spec.whatwg.org/#dom-body-bytes + */ + bytes(): Promise + /** Consumes and returns the body as an ArrayBuffer * https://fetch.spec.whatwg.org/#dom-body-arraybuffer */