Skip to content

Instantly share code, notes, and snippets.

@hjanuschka
Created May 13, 2026 13:13
Show Gist options
  • Select an option

  • Save hjanuschka/3474bf66c6631eb7ad126eb15491e8e2 to your computer and use it in GitHub Desktop.

Select an option

Save hjanuschka/3474bf66c6631eb7ad126eb15491e8e2 to your computer and use it in GitHub Desktop.
Newsletter form captcha submit – A/B proof (RED/GREEN)

Newsletter Form – Captcha Submit A/B Proof

Short verification of the current integration on https://www.alstertal-einkaufszentrum.de/newsletter-registration/unsubscribe/?subId=… (same code path as Rheinparkcenter and other ECE centers).

TL;DR

The captcha SDK works correctly. The intermittent rejection comes from a custom submit listener that is registered before KROT.interceptForm(form) and calls e.stopImmediatePropagation(). When that branch is taken, the SDK's own submit listener never runs, so no token is computed and no token is sent.

Removing that extra listener (or registering it after interceptForm) makes the flow deterministic.


Current integration (simplified)

form.addEventListener('submit', function (e) {
  if (validForm !== true) {
    e.preventDefault();
    e.stopImmediatePropagation();   // ← stops SDK submit listener
  }
});
KROT.interceptForm(form);

Listener order on the form:

  1. validity guard (registered first → runs first)
  2. SDK interception (registered second → runs second, only if propagation continues)

If validForm is not true at the moment of submit (initial click, async validator state, Enter key path, re-submit after token write, …), the SDK listener is skipped.


🔴 RED – reproducing the failure on the live form

Paste in DevTools console on the page, do not modify the DOM:

(async () => {
  while (!window.KROT) await new Promise(r => setTimeout(r, 100));
  const form = document.querySelector('form:has(.captcha_at_hidden_field)');
  const inp  = form.querySelector('.captcha_at_hidden_field');

  const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
  Object.defineProperty(inp, 'value', {
    get() { return desc.get.call(this); },
    set(v) { console.log('[hidden set]', { len: String(v).length }); return desc.set.call(this, v); },
  });
  const origSubmit = form.submit.bind(form);
  form.submit = function () { console.log('[final submit]', { hiddenLen: inp.value.length }); return origSubmit(); };
  const origFetch = window.fetch;
  window.fetch = function (...a) { console.log('[fetch]', String(a[0]).slice(0, 80)); return origFetch.apply(this, a); };

  console.log('armed – submit the form now');
})();

Force the broken state right before clicking submit:

window.validForm = false;

Observed:

  • no https://www.captcha.eu/challenge?… request
  • no [hidden set] with a large value
  • no [final submit]
  • form does nothing OR posts with the placeholder hidden value

🟢 GREEN – proving the fix on the same page

Same page, fresh reload. This snippet removes the existing submit listeners by cloning the form, then re-attaches SDK-native interception:

(async () => {
  while (!window.KROT) await new Promise(r => setTimeout(r, 100));

  const oldForm = document.querySelector('form:has(.captcha_at_hidden_field)');
  const newForm = oldForm.cloneNode(true);
  oldForm.parentNode.replaceChild(newForm, oldForm);
  KROT.interceptForm(newForm);   // no second argument

  const inp = newForm.querySelector('.captcha_at_hidden_field');
  const desc = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value');
  Object.defineProperty(inp, 'value', {
    get() { return desc.get.call(this); },
    set(v) { console.log('[hidden set]', { len: String(v).length, preview: String(v).slice(0,40) }); return desc.set.call(this, v); },
  });
  const origSubmit = newForm.submit.bind(newForm);
  newForm.submit = function () { console.log('[final submit]', { hiddenLen: inp.value.length }); return origSubmit(); };

  console.log('armed – submit the form now');
})();

Observed (verbatim from live test):

[fetch] https://www.captcha.eu/challenge?publicSecret=…
[hidden set] { len: 1796, preview: '{"challenge":"YJD0yT1ojbj/ocJHkbWPR5vHiU' }
[final submit] { hiddenLen: 1796 }
Navigated to /newsletter-registration/unsubscribe/200/

Deterministic across:

  • mouse click on submit button
  • Enter key in input field
  • repeated submits

Recommended change

In the page template, replace:

form.addEventListener('submit', function (e) {
  if (validForm !== true) {
    e.preventDefault();
    e.stopImmediatePropagation();
  }
});
KROT.interceptForm(form);

with one of:

Option A – drop the extra listener (preferred):

KROT.interceptForm(form);

and gate validation via the submit button (button.disabled = !validForm) or HTML5 constraints.

Option B – keep validation, but don't break SDK propagation:

KROT.interceptForm(form);
form.addEventListener('submit', function (e) {
  if (window.validForm !== true) e.preventDefault();
  // no stopImmediatePropagation, no stopPropagation
});

Also, in the footer-newsletter template, this conditional is always false and should be removed or fixed:

if ('newsletter-footer-captchaeu' == 'newsletter-leasing-captchaeu') { validForm = true; }

Validation checklist after rollout

  1. Mouse click → POST body contains a long token in the captcha hidden field
  2. Enter key in email field → same
  3. Repeated submit attempts on the same page load → still works
  4. /unsubscribe/200/ (or equivalent success page) is reached
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment