Workers KV multiple image upload - Custom formData parser - Tutorial

KV multiple image upload - Tutorial

GET

The get should be done with KV bindings (Fast, 30 ms), in arrayBuffer mode

const value = await KV.get(k, 'arrayBuffer');

PUT

You have 2 possibilities :

  • Use the KV API to put the file in KV (Extremely slow, 1.25 s)
  • Use a formData parser to get the file, then use the KV bindings to put the file in KV (Relatively fast, 50-60 ms)

Problem : Response.formData() does not work

Unfortunately, handling multipart/form-data with binary files like images by using response.formData() does not work in Cloudflare Workers. The response goes through a utf-8 transformation which corrupts the files and make them unreadable. In addition, you lose the filename information, which sometimes is important.

Solution : Custom formData parser

I wrote a custom formData parser in JavaScript to do the job. It does not respect the standard API, but it does the job roughtly. It handles binary files like images, text and json files, and simple form values, even those on multiple lines. That should be enough for most usages. Feel free to alter to match your needs.

Try to copy / paste this JS code snippet in your Chrome console:

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,UklGRk4NAABXRUJQVlA4IEINAABQOgCdASpkAJYAPm0wkkckIqGhKTc8oIANiUAXwUYrI+0/dGjLuEHDgl2U/Ubt/edM08OVS+8P9Z4R+ZgKPs51I++PHHwzmJcM9SrNf8v/RWyn+GIWM9gn2NXLQ1LK8V2YW/uu5XIK0YS/Y0hCsVWf+q/vI03Uxj/em4xizoZhOCc4v98GaALEOdGFr5GEV4sWWZY5bXuT+UV+aMB7hXNS/SZJjEuiyROlwbS7jOYGq26ssd+T+e5W0YwkrH96or2EPFFMzYMVmu77K+bvB1WlcUf449jlcbxp3TopJzjuZeoWLO9ee974DP8Cpnt/Rh3DhVuuCnkLlndRMZg0OI+msoc31F/SLwwP7LgxoKx3yfvIXbBpA1k7ywh5N1XBLsyfhMNwrRB74LfgvjLu7ovCatdt75QMva/XTr6Dr1ExY328dRIJ6C3zzVdhybvBMOn4jpbkdzkFOTDH+VP4F51AyzAsbCt9VjLhk+vmkah40mRa4H2zQuIHrhHsoNqX8Qqp6hjLiNfOUerOAJnQiHFSdHS7dqcPLLoRd0dE+vIJFeRf5NzSoS8WgoZi9V2Cnd/ON0QaVa1g7oQPK9qBzYjG8CIMvTfRkA8+YsYeyBx/mW2wfG0EmPnIHXXAAP7517m+fSIFOH+hvxJkGpcY/mNl2lfwfuq2vJDpD6ozGBuEU2CdFkPOEG7d31wPhxa5B58bW9OCmEKtRsWUNwf6HZ12zksP6InLurlpY1Y2fgnGK0k/Bo9BVr8+gbE++7Ocw4wd5xfwvXckH0aXALHDsF5iSeP1/EFxkGKblExgzAf3ePVZuNOY42oeT0sEa+9wyQHX1VIhBS4wQKUlJr9E3oukIn5COGKivDhIg3xLINE2Bq4xZZwdNxiak18eZ+e8YuMAoPHfCBesQNIIhRLduI6vh9ncJBvF6edD33Y70k0RrA/XJlj3LWOQBetQXt//V0djOM/JtizAKF9LUWA+u7TXX9+hPIZX9OxMTrjqw/pWX6m+5O+JWmFnbls+0mfdJPchazECmZPBRGDYmcwkMAKL9pMXv5wk3aYjfCYS6bSggo7V7gmUhxk4BrjbI0TmGTZxwcrSDCf5+cktjo3fWsryDd7eUboc8It675kEu44Idl59YsZJFrp4BS1dZLA83Wh05l9Yv22AJmU+6NWMKwHA4eYsREAy94qI+NpgzuhoY+Xz3SPPQ+Ej1/oU/fks3EVrJEhyptHDEH3QKgtRc0OxvqYwgq3Y3HXWSJ79myXh7ZAjAoFniixnumxifjlZ6RhxPB2/S8HsJY/eGAPZa7w2xRRhCGn+IJXxDR7KwQlaovf8M8iVAO1nvhksJu3U2ZtUH2L/OV3yXdAxhEq0PY6wR/vQym9lVPleuE8Zli91X+vdmn7yKEDE6G70C8hd1x9Z4+oIOXV8luslI8NqdszVeFxUMc5WzJvzuX+XnNcbSEW1T0COT69oZD6lAYpj0PAVE8jYL8GczAVq9jsaxcvNjbAhrvPFTgvFnv8P37qhiBE+ICzCgODiZRBVazvL0ifrvLesMvH+wUED0R5Z7ZN3v4gEOeYA273VwYaQ6bcJ50Soox9SykyyoCvx84NcdiOgSXtGTObSU+gCXn052YFDL4wXIBYLrZMGkDtups6CAT2BjS7738aKDiisprtjzwRT+lX6dgVYusfqpzJIqcsyJ/mI8/bT+2DwjF0yyuQkB62Wo3WkUN+0uTJcjTsVVIj86vJXZjj0iFU1lKm79N71IGpRditcnrskJAzsXv3iqAuSH/sk+Aq295j2wSKA2MQ7D82QFkOzQmTN11XtTPAAsO5+6hBRsYuecZy/3TpDXXlrUu8pKWhxa59QcrsGBaULNpwH9m/br1/sCxzDZ2tFLiMbumWD6dFku1te2V4ZfwpUM1pUk3J6mep5XQl3lRYo004ASnLM8tPZwChGRmzZNJrZdOVNs8vl7O3lvT5hoiKVGGkhedY1DWWLpWeaTaxrhGTWBb/AkE7UiuxzAv9NWfCYmBNWRhSIXZKswxaaMbaOKbmJsyDx7kMNlGjvga1+mFBHOVZDWsfginPZPjUSLmF5y7hlLDfD/kkfuuRum49In2WAe+Mj3KxeU294DjIriv5J2VbdBZjXb3ecYLZHwwb2pH4NqQo5ZctMy8nQ2F1A8CtRRUD/pxiJYfppk1TZOcwKxK/PPQ31izf9ob5CT5INKOl1qfdzXrziItkzxGVZUhWGbqSJO/aYBR9Fvx43SbDtLWJw5oo6yXVvR2hdpnkMUBqPtFtm9S/l5V1XOaOvy28trvG1OBnzutkmwNys1rXOsbxswgVjCl4aqAyv62Ostbo2tBL/fqHR4cojdM5ul+DKpOQtK1X8xl+DVAhcFGrSqvfb99F9YicNZLkHjfS39G+Hhxr9f8Jn3rhCyaYqqBO61i/vUC/c96ySwWO0aeQfYTsmZGxllyRvwWKBnbMQ8O+0ehf2GVpD2MVlhSUNzNzGGl4aEDrPTXR3ONzQWfWzKJHAioraehc531mgUDL4S5a9/H7LKUvd/ItkOsMAkG1vCBzhTz58wxCVOKmG3253VPemdstCbL9ybNVPyrO+xIcj4HGCxfM76vipYH6bYiZz41Bpf92x8q01s4a4iGQEp3yBpfWo9IuFEZWISYmx+gUZqsTUkU6tTI7xEQJmqJ8b/2H/GTwSxRgfMIgY0dAN4sspvpNO2cqipQhMn2piU2/BdA6/PMeR7TTPmZwtGv33tVL8uTN3rFAVCEZyWHV4p9WIcpHxAVnKR8A/XVppIT3WTLYYoqAHW9cjaZsRVN0PiXJkyqk8CjuFsxv+j2MnkWNLo2PtqVEc7yeKiz1jG6WCPiZgdtDKOgIlOwTKH9dtlQizaNUsQmekUYGh8l//MTprQSYLDnAipe8SPt/IdAoqFmpnZxtEnvHEUkPxHKB3zdlobfhsUoPqTDsjDZtqcvctOmkaepHqqvgiqk9hYJCHhgkXNPq+Yd0GTLOo1vcOEC9t0z9PLT+zkk0YlHcLmicAC9wV9o+JKrBrIy0aC93LkHEtTCQuDfT+Nf3EHeJkGTUl0VqG3KMqs9XOkoGS04TPoSctGGe208PHvfkzNK6DsUbTxYsNrCQld5gOJU5jP5qNOKY/dkcn+tOVQQlNnubpVyNB9U4NtkJf9TdNWdx6AMEnPuDoH7q0G4+6nQheqJcG4OD2QIxWdfxC4TqvYZ5tE+aIEeb1QvbQDTM0cDf76NgegB8WuElKbnb4WfzKulVdz3PUFQUl1zs16dgQmIc9K5zLLGhsFfvEMgHqKjvoy43lcUdYvba08vpQBnnWglCLnCRlSOTtntISq/OMAZk0fR0+sfOkNyXmwRpdkjbRJzA/EL6KB78bqQxqN9c+GFY03EdDSBwk6QOQv0dngR8N1QuzpNEchpTKba7EMAZSPoZd2dLDzj5RkHRJ4J5aurc8WkN6Uo0VrNMjG3es5+SyLIzUkP6itJrFt5IkuH1HXt+CvcIhf6NlGHxSfgoviPArTCI6tEAO9qqiO8B3xXi0tRPMwCYzmb9l8P45mUuuu4pj4xF5ab75xpUiYAzmKzWu+a9SESKfHWaB99XO1SNdPO0i37qjTgVRw7j5+/rJf7OBAmSh0PgeG5IPfWCtdUatnPoVkUqoCJ7W6swl9IncozLAthwLSKNQ6F3NFrHgfRGG/hVWyt90ClpZS4LWwnNRwUatWoahL2zuJkRWdw4OkeFC5YzxEg4WRV6YHsbJ4QdHfm3cx2bt3rWlinnoCf1aQ6lMjbGRMWPz96hsITho79NuhCb3Xdwy688yyEdF5NQbJ/EuSB4HnPaGN2S7GU8flaoKgt4nTzfga975E77/NrpRo7fh28/+TjHzmTu2wBD0bcEWZRgDXM/Q6ep0QdOCczWSe6zYDpVJM6xCdfaKji5t7uN7j75qttydJwuYDDc3xTj0lcTW4iZIKg/lRm1H2JvRK3Fj3e7TVR+xlS7jBh3tMyZW9/g8dRVgN64PUvym/rV0ia7gwWsTvgz8oe60CrliRoc1Zu09IcpzVYtWMKSKjoVllHK7DK0+GWwUoYf/IMBcM/qxnodxjR0svLx9R7pAu1h7MpRugCVXFhaXTioLF2Qk3kK+LAasoe6b0Qlfl8c8Eb4cqiTOm3nXHidYJ2yBZUO7X1SbpqjOvQtQUW4ld719/hZw82/GTF3f0vPxGPpnzUWmufNkPwxXsNizdyEQ/ONg757yjIMvVsb8dzmdICaBfWH0u8Jg4vGcQle0pE2LEaCALd+RIhf+PPJmTP2AQdaOAWbqgjZE0YksRilGwDC6LukB53pXkWkztZTLlF5pcfEf8uuTv1HkZEfV2t/BmxOmVDtoRYKC2RbYGtgn/Tov1XH+3ZiNLfV5fUXk/+NojDU2cSy3kHSiZEOrEz09UES7NopIZMGdKR8uHbGB0oe6g/vTYugK/1EI3n5mjw2/veF3F/eYr/5Uh7kA7PNOg2RdkjQbxY6UTREVFkykGmrXnmwy7jMlZIM19nKfhTcBgoxAAAAA';
  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;
}

