Skip to content

Commit

Permalink
fix(Body.body): Normalize Body.body into a node:stream (#924)
Browse files Browse the repository at this point in the history
* body conversion and test

* also handle blobs

* typeof null is object

* test for blob also

* lowercase boundary are easier

* unreachable code, body should never be a blob or buffer any more.

* stream singleton

* use let

* typo

* convert blob stream into a whatwg stream

* lint fix

* update changelog

Co-authored-by: Antoni Kepinski <xxczaki@pm.me>
  • Loading branch information
jimmywarting and xxczaki committed Sep 14, 2021
1 parent 9cd2e43 commit 8721d79
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 27 deletions.
5 changes: 5 additions & 0 deletions docs/CHANGELOG.md
Expand Up @@ -4,6 +4,11 @@ All notable changes will be recorded here.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## unreleased

- other: Deprecated/Discourage [form-data](https://www.npmjs.com/package/form-data) and body.buffer() (#1212)
- fix: Normalize `Body.body` into a `node:stream` (#924)

## v3.0.0

- other: Marking v3 as stable
Expand Down
40 changes: 16 additions & 24 deletions src/body.js
Expand Up @@ -36,7 +36,7 @@ export default class Body {
// Body is undefined or null
body = null;
} else if (isURLSearchParameters(body)) {
// Body is a URLSearchParams
// Body is a URLSearchParams
body = Buffer.from(body.toString());
} else if (isBlob(body)) {
// Body is blob
Expand All @@ -52,16 +52,25 @@ export default class Body {
// Body is stream
} else if (isFormData(body)) {
// Body is an instance of formdata-node
boundary = `NodeFetchFormDataBoundary${getBoundary()}`;
boundary = `nodefetchformdataboundary${getBoundary()}`;
body = Stream.Readable.from(formDataIterator(body, boundary));
} else {
// None of the above
// coerce to string then buffer
body = Buffer.from(String(body));
}

let stream = body;

if (Buffer.isBuffer(body)) {
stream = Stream.Readable.from(body);
} else if (isBlob(body)) {
stream = Stream.Readable.from(body.stream());
}

this[INTERNALS] = {
body,
stream,
boundary,
disturbed: false,
error: null
Expand All @@ -79,7 +88,7 @@ export default class Body {
}

get body() {
return this[INTERNALS].body;
return this[INTERNALS].stream;
}

get bodyUsed() {
Expand Down Expand Up @@ -170,23 +179,13 @@ async function consumeBody(data) {
throw data[INTERNALS].error;
}

let {body} = data;
const {body} = data;

// Body is null
if (body === null) {
return Buffer.alloc(0);
}

// Body is blob
if (isBlob(body)) {
body = Stream.Readable.from(body.stream());
}

// Body is buffer
if (Buffer.isBuffer(body)) {
return body;
}

/* c8 ignore next 3 */
if (!(body instanceof Stream)) {
return Buffer.alloc(0);
Expand Down Expand Up @@ -238,7 +237,7 @@ async function consumeBody(data) {
export const clone = (instance, highWaterMark) => {
let p1;
let p2;
let {body} = instance;
let {body} = instance[INTERNALS];

// Don't allow cloning a used body
if (instance.bodyUsed) {
Expand All @@ -254,7 +253,7 @@ export const clone = (instance, highWaterMark) => {
body.pipe(p1);
body.pipe(p2);
// Set instance body to teed body and return the other teed body
instance[INTERNALS].body = p1;
instance[INTERNALS].stream = p1;
body = p2;
}

Expand Down Expand Up @@ -331,7 +330,7 @@ export const extractContentType = (body, request) => {
* @returns {number | null}
*/
export const getTotalBytes = request => {
const {body} = request;
const {body} = request[INTERNALS];

// Body is null or undefined
if (body === null) {
Expand Down Expand Up @@ -373,13 +372,6 @@ export const writeToStream = (dest, {body}) => {
if (body === null) {
// Body is null
dest.end();
} else if (isBlob(body)) {
// Body is Blob
Stream.Readable.from(body.stream()).pipe(dest);
} else if (Buffer.isBuffer(body)) {
// Body is buffer
dest.write(body);
dest.end();
} else {
// Body is stream
body.pipe(dest);
Expand Down
4 changes: 2 additions & 2 deletions src/index.js
Expand Up @@ -12,7 +12,7 @@ import zlib from 'zlib';
import Stream, {PassThrough, pipeline as pump} from 'stream';
import dataUriToBuffer from 'data-uri-to-buffer';

import {writeToStream} from './body.js';
import {writeToStream, clone} from './body.js';
import Response from './response.js';
import Headers, {fromRawHeaders} from './headers.js';
import Request, {getNodeRequestOptions} from './request.js';
Expand Down Expand Up @@ -166,7 +166,7 @@ export default async function fetch(url, options_) {
agent: request.agent,
compress: request.compress,
method: request.method,
body: request.body,
body: clone(request),
signal: request.signal,
size: request.size
};
Expand Down
2 changes: 1 addition & 1 deletion src/request.js
Expand Up @@ -77,7 +77,7 @@ export default class Request extends Body {
if (inputBody !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(inputBody, this);
if (contentType) {
headers.append('Content-Type', contentType);
headers.set('Content-Type', contentType);
}
}

Expand Down
1 change: 1 addition & 0 deletions src/utils/is.js
Expand Up @@ -35,6 +35,7 @@ export const isURLSearchParameters = object => {
*/
export const isBlob = object => {
return (
object &&
typeof object === 'object' &&
typeof object.arrayBuffer === 'function' &&
typeof object.type === 'string' &&
Expand Down
32 changes: 32 additions & 0 deletions test/response.js
Expand Up @@ -208,6 +208,38 @@ describe('Response', () => {
expect(res.url).to.equal('');
});

it('should cast string to stream using res.body', () => {
const res = new Response('hi');
expect(res.body).to.be.an.instanceof(stream.Readable);
});

it('should cast typed array to stream using res.body', () => {
const res = new Response(Uint8Array.from([97]));
expect(res.body).to.be.an.instanceof(stream.Readable);
});

it('should cast blob to stream using res.body', () => {
const res = new Response(new Blob(['a']));
expect(res.body).to.be.an.instanceof(stream.Readable);
});

it('should not cast null to stream using res.body', () => {
const res = new Response(null);
expect(res.body).to.be.null;
});

it('should cast typed array to text using res.text()', async () => {
const res = new Response(Uint8Array.from([97]));
expect(await res.text()).to.equal('a');
});

it('should cast stream to text using res.text() in a roundabout way', async () => {
const {body} = new Response('a');
expect(body).to.be.an.instanceof(stream.Readable);
const res = new Response(body);
expect(await res.text()).to.equal('a');
});

it('should support error() static method', () => {
const res = Response.error();
expect(res).to.be.an.instanceof(Response);
Expand Down

0 comments on commit 8721d79

Please sign in to comment.