Accessing a formData object in a Cloudflare Workers function

I’m trying to send a formData object to a cloudlflare workers object. Here’s the original fetch event

const response = await fetch('https://example.workers.dev/createproject', {
    method: 'POST',
    body: formData,
    mode: 'no-cors'
    if (response.ok) {
    ...
    }
    else {
    ...
    }

}

And here is my cloudflare workers index.js file

import { json, missing, ThrowableRouter, withParams } from 'itty-router-extras';
import { createClient } from '@supabase/supabase-js';
const router = ThrowableRouter();
// apiKey is the parsed JWT for a user.
// Not currently supported by Supabase's client, but eventually
// you'll be able to auth _as_ a user by providing this value.
const supabase = (apiKey) => createClient(SUPABASE_URL, SUPABASE_API_KEY);
const parseAuthHeader = (header) => {
    if (!header)
        return;
    const [_, token] = header.split("Bearer ");
    return token;
};

  router.post('/createproject', async ({ request }) => {
    try {
      const formData = await request.formData();
      console.log('success');
      console.log(formData);
    }
    catch (error) {
      console.log('error');
      console.log(error)
    }
    console.log(headers);
    console.log(request);
    return new Response('test');
});
  
  /*
  This is the last route we define, it will match anything that hasn't hit a route we've defined
  above, therefore it's useful as a 404 (and avoids us hitting worker exceptions, so make sure to include it!).
  
  Visit any page that doesn't exist (e.g. /foobar) to see it in action.
  */
  
router.all("*", () => new Response("404, not found!", { status: 404 }))

addEventListener("fetch", event => {
    event.respondWith(router.handle(event.request));
});

In the createproject route, everything that’s logged comes back empty (e.g. empty objects, empty arrays) — so it’s clearly not accessing the request body or formData object as expected, or I’m not sending the request / formData correctly from the original fetch call.

Can someone help me fix this code so that I can access the request body and formData inside the createproject route?

See the thread for this user’s formdata parser.

Hey @Judge, thank you so much for following-up on this.

There are no files like images in the formData I’m trying to send. Just plain form data and some json. Would I still need to use that parser?

If so, could you perhaps point to the specific aspects of the parser which are relevant for my use case?

From this code example, I’m not exactly sure which parts are the example data being sent, and which parts are the parser

const main = async () => {
  const o = {};
  const o2 = {};
  const body = new FormData();
  o['50'] = 'data:image/webp;base64,UklGRlIFAABXRUJQVlA4IEYFAABQFQCdASoyAEsAPm00k0ekIyGhKBM7uIANiUAYP/E47JItebRUci3F6WNuxzqDNRcX/tDNVx79R2oi5iO83GSwtvS2WD6Bnz2+ODh34MAr9LxRRyWtOohZTGCza2fg8hcMAupIMZFRya5Y5SxM/oHw2ru86zpogeNUnSnxSo+zCLDLAt/pdPzrJD6E+pqwUUgucIQH7GGEJ5SBBi4qBoNvHaTKRacqewuLQUe5FQLWJHuAAAD+6Zb5OwaddpMri4OFtes/WvWfyLjN9Qbs3jrGT3MN4n4XAbnfwxPnr3Pb/qVSTRKhv9/f2VsPYTKQM//0Vl9d9tP98L+anGlXQAQ3qY6ZgkF2ZOug/WzSo0E9pEMudHwl/ZbJHWin04CCt0w4CTv4LnKBwt4pgWBfSbVhXvSB3bRFmSjNx5/9zDsAT2Yqfg1Ej2loJbdE1kWvYTS3BVKu3v8QWmd165fF3y5irwahactojWUcBJpH/XWfCxIQIGmVlhaq8YyUEMRBcBtcGTFXvzedJPMHb5EtxAYKs5zmV7RB0PYuIo2OAADq138R+SIcWNKE/jeHR7/8zZHwY/xOp/o0cLYsIaGeBnyAIu/2xzGa4Py1amVSIgYXyx4M4B1Ayp8f0GiUTMuMY0LWyPXckGTx8Vw2mGDAP923mFg/csGg/YC3RJ4pcH0lFR66DKRTFsIZYP2SylCOy3HSBXpSE5Bfqot+8SVrBE/RztjSKOxAQj6shwJ7L1Vzc28P2XhZNwikHIiZPxrOTi/w2V4/060iyoEnS+yzGL5CqDYwdjGXW/4r4pqC2wvfM7eS3UYQteQwdHjAQMTLBEDHJbFz/X5hN5he+drNy8+OuyS+ohKnwaADuW6nVrzDAsjFrunm3leKWHpBW8Hueto1OSRHMVOX/CDZGfNSoPHHuPxxeDy7jHnBGICmqLBB3wd/2lM4ytiVtZFTTgeLhuMk+5n1FIj4WjZocRR2/vKRa84xqy178t0NqHuMRCzaJ280aDpYxvI7924uLt8xx6zxB3BVdCkUc80laqT9h/zJeMNZvcl7rbkDC7aKgwp7N6uN8HDD5ZFMgMDYTvyd+3BaQQ06q4GhDIIW1yRJu/7YeLd2QfAehniYuESI1+06uLouzLCDSUtPRSpTlEkAVDsjWDbnGdz56+kMMmf+TLV0gn7/VJ/1cJi010IsakalaKFYOsJz+iYVrOB2dLHEbBbzZKnY48uMiF/MZjR/2WZ0t6fvo/WGOe/x9fwJP+YDSe/pJwfLBZ/ezri+nDmbOaxNkyKunyo5gGzBV3wLNsxnU774eXfzFn9kTsuK+yD0CEdIIBmdq7ZxOfLnHFTfElSqRd/RHzw8j6XVWjl7EVF4yNbrtksNE9OD3pXijW/qzf4rgWLdVoYRZsZzCMYu/Ixe6zAgeGVfyT3AyvPG/U3x7ebntr4/5yPiG+tptQyWm4H3Wll6Qmlc+ZNXUEk/u9b4WudYwEG3Zs1Ki66YtZ64L6zM9NcpRODsYslzHjmibGkWK7KxUOXmh3r+Y4srhwCBpFdVbEFHEg8x/4AZnJyU8zqSZu104Ilsn+9N5UbUYGYGdFFfFP0NiLo0Keuei4qL1q4G2RbYplYj10dXzi7FWGRNCr56Z9xaEPBLRCPEwj/svxhQSxD79yZ1vdLE4HQvHaLv4L0FjuOwFCFvZ31f/ZE1XTLVg2tgM12CqqD8I+TCvcHea3JOgGMggxUyR2L1/Hzt2yiWjNGUs3nw2CK6TdbZPVmHRE2C5W+NbQBUfR40Tty7bmupuzufkMTt0ACSRGbAAAA=';
  o['100'] = 'data:image/webp;base64,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';
  o['test.txt'] = 'data:text/plain;base64,YWFh'; //contains : 'aaa'
  o['test.json'] = 'data:application/json;base64,eyJKb2huIjoiRG9lIn0='; //contains : '{"John":"Doe"}'
  for (const k in o) { body.append(k, await (await fetch(o[k])).blob(), k); }
  o2['1'] = JSON.stringify({ name: 'Alice' });
  o2['2'] = JSON.stringify({ name: 'Bob' });
  o2['x'] = 'yyy';
  o2['u'] = "v\nv2";
  for (const k in o2) { body.append(k, o2[k]); }
  const res = new Response(body);
  //console.log(JSON.stringify(Object.fromEntries(res.headers)));
  //console.log(await res.clone().text());
  const event = { request: res, response: res }; // Fake event, fake request
  console.time('formData');
  const fd = await formData(event); // Returns an array [{name,value,filename,content-type}]
  console.timeEnd('formData');
  console.log(fd);
  if (!fd) { return; }
  const z = fd[1];
  const blob = new Blob([new Uint8Array(z.value, 0, z.value.byteLength)]);
  //const blob = new Blob(z.value);
  const url = URL.createObjectURL(blob);
  document.querySelector('body').insertAdjacentHTML('beforeend', `<img src='${url}' width=50>`);
  //URL.revokeObjectURL(url);
}
main();

const formData = async (event) => {
  // content-type headers must be valid and offer a boundary
  const boundary = await getBoundary(event);
  if (!boundary) { return null; }
  // Payload size must be under 1000kB
  const ab = await event.request.arrayBuffer();
  const max = ab.byteLength;
  if (!await lessThan1000kB(max)) { return null; }
  // Start to parse
  const t = new TextDecoder();
  const fd = Array();
  let o = Object();
  let i = 0;
  //console.log({ ab, i, max });
  while (i < max) {
    i = await getPart(ab, i, max, boundary, t, o);
    if (!o || !o.name) { break; }
    fd.push(o);
    o = Object();
  }
  return fd;
}

const lessThan1000kB = async (max) => {
  if (Math.ceil(max / 1000) > 1000) { return false; }
  return true;
}

const getBoundary = async (event) => {
  const ct = event.request.headers.get('content-type');
  return `--${(ct.match(/^multipart\/form\-data; boundary=(.+)$/))?.[1]}`;
}

const getPart = async (ab, start, max, boundary, t, o) => {
  let s = '';
  let i = start;
  //console.log('A');
  i = await readString(boundary, i, ab, t, max);
  if (i === max) { return max; }
  //console.log('B');
  if (await eof(i, ab, t, max)) { return max; }
  if (i === max) { return max; }
  //console.log('C');
  i = await readNewLine(i, ab, t, max);
  if (i === max) { return max; }
  //console.log('D');
  i = await readString('Content-Disposition: form-data; name="', i, ab, t, max);
  if (i === max) { return max; }
  //console.log('E');
  i = await captureUntil(['"'], i, ab, t, max, o, 'name');
  if (i === max) { return max; }
  //console.log('F');
  if (!o.name) { o = null; return max; }
  if (i === max) { return max; }
  //console.log('G');
  let x;
  if ((x = await readValue("\"\n\n", boundary, i, ab, t, max, o))) { return x; }
  if ((x = await readValue("\"\r\n\r\n", boundary, i, ab, t, max, o))) { return x; }
  return readFile(boundary, i, ab, t, max, o);
}

// Depending on carriage return prefix,
// Try to read a simple value (implicit text/plain)
const readValue = async (s, boundary, i, ab, t, max, o) => {
  if (!await checkString(s, i, ab, t, max)) { return false; }
  //console.log('I');
  i = await readString(s, i, ab, t, max);
  //console.log('I2');
  i = await captureUntil([/*`${"\r\n"}${boundary}`, */`${"\n"}${boundary}`], i, ab, t, max, o, 'value');
  //console.log('I3');
  i -= boundary.length - 1;
  return i;
}

// Read a file, like text, json or images
const readFile = async (boundary, i, ab, t, max, o) => {
  //console.log('J');
  i = await readString('"; filename="', i, ab, t, max);
  if (i === max) { return max; }
  //console.log('K');
  i = await captureUntil(['"'], i, ab, t, max, o, 'filename');
  if (i === max) { return max; }
  //console.log('L');
  i = await readString('"', i, ab, t, max);
  i = await readNewLine(i, ab, t, max);
  if (i === max) { return max; }
  //console.log('M');
  i = await readString('Content-Type: ', i, ab, t, max);
  if (i === max) { return max; }
  //console.log('N');
  i = await captureUntil(["\r", "\n"], i, ab, t, max, o, 'content-type');
  if (i === max) { return max; }
  //console.log('O');
  if (!o['content-type']) { o = null; return max; }
  if (i === max) { return max; }
  //console.log('P');
  i = await readNewLine(i, ab, t, max);
  if (i === max) { return max; }
  //console.log('Q');
  i = await readNewLine(i, ab, t, max);
  if (i === max) { return max; }
  //console.log('R');
  i = await captureUntil([`${"\r\n"}${boundary}`, `${"\n"}${boundary}`], i, ab, t, max, o, 'value', o['content-type']);
  if (i === max) { return max; }
  //console.log('S');
  i -= boundary.length - 1;
  return i;
}

const charAt = async (i, ab, t) => {
  return t.decode(ab.slice(i, i + 1));
}

// Check if end of file is found
// Meaningful only when used directly after readString(boundary)
const eof = async (i, ab, t, max) => {
  if (i + 1 === max) { return false; }
  const s = t.decode(ab.slice(i, i + 2));
  return s === '--';
}

// Try to read \n
// If \r is found, second chance is given
// If max reached, or if \n not found, return max to stop the machine
const readNewLine = async (i, ab, t, max) => {
  if (await charAt(i, ab, t) === "\r") { if (++i === max) { return max; } }
  if (await charAt(i, ab, t) === "\n") { return ++i; }
  return max;
}

const checkString = async (v, i, ab, t, max) => {
  return readString(v, i, ab, t, max, true)
}

// Try to read v
// If max reached, or if v not found, return max
const readString = async (v, i, ab, t, max, check = false) => {
  const start = i;
  let j = 0;
  const len = v.length;
  while (i < max && j < len) {
    ++i;
    ++j;
  }
  const s = t.decode(ab.slice(start, i));
  //console.log('expected : v :', v);
  //console.log(`${check ? 'check' : 'read '}${'    : s :'}`, s);
  if (s === v) { return check ? true : i; }
  return check ? false : max;
}

// Try to capture, until we get one the values inside a (Ex : \r or \n)
const captureUntil = async (a, i, ab, t, max, o, prop, contentType = 'text/plain') => {
  return readUntil(a, i, ab, t, max, true, o, prop, contentType);
}

// Try to read, until we get one the values inside a (Ex : \r or \n)
const readUntil = async (a, i, ab, t, max, capture = false, o = Object(), prop = '_', contentType = 'text/plain') => {
  const start = i;
  // Until end
  while (i < max) {
    //console.log(i, await charAt(i, ab, t));
    // For each token
    for (const v of a) {
      const end = Math.max(start, i - v.length);
      // Extract ending text, at the size of the token
      const tmp = t.decode(ab.slice(end, i));
      // If extracted ending text not equals to the token, do nothing
      if (tmp !== v) { continue; }
      // Capture data
      if (capture) { await captureData(ab, start, end, t, o, prop, contentType); }
      // Return current position
      --i;
      //console.log('readUntilOut1', i);
      return i;
    }
    // Go to next char
    ++i;
  }
  // console.log('readUntilOut2', i);
  return i;
}

const captureData = async (ab, start, end, t, o, prop, contentType) => {
  //console.log('capture', prop, contentType);
  //console.log(start, end);
  const sub = ab.slice(start, end);
  await captureJson(sub, t, o, prop, contentType);
  await captureTextAsJson(sub, t, o, prop, contentType);
  await captureText(sub, t, o, prop, contentType);
  await captureBlob(sub, o, prop, contentType);
}

const captureJson = async (sub, t, o, prop, contentType) => {
  if (contentType !== 'application/json') { return; }
  let s = t.decode(sub);
  //console.log('captureJson', s);
  o[prop] = JSON.parse(s);
}

const captureTextAsJson = async (sub, t, o, prop, contentType) => {
  const s = t.decode(sub);
  if (contentType !== 'text/plain' || s[0] !== '{') { return; }
  //console.log('captureTextAsJson1');
  let o2;
  //try { o2 = JSON.parse(s); } catch (e) { }
  try { o2 = JSON.parse(s); } catch (e) { }
  if (!o2) { return; }
  o[prop] = o2;
  o['content-type'] = 'application/json';
  //console.log('captureTextAsJson2', s);
}

const captureText = async (sub, t, o, prop, contentType) => {
  //console.log('captureText1', contentType);
  if (contentType !== 'text/plain' ||
    o['content-type'] && o['content-type'] !== 'text/plain') { return; }
  let s = t.decode(sub);
  //console.log('captureText2', s);
  o[prop] = s;
}

const captureBlob = async (sub, o, prop, contentType) => {
  if (contentType === 'text/plain' || contentType === 'application/json') { return; }
  //console.log('captureBlob');
  o[prop] = sub;
}