From aa1f884a1670ccfb2f3210b74727ee9df9b710cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 4 Sep 2026 05:23:22 +0000 Subject: [PATCH] Fix production Turnstile bot-check by skipping turnstile.ready() Production loads api.js with async, and Cloudflare throws if ready() is called on that script. The gate treated that throw as a failed challenge, so Run always showed "Could not complete bot check". Wait for render/execute instead; they are already enough. Co-authored-by: Max Schmitt --- frontend/src/turnstile.spec.ts | 13 +++++++++++++ frontend/src/turnstile.ts | 17 +++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/src/turnstile.spec.ts b/frontend/src/turnstile.spec.ts index 259e4dc..9a5c8b5 100644 --- a/frontend/src/turnstile.spec.ts +++ b/frontend/src/turnstile.spec.ts @@ -62,6 +62,19 @@ test('cloudflare gate returns empty when the API is missing', async () => { expect(await gate.getToken(container)).toBe('') }) +test('cloudflare gate ignores ready() throwing from async/defer api.js', async () => { + const { api, options } = mockApi({ + ready: () => { + throw new Error('[Cloudflare Turnstile] Remove async/defer from the Turnstile api.js script tag before using turnstile.ready().') + }, + }) + api.execute = () => { + options.at(-1)?.callback?.('tok') + } + const gate = new CloudflareTurnstileGate('sitekey', { getApi: () => api }) + expect(await gate.getToken(container)).toBe('tok') +}) + test('cloudflare gate does not hang when render throws', async () => { const gate = new CloudflareTurnstileGate('sitekey', { getApi: () => ({ diff --git a/frontend/src/turnstile.ts b/frontend/src/turnstile.ts index 8c968d3..683c675 100644 --- a/frontend/src/turnstile.ts +++ b/frontend/src/turnstile.ts @@ -48,16 +48,6 @@ function isUsableApi(api: TurnstileApi | null | undefined): api is TurnstileApi return typeof api?.render === 'function' && typeof api?.execute === 'function' } -function whenReady(turnstile: TurnstileApi): Promise { - return new Promise((resolve) => { - if (typeof turnstile.ready === 'function') { - turnstile.ready(() => resolve()) - return - } - resolve() - }) -} - export class CloudflareTurnstileGate implements TurnstileGate { readonly mode = 'cloudflare' as const private widgetId: string | null = null @@ -166,10 +156,9 @@ export class CloudflareTurnstileGate implements TurnstileGate { done('') return } - await whenReady(api) - if (settled) { - return - } + // Do not call turnstile.ready(): with async/defer api.js it throws + // "Remove async/defer ... before using turnstile.ready()", which + // used to settle this promise with an empty token on production. const alreadyRendered = Boolean(this.widgetId) if (!this.widgetId) { this.widgetId = api.render(container, {