From 4869e5edcfafe0f926c807a6080343e2b97ce2f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 22:15:57 +0900 Subject: [PATCH] feat: implement `BodyReadable.bytes` (#3391) (#3711) (cherry picked from commit db8e6422f3f3d4ff2dfd4742a0e39974618bdd8b) Co-authored-by: tsctx <91457664+tsctx@users.noreply.github.com> --- README.md | 1 + docs/docs/api/Dispatcher.md | 12 +++++----- docs/docs/api/Fetch.md | 1 + lib/api/readable.js | 42 +++++++++++++++++++++++++++-------- test/client-request.js | 26 ++++++++++++++++++++++ test/readable.js | 21 ++++++++++++++++++ test/types/readable.test-d.ts | 3 +++ types/dispatcher.d.ts | 1 + types/readable.d.ts | 5 +++++ 9 files changed, 98 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4336ef06836..2ac58b6695e 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ The `body` mixins are the most common way to format the request/response body. M - [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) - [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob) +- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes) - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) diff --git a/docs/docs/api/Dispatcher.md b/docs/docs/api/Dispatcher.md index 574030bf686..67819ecd525 100644 --- a/docs/docs/api/Dispatcher.md +++ b/docs/docs/api/Dispatcher.md @@ -488,11 +488,13 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. `body` contains the following additional [body mixin](https://fetch.spec.whatwg.org/#body-mixin) methods and properties: -- `text()` -- `json()` -- `arrayBuffer()` -- `body` -- `bodyUsed` +* [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) +* [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob) +* [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes) +* [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) +* [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) +* `body` +* `bodyUsed` `body` can not be consumed twice. For example, calling `text()` after `json()` throws `TypeError`. diff --git a/docs/docs/api/Fetch.md b/docs/docs/api/Fetch.md index c3406f128dc..00c349847dc 100644 --- a/docs/docs/api/Fetch.md +++ b/docs/docs/api/Fetch.md @@ -28,6 +28,7 @@ This API is implemented as per the standard, you can find documentation on [MDN] - [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer) - [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob) +- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes) - [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata) - [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json) - [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text) diff --git a/lib/api/readable.js b/lib/api/readable.js index a65a7fcb557..47fbf3e0ef1 100644 --- a/lib/api/readable.js +++ b/lib/api/readable.js @@ -121,6 +121,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') @@ -306,6 +311,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[0]) + } + const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer) + + 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 @@ -315,17 +345,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..c67cecdb7f3 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(new TextEncoder().encode(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/readable.js b/test/readable.js index dd0631daf8b..e6a6ed0dccd 100644 --- a/test/readable.js +++ b/test/readable.js @@ -83,6 +83,27 @@ describe('Readable', () => { t.deepStrictEqual(arrayBuffer, expected) }) + test('.bytes()', async function (t) { + t = tspl(t, { plan: 1 }) + + function resume () { + } + function abort () { + } + const r = new Readable({ resume, abort }) + + r.push(Buffer.from('hello')) + r.push(Buffer.from(' world')) + + process.nextTick(() => { + r.push(null) + }) + + const bytes = await r.bytes() + + t.deepStrictEqual(bytes, new TextEncoder().encode('hello world')) + }) + test('.json()', async function (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 0aa2aba00e3..1b4c9c74a5d 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 */