Summary
Cloudflare siteverify tokens are single-use. The lead form consumes the token on the first submit; if the overall request fails, the form re-enables Submit with the same token, so every retry fails timeout-or-duplicate until the widget self-expires. The "try again" copy cannot succeed. The widget also caches a rejected script-load promise module-globally, so one api.js network failure disables the widget for the session.
Evidence
apps/user-application/src/components/leads/lead-capture-form.tsx:30-34,108-118 — only onSuccess resets; error path keeps turnstileToken.
apps/user-application/src/components/leads/turnstile-widget.tsx:23-39 — module-global scriptPromise cached even when rejected; no reset() exposed to the parent.
Proposed failing test (TDD)
lead-capture-form.test.tsx — "resets the turnstile widget after a failed submit": mock submitLead rejecting once and window.turnstile.reset as a spy; submit, await error; assert turnstile.reset was called (or the token was cleared so Submit is disabled until a fresh token). Fails today.
Fix direction
On mutation error, call turnstile.reset() and clear the token; do not cache a rejected script promise.
Summary
Cloudflare siteverify tokens are single-use. The lead form consumes the token on the first submit; if the overall request fails, the form re-enables Submit with the same token, so every retry fails
timeout-or-duplicateuntil the widget self-expires. The "try again" copy cannot succeed. The widget also caches a rejected script-load promise module-globally, so oneapi.jsnetwork failure disables the widget for the session.Evidence
apps/user-application/src/components/leads/lead-capture-form.tsx:30-34,108-118— onlyonSuccessresets; error path keepsturnstileToken.apps/user-application/src/components/leads/turnstile-widget.tsx:23-39— module-globalscriptPromisecached even when rejected; noreset()exposed to the parent.Proposed failing test (TDD)
lead-capture-form.test.tsx—"resets the turnstile widget after a failed submit": mocksubmitLeadrejecting once andwindow.turnstile.resetas a spy; submit, await error; assertturnstile.resetwas called (or the token was cleared so Submit is disabled until a fresh token). Fails today.Fix direction
On mutation error, call
turnstile.reset()and clear the token; do not cache a rejected script promise.