Skip to content

Commit

Permalink
feat(Body): Added support for BodyMixin.formData() and constructing…
Browse files Browse the repository at this point in the history
… bodies with FormData (#1314)

Added support for body toFormData
  • Loading branch information
jimmywarting committed Nov 8, 2021
1 parent 37ac459 commit 3944f24
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 198 deletions.
2 changes: 2 additions & 0 deletions @types/index.d.ts
Expand Up @@ -103,6 +103,7 @@ export type BodyInit =
| Blob
| Buffer
| URLSearchParams
| FormData
| NodeJS.ReadableStream
| string;
declare class BodyMixin {
Expand All @@ -117,6 +118,7 @@ declare class BodyMixin {
*/
buffer(): Promise<Buffer>;
arrayBuffer(): Promise<ArrayBuffer>;
formData(): Promise<FormData>;
blob(): Promise<Blob>;
json(): Promise<unknown>;
text(): Promise<string>;
Expand Down
10 changes: 2 additions & 8 deletions README.md
Expand Up @@ -731,6 +731,8 @@ A boolean property for if this body has been consumed. Per the specs, a consumed

#### body.arrayBuffer()

#### body.formData()

#### body.blob()

#### body.json()
Expand All @@ -743,14 +745,6 @@ A boolean property for if this body has been consumed. Per the specs, a consumed

Consume the body and return a promise that will resolve to one of these formats.

#### body.buffer()

<small>_(node-fetch extension)_</small>

- Returns: `Promise<Buffer>`

Consume the body and return a promise that will resolve to a Buffer.

<a id="class-fetcherror"></a>

### Class: FetchError
Expand Down
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## unreleased

- other: Deprecated/Discourage [form-data](https://www.npmjs.com/package/form-data) and `body.buffer()` (#1212)
- feat: Add `Body#formData()` (#1314)
- fix: Normalize `Body.body` into a `node:stream` (#924)
- fix: Pass url string to `http.request` for parsing IPv6 urls (#1268)
- fix: Throw error when constructing Request with urls including basic auth (#1268)
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -55,14 +55,15 @@
"coveralls": "^3.1.0",
"delay": "^5.0.0",
"form-data": "^4.0.0",
"formdata-node": "^3.5.4",
"formdata-node": "^4.2.4",
"mocha": "^9.1.3",
"p-timeout": "^5.0.0",
"tsd": "^0.14.0",
"xo": "^0.39.1"
},
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"formdata-polyfill": "^4.0.10",
"fetch-blob": "^3.1.2"
},
"tsd": {
Expand Down Expand Up @@ -91,6 +92,7 @@
"unicorn/numeric-separators-style": 0,
"unicorn/explicit-length-check": 0,
"capitalized-comments": 0,
"node/no-unsupported-features/es-syntax": 0,
"@typescript-eslint/member-ordering": 0
},
"overrides": [
Expand Down
37 changes: 25 additions & 12 deletions src/body.js
Expand Up @@ -9,11 +9,11 @@ import Stream, {PassThrough} from 'node:stream';
import {types, deprecate} from 'node:util';

import Blob from 'fetch-blob';
import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js';

import {FetchError} from './errors/fetch-error.js';
import {FetchBaseError} from './errors/base.js';
import {formDataIterator, getBoundary, getFormDataLength} from './utils/form-data.js';
import {isBlob, isURLSearchParameters, isFormData} from './utils/is.js';
import {isBlob, isURLSearchParameters} from './utils/is.js';

const INTERNALS = Symbol('Body internals');

Expand Down Expand Up @@ -50,10 +50,10 @@ export default class Body {
body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
} else if (body instanceof Stream) {
// Body is stream
} else if (isFormData(body)) {
// Body is an instance of formdata-node
boundary = `nodefetchformdataboundary${getBoundary()}`;
body = Stream.Readable.from(formDataIterator(body, boundary));
} else if (body instanceof FormData) {
// Body is FormData
body = formDataToBlob(body);
boundary = body.type.split('=')[1];
} else {
// None of the above
// coerce to string then buffer
Expand Down Expand Up @@ -105,6 +105,24 @@ export default class Body {
return buffer.slice(byteOffset, byteOffset + byteLength);
}

async formData() {
const ct = this.headers.get('content-type');

if (ct.startsWith('application/x-www-form-urlencoded')) {
const formData = new FormData();
const parameters = new URLSearchParams(await this.text());

for (const [name, value] of parameters) {
formData.append(name, value);
}

return formData;
}

const {toFormData} = await import('./utils/multipart-parser.js');
return toFormData(this.body, ct);
}

/**
* Return raw response as Blob
*
Expand Down Expand Up @@ -302,7 +320,7 @@ export const extractContentType = (body, request) => {
return null;
}

if (isFormData(body)) {
if (body instanceof FormData) {
return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
}

Expand Down Expand Up @@ -352,11 +370,6 @@ export const getTotalBytes = request => {
return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null;
}

// Body is a spec-compliant FormData
if (isFormData(body)) {
return getFormDataLength(request[INTERNALS].boundary);
}

// Body is stream
return null;
};
Expand Down
2 changes: 1 addition & 1 deletion src/response.js
Expand Up @@ -29,7 +29,7 @@ export default class Response extends Body {
const headers = new Headers(options.headers);

if (body !== null && !headers.has('Content-Type')) {
const contentType = extractContentType(body);
const contentType = extractContentType(body, this);
if (contentType) {
headers.append('Content-Type', contentType);
}
Expand Down
78 changes: 0 additions & 78 deletions src/utils/form-data.js

This file was deleted.

32 changes: 3 additions & 29 deletions src/utils/is.js
Expand Up @@ -9,8 +9,7 @@ const NAME = Symbol.toStringTag;
/**
* Check if `obj` is a URLSearchParams object
* ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143
*
* @param {*} obj
* @param {*} object - Object to check for
* @return {boolean}
*/
export const isURLSearchParameters = object => {
Expand All @@ -29,8 +28,7 @@ export const isURLSearchParameters = object => {

/**
* Check if `object` is a W3C `Blob` object (which `File` inherits from)
*
* @param {*} obj
* @param {*} object - Object to check for
* @return {boolean}
*/
export const isBlob = object => {
Expand All @@ -45,32 +43,9 @@ export const isBlob = object => {
);
};

/**
* Check if `obj` is a spec-compliant `FormData` object
*
* @param {*} object
* @return {boolean}
*/
export function isFormData(object) {
return (
typeof object === 'object' &&
typeof object.append === 'function' &&
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.getAll === 'function' &&
typeof object.delete === 'function' &&
typeof object.keys === 'function' &&
typeof object.values === 'function' &&
typeof object.entries === 'function' &&
typeof object.constructor === 'function' &&
object[NAME] === 'FormData'
);
}

/**
* Check if `obj` is an instance of AbortSignal.
*
* @param {*} obj
* @param {*} object - Object to check for
* @return {boolean}
*/
export const isAbortSignal = object => {
Expand All @@ -81,4 +56,3 @@ export const isAbortSignal = object => {
)
);
};

0 comments on commit 3944f24

Please sign in to comment.