From 73a19fff39deffc7fb40acc42005a004779e052e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 11:43:21 +0200 Subject: [PATCH 01/31] Added support for body toFormData --- docs/CHANGELOG.md | 1 + package.json | 4 +- src/body.js | 30 ++- src/response.js | 2 +- src/utils/form-data.js | 78 ------ src/utils/multipart-parser.js | 434 ++++++++++++++++++++++++++++++++++ test/form-data.js | 118 ++++----- test/main.js | 2 +- 8 files changed, 510 insertions(+), 159 deletions(-) delete mode 100644 src/utils/form-data.js create mode 100644 src/utils/multipart-parser.js diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 4081cf629..43bf89a2e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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) +- feat: Added support for `Body.formData()` ## v3.0.0 diff --git a/package.json b/package.json index 6252c2125..cfce5caa8 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "coveralls": "^3.1.0", "delay": "^5.0.0", "form-data": "^4.0.0", - "formdata-node": "^3.5.4", "mocha": "^8.3.2", "p-timeout": "^5.0.0", "tsd": "^0.14.0", @@ -63,7 +62,8 @@ }, "dependencies": { "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^3.1.2" + "fetch-blob": "^3.1.2", + "formdata-polyfill": "^4.0.7" }, "tsd": { "cwd": "@types", diff --git a/src/body.js b/src/body.js index 82991eff8..cd5013d95 100644 --- a/src/body.js +++ b/src/body.js @@ -9,10 +9,10 @@ import Stream, {PassThrough} from 'stream'; import {types, deprecate} from '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'; const INTERNALS = Symbol('Body internals'); @@ -52,8 +52,8 @@ export default class Body { // Body is stream } else if (isFormData(body)) { // Body is an instance of formdata-node - boundary = `nodefetchformdataboundary${getBoundary()}`; - body = Stream.Readable.from(formDataIterator(body, boundary)); + body = formDataToBlob(body); + boundary = body.type.split('=')[1]; } else { // None of the above // coerce to string then buffer @@ -105,6 +105,25 @@ 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} = await import('formdata-polyfill/esm.min.js') + const fd = new FormData(); + const parameters = new URLSearchParams(await this.text()); + + for (const [name, value] of parameters) { + fd.append(name, value); + } + + return fd; + } + + const {toFormData} = await import('./utils/multipart-parser.js') + return toFormData(this.body, ct); + } + /** * Return raw response as Blob * @@ -352,11 +371,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; }; diff --git a/src/response.js b/src/response.js index af820d137..f138c7be7 100644 --- a/src/response.js +++ b/src/response.js @@ -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); } diff --git a/src/utils/form-data.js b/src/utils/form-data.js deleted file mode 100644 index 7b66a8a57..000000000 --- a/src/utils/form-data.js +++ /dev/null @@ -1,78 +0,0 @@ -import {randomBytes} from 'crypto'; - -import {isBlob} from './is.js'; - -const carriage = '\r\n'; -const dashes = '-'.repeat(2); -const carriageLength = Buffer.byteLength(carriage); - -/** - * @param {string} boundary - */ -const getFooter = boundary => `${dashes}${boundary}${dashes}${carriage.repeat(2)}`; - -/** - * @param {string} boundary - * @param {string} name - * @param {*} field - * - * @return {string} - */ -function getHeader(boundary, name, field) { - let header = ''; - - header += `${dashes}${boundary}${carriage}`; - header += `Content-Disposition: form-data; name="${name}"`; - - if (isBlob(field)) { - header += `; filename="${field.name}"${carriage}`; - header += `Content-Type: ${field.type || 'application/octet-stream'}`; - } - - return `${header}${carriage.repeat(2)}`; -} - -/** - * @return {string} - */ -export const getBoundary = () => randomBytes(8).toString('hex'); - -/** - * @param {FormData} form - * @param {string} boundary - */ -export async function * formDataIterator(form, boundary) { - for (const [name, value] of form) { - yield getHeader(boundary, name, value); - - if (isBlob(value)) { - yield * value.stream(); - } else { - yield value; - } - - yield carriage; - } - - yield getFooter(boundary); -} - -/** - * @param {FormData} form - * @param {string} boundary - */ -export function getFormDataLength(form, boundary) { - let length = 0; - - for (const [name, value] of form) { - length += Buffer.byteLength(getHeader(boundary, name, value)); - - length += isBlob(value) ? value.size : Buffer.byteLength(String(value)); - - length += carriageLength; - } - - length += Buffer.byteLength(getFooter(boundary)); - - return length; -} diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js new file mode 100644 index 000000000..b2fdc05a1 --- /dev/null +++ b/src/utils/multipart-parser.js @@ -0,0 +1,434 @@ +import {File} from 'fetch-blob/from.js'; +import {FormData} from 'formdata-polyfill/esm.min.js'; + +let s = 0; +const S = { + PARSER_UNINITIALIZED: s++, + START: s++, + START_BOUNDARY: s++, + HEADER_FIELD_START: s++, + HEADER_FIELD: s++, + HEADER_VALUE_START: s++, + HEADER_VALUE: s++, + HEADER_VALUE_ALMOST_DONE: s++, + HEADERS_ALMOST_DONE: s++, + PART_DATA_START: s++, + PART_DATA: s++, + PART_END: s++, + END: s++ +}; + +let f = 1; +const F = { + PART_BOUNDARY: f, + LAST_BOUNDARY: f *= 2 +}; + +const LF = 10; +const CR = 13; +const SPACE = 32; +const HYPHEN = 45; +const COLON = 58; +const A = 97; +const Z = 122; + +const lower = function (c) { + return c | 0x20; +}; + +class MultipartParser { + constructor(string) { + this.index = null; + this.flags = 0; + + this.boundaryChars = {}; + + string = '\r\n--' + string; + const ui8a = new Uint8Array(string.length); + for (let i = 0; i < string.length; i++) { + ui8a[i] = string.charCodeAt(i); + this.boundaryChars[ui8a[i]] = true; + } + + this.boundary = ui8a; + this.lookbehind = new Uint8Array(this.boundary.length + 8); + this.state = S.START; + } + + write(ui8a) { + const self = this; + let i = 0; + const length_ = ui8a.length; + let previousIndex = this.index; + let {lookbehind, boundary, boundaryChars, index, state, flags} = this; + const boundaryLength = this.boundary.length; + const boundaryEnd = boundaryLength - 1; + const bufferLength = ui8a.length; + let c; + let cl; + + const mark = name => { + this[name + 'Mark'] = i; + }; + + const clear = name => { + delete self[name + 'Mark']; + }; + + const callback = function (name, start, end, ui8a) { + if (start !== undefined && start === end) { + return; + } + + const callbackSymbol = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1); + if (callbackSymbol in self) { + self[callbackSymbol](ui8a && ui8a.subarray(start, end)); + } + } + + const dataCallback = function (name, clear) { + const markSymbol = name + 'Mark'; + if (!(markSymbol in self)) { + return; + } + + if (!clear) { + callback(name, self[markSymbol], ui8a.length, ui8a); + self[markSymbol] = 0; + } else { + callback(name, self[markSymbol], i, ui8a); + delete self[markSymbol]; + } + }; + + for (i = 0; i < length_; i++) { + c = ui8a[i]; + + switch (state) { + case S.PARSER_UNINITIALIZED: + return i; + case S.START: + index = 0; + state = S.START_BOUNDARY; + case S.START_BOUNDARY: + if (index === boundary.length - 2) { + if (c === HYPHEN) { + flags |= F.LAST_BOUNDARY; + } else if (c !== CR) { + return i; + } + + index++; + break; + } else if (index - 1 === boundary.length - 2) { + if (flags & F.LAST_BOUNDARY && c === HYPHEN) { + callback('end'); + state = S.END; + flags = 0; + } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { + index = 0; + callback('partBegin'); + state = S.HEADER_FIELD_START; + } else { + return i; + } + + break; + } + + if (c !== boundary[index + 2]) { + index = -2; + } + + if (c === boundary[index + 2]) { + index++; + } + + break; + case S.HEADER_FIELD_START: + state = S.HEADER_FIELD; + mark('headerField'); + index = 0; + case S.HEADER_FIELD: + if (c === CR) { + clear('headerField'); + state = S.HEADERS_ALMOST_DONE; + break; + } + + index++; + if (c === HYPHEN) { + break; + } + + if (c === COLON) { + if (index === 1) { + // empty header field + return i; + } + + dataCallback('headerField', true); + state = S.HEADER_VALUE_START; + break; + } + + cl = lower(c); + if (cl < A || cl > Z) { + return i; + } + + break; + case S.HEADER_VALUE_START: + if (c === SPACE) { + break; + } + + mark('headerValue'); + state = S.HEADER_VALUE; + case S.HEADER_VALUE: + if (c === CR) { + dataCallback('headerValue', true); + callback('headerEnd'); + state = S.HEADER_VALUE_ALMOST_DONE; + } + + break; + case S.HEADER_VALUE_ALMOST_DONE: + if (c !== LF) { + return i; + } + + state = S.HEADER_FIELD_START; + break; + case S.HEADERS_ALMOST_DONE: + if (c !== LF) { + return i; + } + + callback('headersEnd'); + state = S.PART_DATA_START; + break; + case S.PART_DATA_START: + state = S.PART_DATA; + mark('partData'); + case S.PART_DATA: + previousIndex = index; + + if (index === 0) { + // boyer-moore derrived algorithm to safely skip non-boundary data + i += boundaryEnd; + while (i < bufferLength && !(ui8a[i] in boundaryChars)) { + i += boundaryLength; + } + + i -= boundaryEnd; + c = ui8a[i]; + } + + if (index < boundary.length) { + if (boundary[index] === c) { + if (index === 0) { + dataCallback('partData', true); + } + + index++; + } else { + index = 0; + } + } else if (index === boundary.length) { + index++; + if (c === CR) { + // CR = part boundary + flags |= F.PART_BOUNDARY; + } else if (c === HYPHEN) { + // HYPHEN = end boundary + flags |= F.LAST_BOUNDARY; + } else { + index = 0; + } + } else if (index - 1 === boundary.length) { + if (flags & F.PART_BOUNDARY) { + index = 0; + if (c === LF) { + // unset the PART_BOUNDARY flag + flags &= ~F.PART_BOUNDARY; + callback('partEnd'); + callback('partBegin'); + state = S.HEADER_FIELD_START; + break; + } + } else if (flags & F.LAST_BOUNDARY) { + if (c === HYPHEN) { + callback('partEnd'); + callback('end'); + state = S.END; + flags = 0; + } else { + index = 0; + } + } else { + index = 0; + } + } + + if (index > 0) { + // when matching a possible boundary, keep a lookbehind reference + // in case it turns out to be a false lead + lookbehind[index - 1] = c; + } else if (previousIndex > 0) { + // if our boundary turned out to be rubbish, the captured lookbehind + // belongs to partData + const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); + callback('partData', 0, previousIndex, _lookbehind); + previousIndex = 0; + mark('partData'); + + // reconsider the current character even so it interrupted the sequence + // it could be the beginning of a new sequence + i--; + } + + break; + case S.END: + break; + default: + return i; + } + } + + dataCallback('headerField'); + dataCallback('headerValue'); + dataCallback('partData'); + + this.index = index; + this.state = state; + this.flags = flags; + + return length_; + } + + end() { + function callback(self, name) { + const callbackSymbol = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1); + if (callbackSymbol in self) { + self[callbackSymbol](); + } + } + + if ((this.state === S.HEADER_FIELD_START && this.index === 0) || + (this.state === S.PART_DATA && this.index === this.boundary.length)) { + callback(this, 'partEnd'); + callback(this, 'end'); + } else if (this.state !== S.END) { + return new Error('MultipartParser.end(): stream ended unexpectedly'); + } + } +} + +function _fileName(headerValue) { + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); + if (!m) { + return; + } + + const match = m[2] || m[3] || ''; + let filename = match.slice(match.lastIndexOf('\\') + 1); + filename = filename.replace(/%22/g, '"'); + filename = filename.replace(/&#(\d{4});/g, (m, code) => { + return String.fromCharCode(code); + }); + return filename; +} + +export async function toFormData(Body, ct) { + let parser; + if (/multipart/i.test(ct)) { + const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); + if (m) { + parser = new MultipartParser(m[1] || m[2]); + + let headerField; + let headerValue; + const entryChunks = []; + let entryValue; + let entryName; + let contentType; + let filename; + const fd = new FormData(); + + function onPartData(ui8a) { + entryValue += decoder.decode(ui8a, {stream: true}); + } + + function appendToFile(ui8a) { + entryChunks.push(ui8a); + } + + function appendFileToFormData() { + const file = new File(entryChunks, filename, {type: contentType}); + fd.append(entryName, file); + } + + function appendEntryToFormData() { + fd.append(entryName, entryValue); + } + + const decoder = new TextDecoder('utf-8'); + decoder.decode(); + + parser.onPartBegin = function () { + parser.onPartData = onPartData; + parser.onPartEnd = appendEntryToFormData; + + headerField = ''; + headerValue = ''; + entryValue = ''; + entryName = ''; + contentType = ''; + filename = null; + entryChunks.length = 0; + }; + + parser.onHeaderField = function (ui8a) { + headerField += decoder.decode(ui8a, {stream: true}); + }; + + parser.onHeaderValue = function (ui8a) { + headerValue += decoder.decode(ui8a, {stream: true}); + }; + + parser.onHeaderEnd = function () { + headerValue += decoder.decode(); + headerField = headerField.toLowerCase(); + + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); + + if (headerField === 'content-disposition') { + if (m) { + entryName = m[2] || m[3] || ''; + } + + filename = _fileName(headerValue); + parser.onPartData = appendToFile; + parser.onPartEnd = appendFileToFormData; + } else if (headerField === 'content-type') { + contentType = headerValue; + } + }; + + for await (const chunk of Body) { + parser.write(chunk); + } + + parser.end(); + + return fd; + } + + throw new TypeError('no or bad content-type header, no multipart boundary'); + } else { + throw new TypeError('Failed to fetch'); + } +} diff --git a/test/form-data.js b/test/form-data.js index f7f289197..d4d588e75 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -1,103 +1,83 @@ -import {FormData} from 'formdata-node'; -import Blob from 'fetch-blob'; - +import {FormData} from 'formdata-polyfill/esm.min.js'; +import {Blob} from 'fetch-blob/from.js'; import chai from 'chai'; - -import {getFormDataLength, getBoundary, formDataIterator} from '../src/utils/form-data.js'; -import read from './utils/read-stream.js'; +import {Request, Response} from '../src/index.js'; const {expect} = chai; -const carriage = '\r\n'; - -const getFooter = boundary => `--${boundary}--${carriage.repeat(2)}`; - describe('FormData', () => { - it('should return a length for empty form-data', () => { - const form = new FormData(); - const boundary = getBoundary(); + it('Consume empty URLSearchParams as FormData', async () => { + const res = new Response(new URLSearchParams()); + const fd = await res.formData(); - expect(getFormDataLength(form, boundary)).to.be.equal(Buffer.byteLength(getFooter(boundary))); + expect(fd).to.be.instanceOf(FormData); }); - it('should add a Blob field\'s size to the FormData length', () => { - const form = new FormData(); - const boundary = getBoundary(); + it('Consume empty URLSearchParams as FormData', async () => { + const req = new Request('about:blank', { + method: 'POST', + body: new URLSearchParams() + }); + const fd = await req.formData(); - const string = 'Hello, world!'; - const expected = Buffer.byteLength( - `--${boundary}${carriage}` + - `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + - string + - `${carriage}${getFooter(boundary)}` - ); + expect(fd).to.be.instanceOf(FormData); + }); - form.set('field', string); + it('Consume empty response.formData() as FormData', async () => { + const res = new Response(new FormData()); + const fd = await res.formData(); - expect(getFormDataLength(form, boundary)).to.be.equal(expected); + expect(fd).to.be.instanceOf(FormData); }); - it('should return a length for a Blob field', () => { - const form = new FormData(); - const boundary = getBoundary(); - - const blob = new Blob(['Hello, world!'], {type: 'text/plain'}); + it('Consume empty response.formData() as FormData', async () => { + const res = new Response(new FormData()); + const fd = await res.formData(); - form.set('blob', blob); + expect(fd).to.be.instanceOf(FormData); + }); - const expected = blob.size + Buffer.byteLength( - `--${boundary}${carriage}` + - 'Content-Disposition: form-data; name="blob"; ' + - `filename="blob"${carriage}Content-Type: text/plain` + - `${carriage.repeat(3)}${getFooter(boundary)}` - ); + it('Consume empty request.formData() as FormData', async () => { + const req = new Request('about:blank', { + method: 'POST', + body: new FormData() + }); + const fd = await req.formData(); - expect(getFormDataLength(form, boundary)).to.be.equal(expected); + expect(fd).to.be.instanceOf(FormData); }); - it('should create a body from empty form-data', async () => { - const form = new FormData(); - const boundary = getBoundary(); + it('Consume URLSearchParams with entries as FormData', async () => { + const res = new Response(new URLSearchParams({foo: 'bar'})); + const fd = await res.formData(); - expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(getFooter(boundary)); + expect(fd.get('foo')).to.be.equal('bar'); }); - it('should set default content-type', async () => { + it('should return a length for empty form-data', async () => { const form = new FormData(); - const boundary = getBoundary(); + const ab = await new Request('http://a', { + method: 'post', + body: form + }).arrayBuffer(); - form.set('blob', new Blob([])); - - expect(String(await read(formDataIterator(form, boundary)))).to.contain('Content-Type: application/octet-stream'); + expect(ab.byteLength).to.be.greaterThan(30); }); - it('should create a body with a FormData field', async () => { + it('should add a Blob field\'s size to the FormData length', async () => { const form = new FormData(); - const boundary = getBoundary(); - const string = 'Hello, World!'; - + const string = 'Hello, world!'; form.set('field', string); - - const expected = `--${boundary}${carriage}` + - `Content-Disposition: form-data; name="field"${carriage.repeat(2)}` + - string + - `${carriage}${getFooter(boundary)}`; - - expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); + const text = await new Request('about:blank', {method: 'POST', body: form}).text(); }); - it('should create a body with a FormData Blob field', async () => { + it('should return a length for a Blob field', async () => { const form = new FormData(); - const boundary = getBoundary(); - - const expected = `--${boundary}${carriage}` + - 'Content-Disposition: form-data; name="blob"; ' + - `filename="blob"${carriage}Content-Type: text/plain${carriage.repeat(2)}` + - 'Hello, World!' + - `${carriage}${getFooter(boundary)}`; + const blob = new Blob(['Hello, world!'], {type: 'text/plain'}); + form.set('blob', blob); - form.set('blob', new Blob(['Hello, World!'], {type: 'text/plain'})); + const fd = await new Response(form).formData(); - expect(String(await read(formDataIterator(form, boundary)))).to.be.equal(expected); + expect(fd.get('blob').size).to.equal(13); }); }); diff --git a/test/main.js b/test/main.js index 77d352ba4..93171ab57 100644 --- a/test/main.js +++ b/test/main.js @@ -12,7 +12,7 @@ import chaiPromised from 'chai-as-promised'; import chaiIterator from 'chai-iterator'; import chaiString from 'chai-string'; import FormData from 'form-data'; -import {FormData as FormDataNode} from 'formdata-node'; +import {FormData as FormDataNode} from 'formdata-polyfill/esm.min.js'; import delay from 'delay'; import AbortControllerMysticatea from 'abort-controller'; import abortControllerPolyfill from 'abortcontroller-polyfill/dist/abortcontroller.js'; From 9f6c29835afab69b3915190ac512bbcc3b8c512e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 11:51:37 +0200 Subject: [PATCH 02/31] added formData method --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index aa2e2af63..8711dad96 100644 --- a/README.md +++ b/README.md @@ -712,6 +712,8 @@ A boolean property for if this body has been consumed. Per the specs, a consumed #### body.arrayBuffer() +#### body.formData() + #### body.blob() #### body.json() From fe0de91b313365f5b877896a039ccb17d57dfaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 11:51:55 +0200 Subject: [PATCH 03/31] remove discouraged buffer --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index 8711dad96..f01b6b573 100644 --- a/README.md +++ b/README.md @@ -726,14 +726,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() - -_(node-fetch extension)_ - -- Returns: `Promise` - -Consume the body and return a promise that will resolve to a Buffer. - ### Class: FetchError From 78f32f34d755462eab219641a5bb93091270d7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 14:27:21 +0200 Subject: [PATCH 04/31] lint + fix --- package.json | 2 ++ src/body.js | 3 +-- src/utils/multipart-parser.js | 50 +++++++++++++++++------------------ test/form-data.js | 15 ++++++----- 4 files changed, 36 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index cfce5caa8..d6871c4fc 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,8 @@ "unicorn/numeric-separators-style": 0, "unicorn/explicit-length-check": 0, "capitalized-comments": 0, + "node/no-unsupported-features/es-syntax": 0, + "no-fallthrough": 0, "@typescript-eslint/member-ordering": 0 }, "overrides": [ diff --git a/src/body.js b/src/body.js index cd5013d95..1c19c8dea 100644 --- a/src/body.js +++ b/src/body.js @@ -109,7 +109,6 @@ export default class Body { const ct = this.headers.get('content-type'); if (ct.startsWith('application/x-www-form-urlencoded')) { - const {FormData} = await import('formdata-polyfill/esm.min.js') const fd = new FormData(); const parameters = new URLSearchParams(await this.text()); @@ -120,7 +119,7 @@ export default class Body { return fd; } - const {toFormData} = await import('./utils/multipart-parser.js') + const {toFormData} = await import('./utils/multipart-parser.js'); return toFormData(this.body, ct); } diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index b2fdc05a1..e40084efd 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -56,7 +56,6 @@ class MultipartParser { } write(ui8a) { - const self = this; let i = 0; const length_ = ui8a.length; let previousIndex = this.index; @@ -72,32 +71,32 @@ class MultipartParser { }; const clear = name => { - delete self[name + 'Mark']; + delete this[name + 'Mark']; }; - const callback = function (name, start, end, ui8a) { + const callback = (name, start, end, ui8a) => { if (start !== undefined && start === end) { return; } const callbackSymbol = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1); - if (callbackSymbol in self) { - self[callbackSymbol](ui8a && ui8a.subarray(start, end)); + if (callbackSymbol in this) { + this[callbackSymbol](ui8a && ui8a.subarray(start, end)); } - } + }; - const dataCallback = function (name, clear) { + const dataCallback = (name, clear) => { const markSymbol = name + 'Mark'; - if (!(markSymbol in self)) { + if (!(markSymbol in this)) { return; } - if (!clear) { - callback(name, self[markSymbol], ui8a.length, ui8a); - self[markSymbol] = 0; + if (clear) { + callback(name, this[markSymbol], i, ui8a); + delete this[markSymbol]; } else { - callback(name, self[markSymbol], i, ui8a); - delete self[markSymbol]; + callback(name, this[markSymbol], ui8a.length, ui8a); + this[markSymbol] = 0; } }; @@ -350,29 +349,29 @@ export async function toFormData(Body, ct) { let headerField; let headerValue; - const entryChunks = []; let entryValue; let entryName; let contentType; let filename; + const entryChunks = []; const fd = new FormData(); - function onPartData(ui8a) { + const onPartData = ui8a => { entryValue += decoder.decode(ui8a, {stream: true}); - } + }; - function appendToFile(ui8a) { + const appendToFile = ui8a => { entryChunks.push(ui8a); - } + }; - function appendFileToFormData() { + const appendFileToFormData = () => { const file = new File(entryChunks, filename, {type: contentType}); fd.append(entryName, file); - } + }; - function appendEntryToFormData() { + const appendEntryToFormData = () => { fd.append(entryName, entryValue); - } + }; const decoder = new TextDecoder('utf-8'); decoder.decode(); @@ -410,9 +409,10 @@ export async function toFormData(Body, ct) { entryName = m[2] || m[3] || ''; } - filename = _fileName(headerValue); - parser.onPartData = appendToFile; - parser.onPartEnd = appendFileToFormData; + if (filename = _fileName(headerValue)) { + parser.onPartData = appendToFile; + parser.onPartEnd = appendFileToFormData; + } } else if (headerField === 'content-type') { contentType = headerValue; } diff --git a/test/form-data.js b/test/form-data.js index d4d588e75..4a577621d 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -8,7 +8,7 @@ const {expect} = chai; describe('FormData', () => { it('Consume empty URLSearchParams as FormData', async () => { const res = new Response(new URLSearchParams()); - const fd = await res.formData(); + const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); @@ -18,21 +18,21 @@ describe('FormData', () => { method: 'POST', body: new URLSearchParams() }); - const fd = await req.formData(); + const fd = await req.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty response.formData() as FormData', async () => { const res = new Response(new FormData()); - const fd = await res.formData(); + const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty response.formData() as FormData', async () => { const res = new Response(new FormData()); - const fd = await res.formData(); + const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); @@ -42,14 +42,14 @@ describe('FormData', () => { method: 'POST', body: new FormData() }); - const fd = await req.formData(); + const fd = await req.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume URLSearchParams with entries as FormData', async () => { const res = new Response(new URLSearchParams({foo: 'bar'})); - const fd = await res.formData(); + const fd = await res.formData(); expect(fd.get('foo')).to.be.equal('bar'); }); @@ -68,7 +68,8 @@ describe('FormData', () => { const form = new FormData(); const string = 'Hello, world!'; form.set('field', string); - const text = await new Request('about:blank', {method: 'POST', body: form}).text(); + const fd = await new Request('about:blank', {method: 'POST', body: form}).formData(); + expect(fd.get('field')).to.equal(string); }); it('should return a length for a Blob field', async () => { From 0f6e10d912166620aefcf9328efd29f12fa6d6e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 14:29:21 +0200 Subject: [PATCH 05/31] xo fix --- src/utils/multipart-parser.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index e40084efd..8416da659 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -409,7 +409,9 @@ export async function toFormData(Body, ct) { entryName = m[2] || m[3] || ''; } - if (filename = _fileName(headerValue)) { + filename = _fileName(headerValue); + + if (filename) { parser.onPartData = appendToFile; parser.onPartEnd = appendFileToFormData; } From 7b514a30b411d3eda7783174a34d8c8e8c5f85ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 15:12:14 +0200 Subject: [PATCH 06/31] latest v --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6871c4fc..604dcee0b 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,7 @@ "dependencies": { "data-uri-to-buffer": "^3.0.1", "fetch-blob": "^3.1.2", - "formdata-polyfill": "^4.0.7" + "formdata-polyfill": "^4.0.8" }, "tsd": { "cwd": "@types", From 0f2f9bda685a0e259be50f4ba4aef9f40d38ab2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sun, 26 Sep 2021 15:25:07 +0200 Subject: [PATCH 07/31] add type support --- @types/index.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/@types/index.d.ts b/@types/index.d.ts index 9854261f2..50553ddc5 100644 --- a/@types/index.d.ts +++ b/@types/index.d.ts @@ -95,6 +95,7 @@ export type BodyInit = | Blob | Buffer | URLSearchParams + | FormData | NodeJS.ReadableStream | string; declare class BodyMixin { @@ -106,6 +107,7 @@ declare class BodyMixin { buffer(): Promise; arrayBuffer(): Promise; + formData(): Promise; blob(): Promise; json(): Promise; text(): Promise; From 57ce9ebc08017b4197441b8add0070cf542f217b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:12:31 +0200 Subject: [PATCH 08/31] use // fallsthrough instead --- package.json | 1 - src/utils/multipart-parser.js | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 604dcee0b..639a64efe 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "unicorn/explicit-length-check": 0, "capitalized-comments": 0, "node/no-unsupported-features/es-syntax": 0, - "no-fallthrough": 0, "@typescript-eslint/member-ordering": 0 }, "overrides": [ diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 8416da659..de40c308d 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -109,6 +109,7 @@ class MultipartParser { case S.START: index = 0; state = S.START_BOUNDARY; + // fallsthrough case S.START_BOUNDARY: if (index === boundary.length - 2) { if (c === HYPHEN) { @@ -148,6 +149,7 @@ class MultipartParser { state = S.HEADER_FIELD; mark('headerField'); index = 0; + // fallsthrough case S.HEADER_FIELD: if (c === CR) { clear('headerField'); @@ -184,6 +186,7 @@ class MultipartParser { mark('headerValue'); state = S.HEADER_VALUE; + // fallsthrough case S.HEADER_VALUE: if (c === CR) { dataCallback('headerValue', true); @@ -210,6 +213,7 @@ class MultipartParser { case S.PART_DATA_START: state = S.PART_DATA; mark('partData'); + // fallsthrough case S.PART_DATA: previousIndex = index; From 3a9c1bd54a9d2f85f028f7716238e3faf4c2909d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:18:28 +0200 Subject: [PATCH 09/31] hoisted the throw/if conditions --- src/utils/multipart-parser.js | 166 +++++++++++++++++----------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index de40c308d..00d25c8e4 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -345,96 +345,96 @@ function _fileName(headerValue) { } export async function toFormData(Body, ct) { - let parser; - if (/multipart/i.test(ct)) { - const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); - if (m) { - parser = new MultipartParser(m[1] || m[2]); - - let headerField; - let headerValue; - let entryValue; - let entryName; - let contentType; - let filename; - const entryChunks = []; - const fd = new FormData(); - - const onPartData = ui8a => { - entryValue += decoder.decode(ui8a, {stream: true}); - }; - - const appendToFile = ui8a => { - entryChunks.push(ui8a); - }; - - const appendFileToFormData = () => { - const file = new File(entryChunks, filename, {type: contentType}); - fd.append(entryName, file); - }; - - const appendEntryToFormData = () => { - fd.append(entryName, entryValue); - }; - - const decoder = new TextDecoder('utf-8'); - decoder.decode(); - - parser.onPartBegin = function () { - parser.onPartData = onPartData; - parser.onPartEnd = appendEntryToFormData; - - headerField = ''; - headerValue = ''; - entryValue = ''; - entryName = ''; - contentType = ''; - filename = null; - entryChunks.length = 0; - }; - - parser.onHeaderField = function (ui8a) { - headerField += decoder.decode(ui8a, {stream: true}); - }; - - parser.onHeaderValue = function (ui8a) { - headerValue += decoder.decode(ui8a, {stream: true}); - }; - - parser.onHeaderEnd = function () { - headerValue += decoder.decode(); - headerField = headerField.toLowerCase(); - - // matches either a quoted-string or a token (RFC 2616 section 19.5.1) - const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); - - if (headerField === 'content-disposition') { - if (m) { - entryName = m[2] || m[3] || ''; - } + if (!/multipart/i.test(ct)) { + throw new TypeError('Failed to fetch'); + } - filename = _fileName(headerValue); + const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); - if (filename) { - parser.onPartData = appendToFile; - parser.onPartEnd = appendFileToFormData; - } - } else if (headerField === 'content-type') { - contentType = headerValue; - } - }; + if (!m) { + throw new TypeError('no or bad content-type header, no multipart boundary'); + } - for await (const chunk of Body) { - parser.write(chunk); + const parser = new MultipartParser(m[1] || m[2]); + + let headerField; + let headerValue; + let entryValue; + let entryName; + let contentType; + let filename; + const entryChunks = []; + const fd = new FormData(); + + const onPartData = ui8a => { + entryValue += decoder.decode(ui8a, {stream: true}); + }; + + const appendToFile = ui8a => { + entryChunks.push(ui8a); + }; + + const appendFileToFormData = () => { + const file = new File(entryChunks, filename, {type: contentType}); + fd.append(entryName, file); + }; + + const appendEntryToFormData = () => { + fd.append(entryName, entryValue); + }; + + const decoder = new TextDecoder('utf-8'); + decoder.decode(); + + parser.onPartBegin = function () { + parser.onPartData = onPartData; + parser.onPartEnd = appendEntryToFormData; + + headerField = ''; + headerValue = ''; + entryValue = ''; + entryName = ''; + contentType = ''; + filename = null; + entryChunks.length = 0; + }; + + parser.onHeaderField = function (ui8a) { + headerField += decoder.decode(ui8a, {stream: true}); + }; + + parser.onHeaderValue = function (ui8a) { + headerValue += decoder.decode(ui8a, {stream: true}); + }; + + parser.onHeaderEnd = function () { + headerValue += decoder.decode(); + headerField = headerField.toLowerCase(); + + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); + + if (headerField === 'content-disposition') { + if (m) { + entryName = m[2] || m[3] || ''; } - parser.end(); + filename = _fileName(headerValue); - return fd; + if (filename) { + parser.onPartData = appendToFile; + parser.onPartEnd = appendFileToFormData; + } + } else if (headerField === 'content-type') { + contentType = headerValue; } + }; - throw new TypeError('no or bad content-type header, no multipart boundary'); - } else { - throw new TypeError('Failed to fetch'); + for await (const chunk of Body) { + parser.write(chunk); } + + parser.end(); + + return fd; } From 560320d312c095213acaf42483625c06c0e4e981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:26:35 +0200 Subject: [PATCH 10/31] update comment --- src/body.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/body.js b/src/body.js index 1c19c8dea..9e4984ac7 100644 --- a/src/body.js +++ b/src/body.js @@ -51,7 +51,7 @@ export default class Body { } else if (body instanceof Stream) { // Body is stream } else if (isFormData(body)) { - // Body is an instance of formdata-node + // Body is an spec compatible FormData body = formDataToBlob(body); boundary = body.type.split('=')[1]; } else { From 7384b52efaf44a119939af9904ceb7165c802574 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:27:58 +0200 Subject: [PATCH 11/31] rm unused PARSER_UNINITIALIZED --- src/utils/multipart-parser.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 00d25c8e4..5c9859699 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -3,7 +3,6 @@ import {FormData} from 'formdata-polyfill/esm.min.js'; let s = 0; const S = { - PARSER_UNINITIALIZED: s++, START: s++, START_BOUNDARY: s++, HEADER_FIELD_START: s++, @@ -104,8 +103,6 @@ class MultipartParser { c = ui8a[i]; switch (state) { - case S.PARSER_UNINITIALIZED: - return i; case S.START: index = 0; state = S.START_BOUNDARY; From 58005c3ba039700c18c01845795de13550a9415f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:28:26 +0200 Subject: [PATCH 12/31] rm PART_END --- src/utils/multipart-parser.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 5c9859699..b9f00e6fd 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -13,7 +13,6 @@ const S = { HEADERS_ALMOST_DONE: s++, PART_DATA_START: s++, PART_DATA: s++, - PART_END: s++, END: s++ }; From d472c94e5e5ef74a256f85566e80c15b14dc9193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:43:27 +0200 Subject: [PATCH 13/31] added comment/suggestion --- src/utils/multipart-parser.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index b9f00e6fd..cdb4ea8f7 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -291,6 +291,7 @@ class MultipartParser { case S.END: break; default: + console.info(`Unexpected state entered: ${state}`); return i; } } @@ -299,6 +300,7 @@ class MultipartParser { dataCallback('headerValue'); dataCallback('partData'); + // Update properties for the next call this.index = index; this.state = state; this.flags = flags; From 8c869e5baf2d3a69ba15eea6d294657838242161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Tue, 28 Sep 2021 12:43:37 +0200 Subject: [PATCH 14/31] throw instead of returning --- src/utils/multipart-parser.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index cdb4ea8f7..f962be143 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -321,7 +321,7 @@ class MultipartParser { callback(this, 'partEnd'); callback(this, 'end'); } else if (this.state !== S.END) { - return new Error('MultipartParser.end(): stream ended unexpectedly'); + throw new Error('MultipartParser.end(): stream ended unexpectedly'); } } } From b06dd352057b2fff9023574af734c029fa8865a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Wed, 29 Sep 2021 11:13:56 +0200 Subject: [PATCH 15/31] define every function as noop --- src/utils/multipart-parser.js | 93 ++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index f962be143..4be9dfca9 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -34,11 +34,25 @@ const lower = function (c) { return c | 0x20; }; +const noop = () => {}; + class MultipartParser { + /** + * @param {string} string + */ constructor(string) { this.index = null; this.flags = 0; + this.onEnd = noop; + this.onHeaderEnd = noop; + this.onHeaderField = noop; + this.onHeadersEnd = noop; + this.onHeaderValue = noop; + this.onPartBegin = noop; + this.onPartData = noop; + this.onPartEnd = noop; + this.boundaryChars = {}; string = '\r\n--' + string; @@ -53,6 +67,9 @@ class MultipartParser { this.state = S.START; } + /** + * @param {Uint8Array} ui8a + */ write(ui8a) { let i = 0; const length_ = ui8a.length; @@ -72,13 +89,8 @@ class MultipartParser { delete this[name + 'Mark']; }; - const callback = (name, start, end, ui8a) => { - if (start !== undefined && start === end) { - return; - } - - const callbackSymbol = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1); - if (callbackSymbol in this) { + const callback = (callbackSymbol, start, end, ui8a) => { + if (start === undefined || start !== end) { this[callbackSymbol](ui8a && ui8a.subarray(start, end)); } }; @@ -111,22 +123,22 @@ class MultipartParser { if (c === HYPHEN) { flags |= F.LAST_BOUNDARY; } else if (c !== CR) { - return i; + return; } index++; break; } else if (index - 1 === boundary.length - 2) { if (flags & F.LAST_BOUNDARY && c === HYPHEN) { - callback('end'); + callback('onEnd'); state = S.END; flags = 0; } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { index = 0; - callback('partBegin'); + callback('onPartBegin'); state = S.HEADER_FIELD_START; } else { - return i; + return; } break; @@ -143,12 +155,12 @@ class MultipartParser { break; case S.HEADER_FIELD_START: state = S.HEADER_FIELD; - mark('headerField'); + mark('onHeaderField'); index = 0; // fallsthrough case S.HEADER_FIELD: if (c === CR) { - clear('headerField'); + clear('onHeaderField'); state = S.HEADERS_ALMOST_DONE; break; } @@ -161,17 +173,17 @@ class MultipartParser { if (c === COLON) { if (index === 1) { // empty header field - return i; + return; } - dataCallback('headerField', true); + dataCallback('onHeaderField', true); state = S.HEADER_VALUE_START; break; } cl = lower(c); if (cl < A || cl > Z) { - return i; + return; } break; @@ -180,35 +192,35 @@ class MultipartParser { break; } - mark('headerValue'); + mark('onHeaderValue'); state = S.HEADER_VALUE; // fallsthrough case S.HEADER_VALUE: if (c === CR) { - dataCallback('headerValue', true); - callback('headerEnd'); + dataCallback('onHeaderValue', true); + callback('onHeaderEnd'); state = S.HEADER_VALUE_ALMOST_DONE; } break; case S.HEADER_VALUE_ALMOST_DONE: if (c !== LF) { - return i; + return; } state = S.HEADER_FIELD_START; break; case S.HEADERS_ALMOST_DONE: if (c !== LF) { - return i; + return; } - callback('headersEnd'); + callback('onHeadersEnd'); state = S.PART_DATA_START; break; case S.PART_DATA_START: state = S.PART_DATA; - mark('partData'); + mark('onPartData'); // fallsthrough case S.PART_DATA: previousIndex = index; @@ -227,7 +239,7 @@ class MultipartParser { if (index < boundary.length) { if (boundary[index] === c) { if (index === 0) { - dataCallback('partData', true); + dataCallback('onPartData', true); } index++; @@ -251,15 +263,15 @@ class MultipartParser { if (c === LF) { // unset the PART_BOUNDARY flag flags &= ~F.PART_BOUNDARY; - callback('partEnd'); - callback('partBegin'); + callback('onPartEnd'); + callback('onPartBegin'); state = S.HEADER_FIELD_START; break; } } else if (flags & F.LAST_BOUNDARY) { if (c === HYPHEN) { - callback('partEnd'); - callback('end'); + callback('onPartEnd'); + callback('onEnd'); state = S.END; flags = 0; } else { @@ -278,9 +290,9 @@ class MultipartParser { // if our boundary turned out to be rubbish, the captured lookbehind // belongs to partData const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); - callback('partData', 0, previousIndex, _lookbehind); + callback('onPartData', 0, previousIndex, _lookbehind); previousIndex = 0; - mark('partData'); + mark('onPartData'); // reconsider the current character even so it interrupted the sequence // it could be the beginning of a new sequence @@ -292,34 +304,25 @@ class MultipartParser { break; default: console.info(`Unexpected state entered: ${state}`); - return i; + return; } } - dataCallback('headerField'); - dataCallback('headerValue'); - dataCallback('partData'); + dataCallback('onHeaderField'); + dataCallback('onHeaderValue'); + dataCallback('onPartData'); // Update properties for the next call this.index = index; this.state = state; this.flags = flags; - - return length_; } end() { - function callback(self, name) { - const callbackSymbol = 'on' + name.slice(0, 1).toUpperCase() + name.slice(1); - if (callbackSymbol in self) { - self[callbackSymbol](); - } - } - if ((this.state === S.HEADER_FIELD_START && this.index === 0) || (this.state === S.PART_DATA && this.index === this.boundary.length)) { - callback(this, 'partEnd'); - callback(this, 'end'); + this.onPartEnd(); + // this.onEnd(); } else if (this.state !== S.END) { throw new Error('MultipartParser.end(): stream ended unexpectedly'); } From 405bcc08af8fc9e7a9550f4a0a28697849355b5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Wed, 29 Sep 2021 20:20:12 +0200 Subject: [PATCH 16/31] rm unnecessary isFormData --- src/body.js | 6 +++--- src/utils/is.js | 22 ---------------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/body.js b/src/body.js index 9e4984ac7..28a3607d1 100644 --- a/src/body.js +++ b/src/body.js @@ -13,7 +13,7 @@ import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js'; import {FetchError} from './errors/fetch-error.js'; import {FetchBaseError} from './errors/base.js'; -import {isBlob, isURLSearchParameters, isFormData} from './utils/is.js'; +import {isBlob, isURLSearchParameters} from './utils/is.js'; const INTERNALS = Symbol('Body internals'); @@ -50,7 +50,7 @@ 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)) { + } else if (body instanceof FormData) { // Body is an spec compatible FormData body = formDataToBlob(body); boundary = body.type.split('=')[1]; @@ -320,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}`; } diff --git a/src/utils/is.js b/src/utils/is.js index d23b9f027..910e5e51e 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -45,28 +45,6 @@ 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. * From 0881b134634c3ad48491fd08b140ed13f01214d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Wed, 29 Sep 2021 20:20:28 +0200 Subject: [PATCH 17/31] remove unnecessary and invalid jsdoc --- src/utils/is.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/utils/is.js b/src/utils/is.js index 910e5e51e..d0e770e66 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -9,9 +9,6 @@ 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 - * @return {boolean} */ export const isURLSearchParameters = object => { return ( @@ -29,9 +26,6 @@ export const isURLSearchParameters = object => { /** * Check if `object` is a W3C `Blob` object (which `File` inherits from) - * - * @param {*} obj - * @return {boolean} */ export const isBlob = object => { return ( @@ -47,9 +41,6 @@ export const isBlob = object => { /** * Check if `obj` is an instance of AbortSignal. - * - * @param {*} obj - * @return {boolean} */ export const isAbortSignal = object => { return ( @@ -59,4 +50,3 @@ export const isAbortSignal = object => { ) ); }; - From 6a6b949ca19001e9bf77002f9e7f89ca9ee2c969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Wed, 29 Sep 2021 20:20:49 +0200 Subject: [PATCH 18/31] misc --- src/utils/multipart-parser.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 4be9dfca9..e40bebd09 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -44,7 +44,7 @@ class MultipartParser { this.index = null; this.flags = 0; - this.onEnd = noop; + // this.onEnd = noop; this.onHeaderEnd = noop; this.onHeaderField = noop; this.onHeadersEnd = noop; @@ -117,7 +117,7 @@ class MultipartParser { case S.START: index = 0; state = S.START_BOUNDARY; - // fallsthrough + // falls through case S.START_BOUNDARY: if (index === boundary.length - 2) { if (c === HYPHEN) { @@ -130,7 +130,7 @@ class MultipartParser { break; } else if (index - 1 === boundary.length - 2) { if (flags & F.LAST_BOUNDARY && c === HYPHEN) { - callback('onEnd'); + // callback('onEnd'); state = S.END; flags = 0; } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { @@ -157,7 +157,7 @@ class MultipartParser { state = S.HEADER_FIELD; mark('onHeaderField'); index = 0; - // fallsthrough + // falls through case S.HEADER_FIELD: if (c === CR) { clear('onHeaderField'); @@ -194,7 +194,7 @@ class MultipartParser { mark('onHeaderValue'); state = S.HEADER_VALUE; - // fallsthrough + // falls through case S.HEADER_VALUE: if (c === CR) { dataCallback('onHeaderValue', true); @@ -221,7 +221,7 @@ class MultipartParser { case S.PART_DATA_START: state = S.PART_DATA; mark('onPartData'); - // fallsthrough + // falls through case S.PART_DATA: previousIndex = index; @@ -271,7 +271,7 @@ class MultipartParser { } else if (flags & F.LAST_BOUNDARY) { if (c === HYPHEN) { callback('onPartEnd'); - callback('onEnd'); + // callback('onEnd'); state = S.END; flags = 0; } else { @@ -412,10 +412,10 @@ export async function toFormData(Body, ct) { headerValue += decoder.decode(); headerField = headerField.toLowerCase(); - // matches either a quoted-string or a token (RFC 2616 section 19.5.1) - const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); - if (headerField === 'content-disposition') { + // matches either a quoted-string or a token (RFC 2616 section 19.5.1) + const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); + if (m) { entryName = m[2] || m[3] || ''; } From d98efc2a485f8d92d46edc5eaac23240c98dc9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Wed, 29 Sep 2021 23:59:38 +0200 Subject: [PATCH 19/31] re-added test for formdata-node support --- package.json | 3 ++- src/utils/multipart-parser.js | 3 +++ test/form-data.js | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 639a64efe..f8b2d3068 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,8 @@ "dependencies": { "data-uri-to-buffer": "^3.0.1", "fetch-blob": "^3.1.2", - "formdata-polyfill": "^4.0.8" + "formdata-node": "^4.2.4", + "formdata-polyfill": "^4.0.9" }, "tsd": { "cwd": "@types", diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index e40bebd09..7cd3f764e 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -429,6 +429,9 @@ export async function toFormData(Body, ct) { } else if (headerField === 'content-type') { contentType = headerValue; } + + headerValue = '' + headerField = '' }; for await (const chunk of Body) { diff --git a/test/form-data.js b/test/form-data.js index 4a577621d..c4e7f25a0 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -1,3 +1,4 @@ +import {FormData as FormDataNode} from 'formdata-node'; import {FormData} from 'formdata-polyfill/esm.min.js'; import {Blob} from 'fetch-blob/from.js'; import chai from 'chai'; @@ -81,4 +82,14 @@ describe('FormData', () => { expect(fd.get('blob').size).to.equal(13); }); + + it('FormData-node still works thanks to symbol.hasInstance', async () => { + const form = new FormData() + form.append('file', new Blob(['abc'], {type: 'text/html'})) + const res = new Response(form); + const fd = await res.formData(); + + expect(await fd.get('file').text()).to.equal('abc'); + expect(fd.get('file').type).to.equal('text/html'); + }); }); From 1058f55a44451dfab1ab3374995c2b3a465d3cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Thu, 30 Sep 2021 00:02:13 +0200 Subject: [PATCH 20/31] lint stuff --- src/utils/multipart-parser.js | 4 ++-- test/form-data.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 7cd3f764e..503d8810d 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -430,8 +430,8 @@ export async function toFormData(Body, ct) { contentType = headerValue; } - headerValue = '' - headerField = '' + headerValue = ''; + headerField = ''; }; for await (const chunk of Body) { diff --git a/test/form-data.js b/test/form-data.js index c4e7f25a0..9acbab948 100644 --- a/test/form-data.js +++ b/test/form-data.js @@ -84,8 +84,8 @@ describe('FormData', () => { }); it('FormData-node still works thanks to symbol.hasInstance', async () => { - const form = new FormData() - form.append('file', new Blob(['abc'], {type: 'text/html'})) + const form = new FormDataNode(); + form.append('file', new Blob(['abc'], {type: 'text/html'})); const res = new Response(form); const fd = await res.formData(); From 5a3b0427d05622f3d84db7c5c5910d6eaff56942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Thu, 30 Sep 2021 00:06:24 +0200 Subject: [PATCH 21/31] moved formdata-node to devDep --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f8b2d3068..4a7f311fb 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "chai-string": "^1.5.0", "coveralls": "^3.1.0", "delay": "^5.0.0", + "formdata-node": "^4.2.4", "form-data": "^4.0.0", "mocha": "^8.3.2", "p-timeout": "^5.0.0", @@ -63,7 +64,6 @@ "dependencies": { "data-uri-to-buffer": "^3.0.1", "fetch-blob": "^3.1.2", - "formdata-node": "^4.2.4", "formdata-polyfill": "^4.0.9" }, "tsd": { From a378d7ac0e582fb86a6e538614fbd157f5eae139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Thu, 30 Sep 2021 15:19:22 +0200 Subject: [PATCH 22/31] bump formdata for null comparison --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 4a7f311fb..79552b92d 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "chai-string": "^1.5.0", "coveralls": "^3.1.0", "delay": "^5.0.0", - "formdata-node": "^4.2.4", "form-data": "^4.0.0", + "formdata-node": "^4.2.4", "mocha": "^8.3.2", "p-timeout": "^5.0.0", "tsd": "^0.14.0", @@ -64,7 +64,7 @@ "dependencies": { "data-uri-to-buffer": "^3.0.1", "fetch-blob": "^3.1.2", - "formdata-polyfill": "^4.0.9" + "formdata-polyfill": "^4.0.10" }, "tsd": { "cwd": "@types", From 87d1bbbf48c587e82a1f141bf2803b9695d7524f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 1 Oct 2021 15:54:29 +0200 Subject: [PATCH 23/31] no short name for formData --- src/body.js | 6 +++--- src/utils/multipart-parser.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/body.js b/src/body.js index 28a3607d1..df32b6419 100644 --- a/src/body.js +++ b/src/body.js @@ -109,14 +109,14 @@ export default class Body { const ct = this.headers.get('content-type'); if (ct.startsWith('application/x-www-form-urlencoded')) { - const fd = new FormData(); + const formData = new FormData(); const parameters = new URLSearchParams(await this.text()); for (const [name, value] of parameters) { - fd.append(name, value); + formData.append(name, value); } - return fd; + return formData; } const {toFormData} = await import('./utils/multipart-parser.js'); diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 503d8810d..b3d69f056 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -365,7 +365,7 @@ export async function toFormData(Body, ct) { let contentType; let filename; const entryChunks = []; - const fd = new FormData(); + const formData = new FormData(); const onPartData = ui8a => { entryValue += decoder.decode(ui8a, {stream: true}); @@ -377,11 +377,11 @@ export async function toFormData(Body, ct) { const appendFileToFormData = () => { const file = new File(entryChunks, filename, {type: contentType}); - fd.append(entryName, file); + formData.append(entryName, file); }; const appendEntryToFormData = () => { - fd.append(entryName, entryValue); + formData.append(entryName, entryValue); }; const decoder = new TextDecoder('utf-8'); @@ -440,5 +440,5 @@ export async function toFormData(Body, ct) { parser.end(); - return fd; + return formData; } From b26758459de9fd3dd3dab64a7dd1a26d052cac02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 8 Oct 2021 13:48:03 +0200 Subject: [PATCH 24/31] lint fix --- test/response.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/response.js b/test/response.js index 6f020b45b..9e3d0647c 100644 --- a/test/response.js +++ b/test/response.js @@ -131,7 +131,7 @@ describe('Response', () => { expect(cl.url).to.equal(base); expect(cl.status).to.equal(346); expect(cl.statusText).to.equal('production'); - expect(cl.highWaterMark).to.equal(789) + expect(cl.highWaterMark).to.equal(789); expect(cl.ok).to.be.false; // Clone body shouldn't be the same body expect(cl.body).to.not.equal(body); From c9a1a576463f172ed8cb67ad9951c69f344fdf2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 13:19:03 +0200 Subject: [PATCH 25/31] bring back return --- src/utils/is.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/utils/is.js b/src/utils/is.js index d0e770e66..377161ff1 100644 --- a/src/utils/is.js +++ b/src/utils/is.js @@ -9,6 +9,8 @@ const NAME = Symbol.toStringTag; /** * Check if `obj` is a URLSearchParams object * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 + * @param {*} object - Object to check for + * @return {boolean} */ export const isURLSearchParameters = object => { return ( @@ -26,6 +28,8 @@ export const isURLSearchParameters = object => { /** * Check if `object` is a W3C `Blob` object (which `File` inherits from) + * @param {*} object - Object to check for + * @return {boolean} */ export const isBlob = object => { return ( @@ -41,6 +45,8 @@ export const isBlob = object => { /** * Check if `obj` is an instance of AbortSignal. + * @param {*} object - Object to check for + * @return {boolean} */ export const isAbortSignal = object => { return ( From d3b38995d7aa3b4cab01845d2bc2d31fb0151402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 13:21:27 +0200 Subject: [PATCH 26/31] throw on default --- src/utils/multipart-parser.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index b3d69f056..32b29d23c 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -303,8 +303,7 @@ class MultipartParser { case S.END: break; default: - console.info(`Unexpected state entered: ${state}`); - return; + throw new Error(`Unexpected state entered: ${state}`); } } From bcec478007932d4b1cdc781e0ce441443c43c058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 13:23:58 +0200 Subject: [PATCH 27/31] remove some code comments --- src/utils/multipart-parser.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 32b29d23c..4725f5f3c 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -44,7 +44,6 @@ class MultipartParser { this.index = null; this.flags = 0; - // this.onEnd = noop; this.onHeaderEnd = noop; this.onHeaderField = noop; this.onHeadersEnd = noop; @@ -130,7 +129,6 @@ class MultipartParser { break; } else if (index - 1 === boundary.length - 2) { if (flags & F.LAST_BOUNDARY && c === HYPHEN) { - // callback('onEnd'); state = S.END; flags = 0; } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { @@ -271,7 +269,6 @@ class MultipartParser { } else if (flags & F.LAST_BOUNDARY) { if (c === HYPHEN) { callback('onPartEnd'); - // callback('onEnd'); state = S.END; flags = 0; } else { @@ -321,7 +318,6 @@ class MultipartParser { if ((this.state === S.HEADER_FIELD_START && this.index === 0) || (this.state === S.PART_DATA && this.index === this.boundary.length)) { this.onPartEnd(); - // this.onEnd(); } else if (this.state !== S.END) { throw new Error('MultipartParser.end(): stream ended unexpectedly'); } From d331ad85cf57c372930cb42180f13883fe45c17f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 13:28:03 +0200 Subject: [PATCH 28/31] change the first state --- src/utils/multipart-parser.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 4725f5f3c..5eb59a713 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -41,7 +41,7 @@ class MultipartParser { * @param {string} string */ constructor(string) { - this.index = null; + this.index = 0; this.flags = 0; this.onHeaderEnd = noop; @@ -63,7 +63,7 @@ class MultipartParser { this.boundary = ui8a; this.lookbehind = new Uint8Array(this.boundary.length + 8); - this.state = S.START; + this.state = S.START_BOUNDARY; } /** From 81dc6395980de7f1885a795556bf7d0a384ce0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 13:33:08 +0200 Subject: [PATCH 29/31] better name --- src/utils/multipart-parser.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 5eb59a713..2b5a2a308 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -38,9 +38,9 @@ const noop = () => {}; class MultipartParser { /** - * @param {string} string + * @param {string} boundary */ - constructor(string) { + constructor(boundary) { this.index = 0; this.flags = 0; @@ -54,10 +54,10 @@ class MultipartParser { this.boundaryChars = {}; - string = '\r\n--' + string; - const ui8a = new Uint8Array(string.length); - for (let i = 0; i < string.length; i++) { - ui8a[i] = string.charCodeAt(i); + boundary = '\r\n--' + boundary; + const ui8a = new Uint8Array(boundary.length); + for (let i = 0; i < boundary.length; i++) { + ui8a[i] = boundary.charCodeAt(i); this.boundaryChars[ui8a[i]] = true; } @@ -67,16 +67,16 @@ class MultipartParser { } /** - * @param {Uint8Array} ui8a + * @param {Uint8Array} data */ - write(ui8a) { + write(data) { let i = 0; - const length_ = ui8a.length; + const length_ = data.length; let previousIndex = this.index; let {lookbehind, boundary, boundaryChars, index, state, flags} = this; const boundaryLength = this.boundary.length; const boundaryEnd = boundaryLength - 1; - const bufferLength = ui8a.length; + const bufferLength = data.length; let c; let cl; @@ -101,16 +101,16 @@ class MultipartParser { } if (clear) { - callback(name, this[markSymbol], i, ui8a); + callback(name, this[markSymbol], i, data); delete this[markSymbol]; } else { - callback(name, this[markSymbol], ui8a.length, ui8a); + callback(name, this[markSymbol], data.length, data); this[markSymbol] = 0; } }; for (i = 0; i < length_; i++) { - c = ui8a[i]; + c = data[i]; switch (state) { case S.START: @@ -226,12 +226,12 @@ class MultipartParser { if (index === 0) { // boyer-moore derrived algorithm to safely skip non-boundary data i += boundaryEnd; - while (i < bufferLength && !(ui8a[i] in boundaryChars)) { + while (i < bufferLength && !(data[i] in boundaryChars)) { i += boundaryLength; } i -= boundaryEnd; - c = ui8a[i]; + c = data[i]; } if (index < boundary.length) { From 2882e392029d37ee606edc723588f0a819093091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Fri, 15 Oct 2021 19:07:51 +0200 Subject: [PATCH 30/31] remove start state --- src/utils/multipart-parser.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/utils/multipart-parser.js b/src/utils/multipart-parser.js index 2b5a2a308..5ad06f98e 100644 --- a/src/utils/multipart-parser.js +++ b/src/utils/multipart-parser.js @@ -3,7 +3,6 @@ import {FormData} from 'formdata-polyfill/esm.min.js'; let s = 0; const S = { - START: s++, START_BOUNDARY: s++, HEADER_FIELD_START: s++, HEADER_FIELD: s++, @@ -30,9 +29,7 @@ const COLON = 58; const A = 97; const Z = 122; -const lower = function (c) { - return c | 0x20; -}; +const lower = c => c | 0x20; const noop = () => {}; @@ -113,10 +110,6 @@ class MultipartParser { c = data[i]; switch (state) { - case S.START: - index = 0; - state = S.START_BOUNDARY; - // falls through case S.START_BOUNDARY: if (index === boundary.length - 2) { if (c === HYPHEN) { From cfaeb63212c8ba0ea90a0271974aab84b0205739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jimmy=20Wa=CC=88rting?= Date: Sat, 16 Oct 2021 07:58:38 +0200 Subject: [PATCH 31/31] shorter comments --- docs/CHANGELOG.md | 2 +- src/body.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index a1d3993bf..3825525f3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,7 +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: Added support for `Body.formData()` (#1314) +- 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) diff --git a/src/body.js b/src/body.js index df32b6419..9c27d1538 100644 --- a/src/body.js +++ b/src/body.js @@ -51,7 +51,7 @@ export default class Body { } else if (body instanceof Stream) { // Body is stream } else if (body instanceof FormData) { - // Body is an spec compatible FormData + // Body is FormData body = formDataToBlob(body); boundary = body.type.split('=')[1]; } else {