POST body lost when JS challenge falls back to CAPTCHA?

I have a Firewall Rule configured to trigger a “Challenge (Captcha)” on a form submission target, to prevent spam. This works fine, but the CAPTCHA is annoying, so I wanted to try changing it to a “JS Challenge”. When I tested this in Firefox, the JS Challenge failed and fell back to a CAPTCHA. However, looking afterwards at the submitted form data, it seems the submission didn’t go through correctly; it went through, once I passed the CAPTCHA challenge, but the submitted form data seemed to have been lost at some point in the process.

I tried a few times and it seems this only happens when the JS challenge fails and falls back to the CAPTCHA. If the JS challenge succeeds, the form is submitted as expected. I haven’t been able to make the JS challenge fail in other browsers, so I can’t rule out the possibility that this is a Firefox bug.

Below is the form submission handler (it’s a Cloudflare Worker). The actual result I got was that an object was stored in KV with just a timestamp and IP, which suggests that the request method was correctly retained (as a GET would have been rejected entirely), but readFormFields(request) returned an empty object, which I think means the POST body fields must have disappeared.

import { uuid } from '@cfworker/uuid';

const redirectURL = ''

addEventListener("fetch", event => {
  if (event.request.method === "POST") {
    return event.respondWith(handleRequest(event.request));
  else  {
    return event.respondWith(new Response(`Method not allowed`, { status: 405 }));

async function handleRequest(request) {
  // Get the submitted message.
  const now = new Date().toISOString();
  const message = JSON.stringify({
    timestamp: now,
    ip: request.headers.get("cf-connecting-ip"),
    ...await readFormFields(request),

  // KV requires a key.
  const key = uuid();

  // Write the message to KV.
  await MESSAGES.put(key, message);

  // Redirect.
  return Response.redirect(redirectURL, 302);

function filterFields(raw, allowed) {
  return Object.keys(raw)
    .filter(key => allowed.includes(key))
    .reduce((obj, key) => {
      return {
        [key]: raw[key]
    }, {});

async function readFormFields(request) {
  const { headers } = request;
  const contentType = headers.get("content-type") || "";
  const expectedFields = ['name', 'mail', 'subject', 'message'];

  if (contentType.includes("application/x-www-form-urlencoded")
      || contentType.includes("multipart/form-data")) {
    const formData = await request.formData();
    const body = {};
    for (const entry of formData.entries()) {
      body[entry[0]] = entry[1];
    return filterFields(body, expectedFields);

This topic was automatically closed after 30 days. New replies are no longer allowed.