It should display :

0: {name: "50", filename: "50", content-type: "image/webp", value: ArrayBuffer(1370)}
1: {name: "100", filename: "100", content-type: "image/webp", value: ArrayBuffer(3414)}
2: {name: "test.txt", filename: "test.txt", content-type: "text/plain", value: "aaa"}
3: {name: "test.json", filename: "test.json", content-type: "application/json", value: {…}}
4: {name: "1", value: {…}, content-type: "application/json"}
5: {name: "2", value: {…}, content-type: "application/json"}
6: {name: "x", value: "yyy"}
7: {name: "u", value: "v↵v2"}

… indicating the form data has been successfully parsed, in 39 ms.

Now, with KV bindings you just have to :

  • PUT the value into KV
  • GET the value from KV using the arrayBuffer mode

And voilà ! :grinning:

6 Likes

This doesn’t timeout over the Worker limit?

The FormData format itself is limited since you can only read it sequentially (byte after byte). So yes, it is slow for multiple entries. 39 ms is at the limit if you do some KV sets after, but in my experience it still works as I didn’t see any hard limit (tried up to 70ms at peak).

For increased performance, I switched on another methodology :

Client-side : Read all images, transform them as an arrayBuffer, concat them as an arrayBuffer, and fill the content type header with custom data : width of images and their respective size in terms of ArrayBuffer length.

CloudFlare Workers : Read data as ArrayBuffer, read the content type header, and, since I know the boundaries, process all images asynchronously (Promise.all).

This is a lot faster, like 10ms.

1 Like

Would love an updated faster version of this :slight_smile:

1 Like