From dd09d38cf788ca0973037c49dcd86911dac6eec6 Mon Sep 17 00:00:00 2001 From: bsid Date: Wed, 4 Feb 2026 13:10:47 -0700 Subject: [PATCH] fix(clerk-js): Add retry logic and race condition fixes for Cloudflare captcha error 200100 This fixes Cloudflare Turnstile error 200100 ("Widget not found") which occurs when the captcha container element isn't available during rendering. Changes: - Add '200' to shouldRetryTurnstileErrorCode to retry all 200xxx errors (including 200100) - Add waitForElement() call before captcha.render() to verify container exists - Update smart widget initialization to use waitForElement() instead of getElementById() - Add test coverage for error codes 200, 200100, and 200xxx These changes prevent race conditions where DOM elements are removed or not ready between checks and render calls, particularly during React/framework re-renders. Co-Authored-By: Claude Sonnet 4.5 --- .../clerk-js/src/utils/__tests__/captcha.test.ts | 4 +++- packages/clerk-js/src/utils/captcha/turnstile.ts | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/clerk-js/src/utils/__tests__/captcha.test.ts b/packages/clerk-js/src/utils/__tests__/captcha.test.ts index f2e65933bd9..cec37f051e4 100644 --- a/packages/clerk-js/src/utils/__tests__/captcha.test.ts +++ b/packages/clerk-js/src/utils/__tests__/captcha.test.ts @@ -14,9 +14,11 @@ describe('shouldRetryTurnstileErrorCode', () => { ['104xxx', true], ['106xxx', true], ['110600', true], + ['200', true], + ['200100', true], + ['200xxx', true], ['300xxx', true], ['600xxx', true], - ['200010', false], ['100405', false], ['105001', false], ['110430', false], diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 40cce095d46..f9378e77ec3 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -23,7 +23,7 @@ declare global { } export const shouldRetryTurnstileErrorCode = (errorCode: string) => { - const codesWithRetries = ['crashed', 'undefined_error', '102', '103', '104', '106', '110600', '300', '600']; + const codesWithRetries = ['crashed', 'undefined_error', '102', '103', '104', '106', '110600', '200', '300', '600']; return !!codesWithRetries.find(w => errorCode.startsWith(w)); }; @@ -117,7 +117,8 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { // smart widget with container provided by user if (!widgetContainerQuerySelector && widgetType === 'smart') { - const visibleDiv = document.getElementById(CAPTCHA_ELEMENT_ID); + // Use waitForElement to ensure the element is ready, similar to modal approach + const visibleDiv = await waitForElement(`#${CAPTCHA_ELEMENT_ID}`).catch(() => null); if (visibleDiv) { captchaTypeUsed = 'smart'; captchaWidgetType = 'smart'; @@ -147,8 +148,15 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { } const handleCaptchaTokenGeneration = async (): Promise<[string, string]> => { - return new Promise((resolve, reject) => { + return new Promise(async (resolve, reject) => { try { + // Re-verify element exists right before render to prevent 200100 errors + const containerElement = await waitForElement(widgetContainerQuerySelector); + if (!containerElement) { + reject(['Widget container element not found', undefined]); + return; + } + const id = captcha.render(widgetContainerQuerySelector, { sitekey: turnstileSiteKey, appearance: 'interaction-only',