Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

[Backport v6.x] feat: implement BodyReadable.bytes #3711

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
12 changes: 7 additions & 5 deletions docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
1 change: 1 addition & 0 deletions docs/docs/api/Fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
42 changes: 33 additions & 9 deletions lib/api/readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
26 changes: 26 additions & 0 deletions test/client-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Expand Down
21 changes: 21 additions & 0 deletions test/readable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })

Expand Down
3 changes: 3 additions & 0 deletions test/types/readable.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ expectAssignable<BodyReadable>(new BodyReadable())
// blob
expectAssignable<Promise<Blob>>(readable.blob())

// bytes
expectAssignable<Promise<Uint8Array>>(readable.bytes())

// arrayBuffer
expectAssignable<Promise<ArrayBuffer>>(readable.arrayBuffer())

Expand Down
1 change: 1 addition & 0 deletions types/dispatcher.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ declare namespace Dispatcher {
readonly bodyUsed: boolean;
arrayBuffer(): Promise<ArrayBuffer>;
blob(): Promise<Blob>;
bytes(): Promise<Uint8Array>;
formData(): Promise<never>;
json(): Promise<unknown>;
text(): Promise<string>;
Expand Down
5 changes: 5 additions & 0 deletions types/readable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ declare class BodyReadable extends Readable {
*/
blob(): Promise<Blob>

/** Consumes and returns the body as an Uint8Array
* https://fetch.spec.whatwg.org/#dom-body-bytes
*/
bytes(): Promise<Uint8Array>

/** Consumes and returns the body as an ArrayBuffer
* https://fetch.spec.whatwg.org/#dom-body-arraybuffer
*/
Expand Down
Loading