From 8721d79208ad52c44fffb4b5b5cfa13b936022c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20W=C3=A4rting?= Date: Tue, 14 Sep 2021 11:39:33 +0200 Subject: [PATCH] fix(Body.body): Normalize `Body.body` into a `node:stream` (#924) * 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 --- docs/CHANGELOG.md | 5 +++++ src/body.js | 40 ++++++++++++++++------------------------ src/index.js | 4 ++-- src/request.js | 2 +- src/utils/is.js | 1 + test/response.js | 32 ++++++++++++++++++++++++++++++++ 6 files changed, 57 insertions(+), 27 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 71ec19ae7..4081cf629 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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 diff --git a/src/body.js b/src/body.js index ecc50ed5f..82991eff8 100644 --- a/src/body.js +++ b/src/body.js @@ -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 @@ -52,7 +52,7 @@ 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 @@ -60,8 +60,17 @@ export default class Body { 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 @@ -79,7 +88,7 @@ export default class Body { } get body() { - return this[INTERNALS].body; + return this[INTERNALS].stream; } get bodyUsed() { @@ -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); @@ -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) { @@ -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; } @@ -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) { @@ -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); diff --git a/src/index.js b/src/index.js index a87666512..0c5e917b7 100644 --- a/src/index.js +++ b/src/index.js @@ -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'; @@ -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 }; diff --git a/src/request.js b/src/request.js index e5856b2c7..8336150c3 100644 --- a/src/request.js +++ b/src/request.js @@ -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); } } diff --git a/src/utils/is.js b/src/utils/is.js index fa8d15922..d23b9f027 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -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' && diff --git a/test/response.js b/test/response.js index 9b89fefb6..7c3dab5f0 100644 --- a/test/response.js +++ b/test/response.js @@ -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);