├── .gitignore ├── LICENSE ├── README.md ├── example ├── polyfill │ ├── package.json │ ├── rollup.config.js │ └── src │ │ └── main.js └── ponyfill │ ├── package.json │ ├── rollup.config.js │ └── src │ └── main.js ├── package-lock.json ├── package.json ├── src ├── index.ts └── ponyfill.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .rpt2_cache/ 2 | node_modules/ 3 | lib/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Steve Lam 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TL;DR 2 | 3 | A polyfill for `Request.prototype.formData` on cloudflare workers because they're not compliant to the [whatwg spec](https://fetch.spec.whatwg.org/#dom-body-formdata) even though [their documentation suggests otherwise](https://developers.cloudflare.com/workers/reference/runtime/apis/fetch/#methods-1). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | npm install @ssttevee/cfw-formdata-polyfill 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```js 14 | import `@ssttevee/cfw-formdata-polyfill`; 15 | 16 | // get req from handler 17 | 18 | const fd = await req.formData(); 19 | ``` 20 | 21 | or if a [ponyfill](https://ponyfill.com/) is more preferrable 22 | 23 | ```js 24 | import FormDataFromRequest from '@ssttevee/cfw-formdata-polyfill/ponyfill'; 25 | 26 | // get req from handler 27 | 28 | const fd = await FormDataFromRequest.call(req); 29 | ``` 30 | 31 | ## Examples 32 | 33 | There are some full examples in the [examples directory](https://github.com/ssttevee/cfw-formdata-polyfill/tree/master/src). 34 | 35 | # Native `Request.prototype.formData` on Cloudflare Workers 36 | 37 | Since before the public launch of Cloudflare Workers until the time of writing, `Request.prototype.formData` has been able to handle `application/x-www-form-urlencoded` payloads as one would expect. However, their support for `multipart/form-data` has been... undocumented. 38 | 39 | ## Native `multipart/form-data` support 40 | 41 | The native `Request.prototype.formData` does technically support `multipart/form-data`. However, files are returned as binary strings rather than a Blob. That means the filename and content type metadata is lost. Though this is somewhat justified upon closer inspection of the environment. 42 | 43 | ## The FormData API 44 | 45 | The FormData API, as long as no files are involved, seems to work as one would expect. However, advanced users will quickly realize that it cannot be used to construct `multipart/form-data` payloads to be used with `fetch`. It doesn't even accept any type other than strings. This is because the Blob API doesn't exist on Cloudflare Workers. 46 | 47 | ## The Blob API 48 | 49 | The Blob API, as designed for the browser, is meant to be an abstraction for reading arbitrary data, outside of the browser sandbox, from the operating system. Meanwhile, there is no parallel in the Cloudflare Workers environment. This, I believe, is the primary reason that this halfway point for the FormData API exists on Cloudflare Workers. 50 | 51 | ## Polyfilling 52 | 53 | This package was designed to be used in conjuction with a bundler -- I prefer rollup -- to fix `Request.prototype.formData` and to be able to use it for receiving `multipart/form-data` uploads. 54 | -------------------------------------------------------------------------------- /example/polyfill/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "polyfill-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup -c" 6 | }, 7 | "devDependencies": { 8 | "@ssttevee/cfw-formdata-polyfill": "^0.1.0", 9 | "rollup": "^1.20.3", 10 | "rollup-plugin-inject": "^3.0.1", 11 | "rollup-plugin-node-resolve": "^5.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/polyfill/rollup.config.js: -------------------------------------------------------------------------------- 1 | import inject from 'rollup-plugin-inject'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | 4 | module.exports = { 5 | input: 'src/main.js', 6 | output: { 7 | file: 'build/main.js', 8 | format: 'esm', 9 | }, 10 | plugins: [ 11 | /* remove this */ { resolveId: (source) => source === '@ssttevee/cfw-formdata-polyfill' ? { id: require.resolve('../../index.js') } : undefined }, 12 | resolve(), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /example/polyfill/src/main.js: -------------------------------------------------------------------------------- 1 | import '@ssttevee/cfw-formdata-polyfill'; 2 | 3 | addEventListener('fetch', (event) => { 4 | event.respondWith(handleRequest(event.request)); 5 | }); 6 | 7 | /** 8 | * Fetch and log a given request object 9 | * @param {Request} request 10 | */ 11 | async function handleRequest(request) { 12 | const res = { files: undefined, form: undefined }; 13 | if (request.method === 'POST') { 14 | try { 15 | const fd = await request.formData(); 16 | for (const [name, value] of fd.entries()) { 17 | if (value instanceof Blob) { 18 | if (!res.files) { 19 | res.files = {}; 20 | } 21 | 22 | res.files[name] = btoa(new FileReaderSync().readAsBinaryString(value)); 23 | } else { 24 | if (!res.form) { 25 | res.form = {}; 26 | } 27 | 28 | res.form[name] = value; 29 | } 30 | } 31 | } catch (err) { 32 | return new Response(err.stack, { status: 500 }); 33 | } 34 | } 35 | 36 | return new Response(JSON.stringify(res)); 37 | } 38 | -------------------------------------------------------------------------------- /example/ponyfill/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ponyfill-example", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "build": "rollup -c" 6 | }, 7 | "devDependencies": { 8 | "@ssttevee/cfw-formdata-polyfill": "^0.1.0", 9 | "rollup": "^1.20.3", 10 | "rollup-plugin-inject": "^3.0.1", 11 | "rollup-plugin-node-resolve": "^5.2.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ponyfill/rollup.config.js: -------------------------------------------------------------------------------- 1 | import inject from 'rollup-plugin-inject'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | 4 | module.exports = { 5 | input: 'src/main.js', 6 | output: { 7 | file: 'build/main.js', 8 | format: 'esm', 9 | }, 10 | plugins: [ 11 | /* remove this */ { resolveId: (source) => source === '@ssttevee/cfw-formdata-polyfill/ponyfill' ? { id: require.resolve('../../ponyfill.js') } : undefined }, 12 | resolve(), 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /example/ponyfill/src/main.js: -------------------------------------------------------------------------------- 1 | import FormDataFromRequest from '@ssttevee/cfw-formdata-polyfill/ponyfill'; 2 | 3 | addEventListener('fetch', (event) => { 4 | event.respondWith(handleRequest(event.request)); 5 | }); 6 | 7 | /** 8 | * Fetch and log a given request object 9 | * @param {Request} request 10 | */ 11 | async function handleRequest(request) { 12 | const res = { files: undefined, form: undefined }; 13 | if (request.method === 'POST') { 14 | try { 15 | const fd = await FormDataFromRequest.call(request); 16 | for (const [name, value] of fd.entries()) { 17 | if (value instanceof Blob) { 18 | if (!res.files) { 19 | res.files = {}; 20 | } 21 | 22 | res.files[name] = btoa(new FileReaderSync().readAsBinaryString(value)); 23 | } else { 24 | if (!res.form) { 25 | res.form = {}; 26 | } 27 | 28 | res.form[name] = value; 29 | } 30 | } 31 | } catch (err) { 32 | return new Response(err.stack, { status: 500 }); 33 | } 34 | } 35 | 36 | return new Response(JSON.stringify(res)); 37 | } 38 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssttevee/cfw-formdata-polyfill", 3 | "version": "0.2.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@ssttevee/cfw-formdata-polyfill", 9 | "version": "0.2.1", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@ssttevee/multipart-parser": "^0.1.6", 13 | "@ssttevee/u8-utils": "^0.1.3" 14 | }, 15 | "devDependencies": { 16 | "typescript": "^3.5.3" 17 | } 18 | }, 19 | "node_modules/@ssttevee/multipart-parser": { 20 | "version": "0.1.6", 21 | "resolved": "https://registry.npmjs.org/@ssttevee/multipart-parser/-/multipart-parser-0.1.6.tgz", 22 | "integrity": "sha512-UYsSZzxSKB7SFcgfE6wGeng4VR4/rC33zoWxtME4deirIztP/XPUBbX+lbLqV1+YdlKzmow4a4K1VfMB1DH00Q==", 23 | "dependencies": { 24 | "@ssttevee/streamsearch": "^0.1.3", 25 | "@ssttevee/u8-utils": "^0.1.3" 26 | } 27 | }, 28 | "node_modules/@ssttevee/streamsearch": { 29 | "version": "0.1.4", 30 | "resolved": "https://registry.npmjs.org/@ssttevee/streamsearch/-/streamsearch-0.1.4.tgz", 31 | "integrity": "sha512-9yMRfhL/01h201Zuv65ly43NUpNy6wVdu9zT35bvmc7ct4OZCl5DLdfc7gzZB9teG6YuP0/GmlnuXtMgmA5axg==", 32 | "dependencies": { 33 | "@ssttevee/u8-utils": "^0.1.4" 34 | } 35 | }, 36 | "node_modules/@ssttevee/streamsearch/node_modules/@ssttevee/u8-utils": { 37 | "version": "0.1.4", 38 | "resolved": "https://registry.npmjs.org/@ssttevee/u8-utils/-/u8-utils-0.1.4.tgz", 39 | "integrity": "sha512-CqnrqVpq/PAp0K+H1KfRHwCZlNzqWPM080Ps33UHHe2QbGURHNBZdlK2ycNJRNHkroMtkCyabrjWF1UsFjOrEw==" 40 | }, 41 | "node_modules/@ssttevee/u8-utils": { 42 | "version": "0.1.3", 43 | "resolved": "https://registry.npmjs.org/@ssttevee/u8-utils/-/u8-utils-0.1.3.tgz", 44 | "integrity": "sha512-XX1JMEmKtxL7W3u9ZTZRkYVEk9/vhGD0DCJ0GHO15m+nO0PeUxcvLuGF3Y7w71DU2NYtxVWg+QOkcXqwbru2VA==" 45 | }, 46 | "node_modules/typescript": { 47 | "version": "3.5.3", 48 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", 49 | "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", 50 | "dev": true, 51 | "bin": { 52 | "tsc": "bin/tsc", 53 | "tsserver": "bin/tsserver" 54 | }, 55 | "engines": { 56 | "node": ">=4.2.0" 57 | } 58 | } 59 | }, 60 | "dependencies": { 61 | "@ssttevee/multipart-parser": { 62 | "version": "0.1.6", 63 | "resolved": "https://registry.npmjs.org/@ssttevee/multipart-parser/-/multipart-parser-0.1.6.tgz", 64 | "integrity": "sha512-UYsSZzxSKB7SFcgfE6wGeng4VR4/rC33zoWxtME4deirIztP/XPUBbX+lbLqV1+YdlKzmow4a4K1VfMB1DH00Q==", 65 | "requires": { 66 | "@ssttevee/streamsearch": "^0.1.3", 67 | "@ssttevee/u8-utils": "^0.1.3" 68 | } 69 | }, 70 | "@ssttevee/streamsearch": { 71 | "version": "0.1.4", 72 | "resolved": "https://registry.npmjs.org/@ssttevee/streamsearch/-/streamsearch-0.1.4.tgz", 73 | "integrity": "sha512-9yMRfhL/01h201Zuv65ly43NUpNy6wVdu9zT35bvmc7ct4OZCl5DLdfc7gzZB9teG6YuP0/GmlnuXtMgmA5axg==", 74 | "requires": { 75 | "@ssttevee/u8-utils": "^0.1.4" 76 | }, 77 | "dependencies": { 78 | "@ssttevee/u8-utils": { 79 | "version": "0.1.4", 80 | "resolved": "https://registry.npmjs.org/@ssttevee/u8-utils/-/u8-utils-0.1.4.tgz", 81 | "integrity": "sha512-CqnrqVpq/PAp0K+H1KfRHwCZlNzqWPM080Ps33UHHe2QbGURHNBZdlK2ycNJRNHkroMtkCyabrjWF1UsFjOrEw==" 82 | } 83 | } 84 | }, 85 | "@ssttevee/u8-utils": { 86 | "version": "0.1.3", 87 | "resolved": "https://registry.npmjs.org/@ssttevee/u8-utils/-/u8-utils-0.1.3.tgz", 88 | "integrity": "sha512-XX1JMEmKtxL7W3u9ZTZRkYVEk9/vhGD0DCJ0GHO15m+nO0PeUxcvLuGF3Y7w71DU2NYtxVWg+QOkcXqwbru2VA==" 89 | }, 90 | "typescript": { 91 | "version": "3.5.3", 92 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", 93 | "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", 94 | "dev": true 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ssttevee/cfw-formdata-polyfill", 3 | "type": "module", 4 | "version": "0.2.1", 5 | "description": "A polyfill for Request.prototype.formData on cloudflare workers", 6 | "main": "index.js", 7 | "files": [ 8 | "*.js", 9 | "*.d.ts" 10 | ], 11 | "scripts": { 12 | "build": "tsc && rm lib/index.d.ts && mv lib/* .", 13 | "clean": "rm *.js *.d.ts", 14 | "prepack": "npm run build", 15 | "postpack": "npm run clean" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/ssttevee/js-cfw-formdata-polyfill.git" 20 | }, 21 | "keywords": [ 22 | "cloudflare", 23 | "workers", 24 | "cfw", 25 | "formdata", 26 | "polyfill", 27 | "ponyfill" 28 | ], 29 | "author": "ssttevee", 30 | "license": "MIT", 31 | "dependencies": { 32 | "@ssttevee/multipart-parser": "^0.1.6", 33 | "@ssttevee/u8-utils": "^0.1.3" 34 | }, 35 | "devDependencies": { 36 | "typescript": "^3.5.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import FormDataFromRequest from './ponyfill'; 2 | 3 | Request.prototype.formData = FormDataFromRequest; 4 | -------------------------------------------------------------------------------- /src/ponyfill.ts: -------------------------------------------------------------------------------- 1 | import { parseMultipart } from '@ssttevee/multipart-parser'; 2 | import { arrayToString } from '@ssttevee/u8-utils'; 3 | 4 | const RE_MULTIPART = /^multipart\/form-data(?:;\s*boundary=(?:"((?:[^"]|\\")+)"|([^\s;]+)))$/; 5 | const RE_URLENCODED = /^application\/x-www-form-urlencoded(?:;|$|\s)/; 6 | 7 | type Parser = (this: Request) => Promise; 8 | 9 | export function makeWrapper(fallbackParser: Parser): Parser { 10 | return async function(this: Request): Promise { 11 | const contentType = this.headers.get('content-type'); 12 | const matches = RE_MULTIPART.exec(contentType); 13 | if (matches && (matches[1] || matches[2])) { 14 | const parts = await parseMultipart(this.body, matches[1] || matches[2]); 15 | const fd = new FormData(); 16 | for (const { name, data, filename, contentType } of parts) { 17 | fd.append(name, filename ? new Blob([data], { type: contentType }) : arrayToString(data), filename); 18 | } 19 | 20 | return fd; 21 | } 22 | 23 | if (contentType.match(RE_URLENCODED)) { 24 | return fallbackParser.call(this); 25 | } 26 | 27 | throw new TypeError('Unexpected Content-Type: ' + contentType); 28 | } 29 | } 30 | 31 | export default makeWrapper(Request.prototype.formData as any); 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "target": "esnext", 5 | "outDir": "lib", 6 | "lib": [ 7 | "esnext", 8 | "webworker", 9 | ], 10 | "moduleResolution": "node", 11 | "esModuleInterop": true, 12 | "declaration": true, 13 | }, 14 | "include": [ 15 | "src/**/*", 16 | ], 17 | } 18 | --------------------------------------------------------------------------------