Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix(Body.body): Normalize Body.body into a node:stream #924

Merged
merged 14 commits into from Sep 14, 2021
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