{"id":13726350,"url":"https://github.com/TomasHubelbauer/workers-formdata","last_synced_at":"2025-05-07T21:32:07.924Z","repository":{"id":49063946,"uuid":"247045346","full_name":"TomasHubelbauer/workers-formdata","owner":"TomasHubelbauer","description":"FormData support for Cloudflare Workers","archived":false,"fork":false,"pushed_at":"2022-08-30T18:08:27.000Z","size":26,"stargazers_count":27,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-08-04T01:28:48.499Z","etag":null,"topics":["cloudflare","cloudflare-workers","formdata","multipart"],"latest_commit_sha":null,"homepage":"","language":"Markdown","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/TomasHubelbauer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-13T10:28:31.000Z","updated_at":"2024-05-11T02:22:59.000Z","dependencies_parsed_at":"2023-01-17T00:30:27.880Z","dependency_job_id":null,"html_url":"https://github.com/TomasHubelbauer/workers-formdata","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fworkers-formdata","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fworkers-formdata/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fworkers-formdata/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/TomasHubelbauer%2Fworkers-formdata/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/TomasHubelbauer","download_url":"https://codeload.github.com/TomasHubelbauer/workers-formdata/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224654228,"owners_count":17347700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflare","cloudflare-workers","formdata","multipart"],"created_at":"2024-08-03T01:03:00.599Z","updated_at":"2024-11-14T16:33:42.222Z","avatar_url":"https://github.com/TomasHubelbauer.png","language":"Markdown","readme":"# CloudFlare Workers `FormData`\n\n\u003e Disclaimer: This should now be unnecessary!\n\u003e See https://developers.cloudflare.com/workers/platform/compatibility-dates/#formdata-parsing-supports-file\n\nCloudFlare Workers supports getting a `FormData` instance from the request using\n`request.formData()`. However, the resulting `FormData` instance, when used with\n`formData.get('file')`, does not return a `File` instance (inclusive of the file\nname and type), but a string representation of the file's contents. While it may\nbe possible to use `charCodeAt` to construct a `Uint8Array` from the string and\nrecover the file's binary contents this way, access to the file name and type is\nseemingly impossible.\n\nI've opened a thread on the CloudFlare Community site for this:\nhttps://community.cloudflare.com/t/worker-formdata-get-file-instance/155009\n\nThis might get fixed, it might not, I don't know. Meanwhile, I've prototyped a\nbasic MIME multipart parser, which is able to recover the file binary contents as\nwell as the metadata (name and type). Find it here:\n\nhttps://github.com/TomasHubelbauer/mime-multipart\n\nThe contents of the script (in `index.html`) need to be pasted to the worker script\ndirectly, it cannot be referenced in any other way, because Workers does not support\nESM (https://community.cloudflare.com/t/esm-modules-in-cloudflare-workers/155721)\nand `eval` is disabled in the worker script context.\n\nA full example (including the `parseMimeMultipart` function as of the time of writing)\nof a worker which accepts a file to be uploaded and serves it right back:\n\n```javascript\naddEventListener('fetch', event =\u003e event.respondWith(serve(event.request)))\n\nasync function serve(request) {\n  try {\n    const url = new URL(request.url);\n    const action = request.method + ' ' + url.pathname;\n    if (action === 'GET /') {\n      const html = `\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n  \u003chead\u003e\n    \u003cmeta charset=\"utf-8\" /\u003e\n    \u003ctitle\u003eTest\u003c/title\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cform method=\"POST\" enctype=\"multipart/form-data\"\u003e\n      \u003cinput type=\"file\" multiple name=\"files[]\" /\u003e\n      \u003cinput type=\"submit\" /\u003e\n    \u003c/form\u003e\n  \u003c/body\u003e\n\u003c/html\u003e\n`;\n\n      return new Response(html, { headers: { 'Content-Type': 'html' } });\n    }\n\n    const uint8Arrray = await request.arrayBuffer();\n    const parts = [...parseMimeMultipart(uint8Arrray)];\n    if (parts.length === 0) {\n      return new Response('No parts!');\n    }\n\n    if (parts.length \u003e 1) {\n      return new Response('Too many parts!');\n    }\n\n    const [part] = parts;\n    const type = part.headers.find(h =\u003e h.name === 'Content-Type')?.values[0] || 'application/octet-stream';\n    const blob = uint8Arrray.slice(part.index, part.index + part.length);\n    return new Response(blob, { headers: { 'Content-Type': type } });\n  } catch (error) {\n    return new Response(error.message);\n  }\n}\n\n// https://github.com/TomasHubelbauer/mime-multipart\nfunction* parseMimeMultipart(/** @type {Uint8Array} */ uint8Array) {\n  const textDecoder = new TextDecoder();\n  /** @typedef {{ name: string; values: string[]; }} Header */\n  /** @typedef {{ type: 'boundary'; boundary: string; }} Boundary */\n  /** @typedef {{ type: 'header-name'; boundary: string; name: string; headers: Header[]; }} HeaderName */\n  /** @typedef {{ type: 'header-value'; boundary: string; name: string; value: string; values: string[]; headers: Header[]; }} HeaderValue */\n  /** @typedef {{ type: 'content'; boundary: string; headers: Headers[]; index: number; length: number; }} Content */\n  /** @type {Boundary | HeaderName | HeaderValue | Content} */\n  let state = { type: 'boundary', boundary: '' };\n  let index = 0;\n  let line = 0;\n  let column = 0;\n  for (; index \u003c uint8Array.byteLength; index++) {\n    const character = textDecoder.decode(uint8Array.slice(index, index + 1));\n    if (character === '\\n') {\n      line++;\n      column = 0;\n    }\n\n    column++;\n\n    switch (state.type) {\n      case 'boundary': {\n        // Check Windows newlines\n        if (character === '\\r') {\n          if (textDecoder.decode(uint8Array.slice(index + 1, index + 2)) !== '\\n') {\n            throw new Error(`At ${index} (${line}:${column}): found an incomplete Windows newline.`);\n          }\n\n          break;\n        }\n\n        if (character === '\\n') {\n          state = { type: 'header-name', boundary: state.boundary, name: '', value: '', headers: [] };\n          break;\n        }\n\n        state.boundary += character;\n        break;\n      }\n      case 'header-name': {\n        // Check Windows newlines\n        if (character === '\\r') {\n          if (textDecoder.decode(uint8Array.slice(index + 1, index + 2)) !== '\\n') {\n            throw new Error(`At ${index} (${line}:${column}): found an incomplete Windows newline.`);\n          }\n\n          break;\n        }\n\n        if (character === '\\n') {\n          if (state.name === '') {\n            state = { type: 'content', boundary: state.boundary, headers: state.headers, index: index + 1, length: 0 };\n            break;\n          }\n          else {\n            throw new Error(`At ${index} (${line}:${column}): a newline in a header name '${state.name}' is not allowed.`);\n          }\n        }\n\n        if (character === ':') {\n          state = { type: 'header-value', boundary: state.boundary, name: state.name, value: '', values: [], headers: state.headers };\n          break;\n        }\n\n        state.name += character;\n        break;\n      }\n      case 'header-value': {\n        // Check Windows newlines\n        if (character === '\\r') {\n          if (textDecoder.decode(uint8Array.slice(index + 1, index + 2)) !== '\\n') {\n            throw new Error(`At ${index} (${line}:${column}): found an incomplete Windows newline.`);\n          }\n\n          break;\n        }\n\n        if (character === ';') {\n          state.values.push(state.value);\n          state.value = '';\n          break;\n        }\n\n        if (character === ' ') {\n          // Ignore white-space prior to the value content\n          if (state.value === '') {\n            break;\n          }\n        }\n\n        if (character === '\\n') {\n          state.values.push(state.value);\n          state = { type: 'header-name', boundary: state.boundary, name: '', value: '', headers: [{ name: state.name, values: state.values }, ...state.headers] };\n          break;\n        }\n\n        state.value += character;\n        break;\n      }\n      case 'content': {\n        // If the newline is followed by the boundary, then the content ends\n        if (character === '\\n' || character === '\\r' \u0026\u0026 textDecoder.decode(uint8Array.slice(index + 1, index + 2)) === '\\n') {\n          if (character === '\\r') {\n            index++;\n          }\n\n          const boundaryCheck = textDecoder.decode(uint8Array.slice(index + '\\n'.length, index + '\\n'.length + state.boundary.length));\n          if (boundaryCheck === state.boundary) {\n            const conclusionCheck = textDecoder.decode(uint8Array.slice(index + '\\n'.length + state.boundary.length, index + '\\n'.length + state.boundary.length + '--'.length));\n            if (conclusionCheck === '--') {\n              index += '\\n'.length + state.boundary.length + '--'.length;\n              yield { headers: state.headers, index: state.index, length: state.length };\n\n              if (index !== uint8Array.byteLength) {\n                const excess = uint8Array.slice(index);\n                if (textDecoder.decode(excess) === '\\n' || textDecoder.decode(excess) === '\\r\\n') {\n                  return;\n                }\n\n                throw new Error(`At ${index} (${line}:${column}): content is present past the expected end of data ${uint8Array.byteLength}.`);\n              }\n\n              return;\n            }\n            else {\n              yield { headers: state.headers, index: state.index, length: state.length };\n              state = { type: 'boundary', boundary: '' };\n              break;\n            }\n          }\n        }\n\n        state.length++;\n        break;\n      }\n      default: {\n        throw new Error(`At ${index} (${line}:${column}): invalid state ${JSON.stringify(state)}.`);\n      }\n    }\n  }\n\n  if (state.type !== 'content') {\n    throw new Error(`At ${index} (${line}:${column}): expected content state, got ${JSON.stringify(state)}.`);\n  }\n};\n```\n\n## To-Do\n\n### See if with Durable Objects and ESM this script could be a dependency\n\nhttps://developers.cloudflare.com/workers/cli-wrangler/configuration#modules\n\nCloudflare Workers now seem to support ESM, but the questions remains if this is\ntrue ESM or some Wrangler bundling, and if so, if it supports pulling in HTTP\nmodules as opposed to file protocol modules only.\n\nWould be good to try this while I still have Workers subscription.\n","funding_links":[],"categories":["Others"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTomasHubelbauer%2Fworkers-formdata","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FTomasHubelbauer%2Fworkers-formdata","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FTomasHubelbauer%2Fworkers-formdata/lists"}