Skip to content

Commit b65b0e1

Browse files
committed
Support Iterable and AsyncIterable as request body
Fixes #2392
1 parent 23d0b6b commit b65b0e1

File tree

5 files changed

+74
-7
lines changed

5 files changed

+74
-7
lines changed

documentation/2-options.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ stream.on('data', console.log);
293293

294294
### `body`
295295

296-
**Type: `string | Buffer | stream.Readable | Generator | AsyncGenerator | FormData` or [`form-data` instance](https://github.com/form-data/form-data)**
296+
**Type: `string | Buffer | stream.Readable | Generator | AsyncGenerator | Iterable | AsyncIterable | FormData` or [`form-data` instance](https://github.com/form-data/form-data)**
297297

298298
The payload to send.
299299

@@ -312,6 +312,22 @@ console.log(data);
312312
//=> 'Hello, world!'
313313
```
314314

315+
You can use `Iterable` and `AsyncIterable` objects as request body, including Web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
316+
317+
```js
318+
import got from 'got';
319+
320+
// Using an async generator
321+
async function* generateData() {
322+
yield 'Hello, ';
323+
yield 'world!';
324+
}
325+
326+
await got.post('https://httpbin.org/anything', {
327+
body: generateData()
328+
});
329+
```
330+
315331
Since Got 12, you can use spec-compliant [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) objects as request body, such as [`formdata-node`](https://github.com/octet-stream/form-data) or [`formdata-polyfill`](https://github.com/jimmywarting/FormData):
316332

317333
```js

source/core/index.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export type Progress = {
5353
const supportsBrotli = is.string(process.versions.brotli);
5454

5555
const methodsWithoutBody: ReadonlySet<string> = new Set(['GET', 'HEAD']);
56+
// Methods that should auto-end streams when no body is provided
57+
const methodsWithoutBodyStream: ReadonlySet<string> = new Set(['OPTIONS', 'DELETE', 'PATCH']);
5658

5759
export type GotEventFunction<T> =
5860
/**
@@ -976,7 +978,7 @@ export default class Request extends Duplex implements RequestEvents<Request> {
976978

977979
if (is.nodeStream(body)) {
978980
body.pipe(currentRequest);
979-
} else if (is.generator(body) || is.asyncGenerator(body)) {
981+
} else if (is.asyncIterable(body) || (is.iterable(body) && !is.string(body) && !isBuffer(body))) {
980982
(async () => {
981983
try {
982984
for await (const chunk of body) {
@@ -991,7 +993,9 @@ export default class Request extends Duplex implements RequestEvents<Request> {
991993
} else if (is.undefined(body)) {
992994
// No body to send, end the request
993995
const cannotHaveBody = methodsWithoutBody.has(this.options.method) && !(this.options.method === 'GET' && this.options.allowGetBody);
994-
if ((this._noPipe ?? false) || cannotHaveBody || currentRequest !== this) {
996+
const shouldAutoEndStream = methodsWithoutBodyStream.has(this.options.method);
997+
998+
if ((this._noPipe ?? false) || cannotHaveBody || currentRequest !== this || shouldAutoEndStream) {
995999
currentRequest.end();
9961000
}
9971001
} else {

source/core/options.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1342,13 +1342,30 @@ export default class Options {
13421342
The `content-length` header will be automatically set if `body` is a `string` / `Buffer` / [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) / [`form-data` instance](https://github.com/form-data/form-data), and `content-length` and `transfer-encoding` are not manually set in `options.headers`.
13431343
13441344
Since Got 12, the `content-length` is not automatically set when `body` is a `fs.createReadStream`.
1345+
1346+
You can use `Iterable` and `AsyncIterable` objects as request body, including Web [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream):
1347+
1348+
@example
1349+
```
1350+
import got from 'got';
1351+
1352+
// Using an async generator
1353+
async function* generateData() {
1354+
yield 'Hello, ';
1355+
yield 'world!';
1356+
}
1357+
1358+
await got.post('https://httpbin.org/anything', {
1359+
body: generateData()
1360+
});
1361+
```
13451362
*/
1346-
get body(): string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined {
1363+
get body(): string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined {
13471364
return this._internals.body;
13481365
}
13491366

1350-
set body(value: string | Buffer | Readable | Generator | AsyncGenerator | FormDataLike | undefined) {
1351-
assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, isFormData, is.undefined], value);
1367+
set body(value: string | Buffer | Readable | Generator | AsyncGenerator | Iterable<unknown> | AsyncIterable<unknown> | FormDataLike | undefined) {
1368+
assert.any([is.string, is.buffer, is.nodeStream, is.generator, is.asyncGenerator, is.iterable, is.asyncIterable, isFormData, is.undefined], value);
13521369

13531370
if (is.nodeStream(value)) {
13541371
assert.truthy(value.readable);

test/hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1315,7 +1315,7 @@ test('`beforeRequest` change body', withServer, async (t, server, got) => {
13151315
beforeRequest: [
13161316
options => {
13171317
options.body = JSON.stringify({payload: 'new'});
1318-
options.headers['content-length'] = options.body.length.toString();
1318+
options.headers['content-length'] = (options.body as string).length.toString();
13191319
},
13201320
],
13211321
},

test/post.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,3 +443,33 @@ test('formdata retry', withServer, async (t, server, got) => {
443443
message: 'Cannot retry with consumed body stream',
444444
});
445445
});
446+
447+
test('body - sends async iterable', withServer, async (t, server, got) => {
448+
server.post('/', defaultEndpoint);
449+
450+
async function * generateData() {
451+
yield 'Hello, ';
452+
yield 'world!';
453+
}
454+
455+
const body = await got.post({
456+
body: generateData(),
457+
}).text();
458+
459+
t.is(body, 'Hello, world!');
460+
});
461+
462+
test('body - sends iterable', withServer, async (t, server, got) => {
463+
server.post('/', defaultEndpoint);
464+
465+
function * generateData() {
466+
yield 'foo';
467+
yield 'bar';
468+
}
469+
470+
const body = await got.post({
471+
body: generateData(),
472+
}).text();
473+
474+
t.is(body, 'foobar');
475+
});

0 commit comments

Comments
 (0)