cloudflare-turnstile by jezweb/claude-skills
npx skills add https://github.com/jezweb/claude-skills --skill cloudflare-turnstile状态 : 生产就绪 ✅ 最后更新 : 2026-01-21 依赖项 : 无 (可选: @marsidev/react-turnstile for React) 最新版本 : @marsidev/react-turnstile@1.4.1, turnstile-types@1.2.3
近期更新 (2025) :
rerenderOnCallbackChange 属性以解决 React 闭包问题# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
# Copy sitekey (public) and secret key (private)
# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')
const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP')) // REQUIRED - see Critical Rules
const result = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{ method: 'POST', body: verifyFormData }
)
const outcome = await result.json()
if (!outcome.success) return new Response('Invalid', { status: 401 })
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键要点:
隐式 (页面加载时自动渲染):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess"></div>
显式 (为 SPA 提供程序化控制):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId) // Reset widget
turnstile.getResponse(widgetId) // Get token
React (使用 @marsidev/react-turnstile):
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />
✅ 调用 Siteverify API - 服务器端验证是强制性的 ✅ 使用 HTTPS - 切勿通过 HTTP 进行验证 ✅ 保护密钥 - 切勿在前端代码中暴露 ✅ 处理 Token 过期 - Token 在 5 分钟后过期 ✅ 实现错误回调 - 优雅地处理失败情况 ✅ 使用测试密钥进行测试 - 测试 sitekey: 1x00000000000000000000AA ✅ 设置合理的超时时间 - 不要无限期等待验证 ✅ 验证 action/hostname - 当指定时检查额外字段 ✅ 定期轮换密钥 - 使用仪表板或 API 轮换密钥 ✅ 监控分析数据 - 跟踪解决率和失败情况 ✅ 始终将客户端 IP 传递给 Siteverify - 使用 CF-Connecting-IP 头(Workers)或 X-Forwarded-For(Node.js)。Cloudflare 在 2025 年 1 月曾短暂强制执行严格的 remoteip 验证,导致未传递正确 IP 的网站大规模失败
❌ 跳过服务器验证 - 仅客户端验证 = 安全漏洞 ❌ 代理 api.js 脚本 - 必须从 Cloudflare CDN 加载 ❌ 重复使用 Token - 每个 Token 仅能使用一次 ❌ 使用 GET 请求 - Siteverify 仅接受 POST ❌ 暴露密钥 - 仅将密钥保存在后端环境中 ❌ 信任客户端验证 - Token 可以被伪造 ❌ 缓存 api.js - 未来的更新会破坏你的集成 ❌ 在测试中使用生产密钥 - 请使用测试密钥代替 ❌ 忽略错误回调 - 始终处理失败情况
此技能可预防 15 个已记录的问题:
错误 : Turnstile 分析仪表板中 Token 验证为零 来源 : https://developers.cloudflare.com/turnstile/get-started/ 发生原因 : 开发者仅实现了客户端小部件,跳过了 Siteverify 调用 预防措施 : 所有模板都包含使用 Siteverify API 的强制性服务器端验证
错误 : 延迟提交的有效 Token 返回 success: false 来源 : https://developers.cloudflare.com/turnstile/get-started/server-side-validation 发生原因 : Token 在生成 300 秒后过期 预防措施 : 模板记录了 TTL 并在过期时实现了 Token 刷新
错误 : 安全绕过 - 攻击者可以验证自己的 Token 来源 : https://developers.cloudflare.com/turnstile/get-started/server-side-validation 发生原因 : 密钥硬编码在 JavaScript 中或在源代码中可见 预防措施 : 所有模板都展示了仅在后端使用环境变量进行的验证
错误 : API 返回 405 Method Not Allowed 来源 : https://developers.cloudflare.com/turnstile/migration/recaptcha 发生原因 : reCAPTCHA 支持 GET,Turnstile 要求 POST 预防措施 : 模板使用 POST 并附带 FormData 或 JSON 请求体
错误 : 错误 200500 - "加载错误:无法加载 iframe" 来源 : https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 发生原因 : CSP 阻止了 challenges.cloudflare.com iframe 预防措施 : 技能包含 CSP 配置参考和 check-csp.sh 脚本
错误 : 合法用户的通用客户端执行错误 来源 : https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 发生原因 : 未知 - 似乎是 Cloudflare 端的问题 (2025) 预防措施 : 模板实现了错误回调、重试逻辑和备用处理
错误 : 小部件因"配置错误"而失败 来源 : https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 发生原因 : 小部件配置中缺少或删除了主机名 预防措施 : 模板记录了主机名白名单要求和验证步骤
错误 : 当 Safari 的"隐藏 IP 地址"功能启用时出现错误 300010 来源 : https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 发生原因 : 隐私设置干扰了挑战信号 预防措施 : 错误处理参考文档记录了 Safari 的解决方法(禁用隐藏 IP)
错误 : 在成功动画期间验证失败 来源 : https://github.com/brave/brave-browser/issues/45608 (2025年4月) 发生原因 : Brave 防护盾阻止了动画脚本 预防措施 : 模板在动画完成前处理成功状态
错误 : @marsidev/react-turnstile 破坏了 Jest 测试 来源 : https://github.com/marsidev/react-turnstile/issues/112 (2025年10月) 发生原因 : 与 Jest 的模块解析问题 预防措施 : 测试指南包含 Jest 模拟模式和测试 sitekey 的使用
错误 : 错误 110200 - "未知域名:域名不允许" 来源 : https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 发生原因 : 在开发环境中使用了生产小部件,但 localhost 不在白名单中 预防措施 : 模板在开发中使用测试密钥,记录了 localhost 白名单要求
错误 : success: false 并附带"token already spent"错误 来源 : https://developers.cloudflare.com/turnstile/troubleshooting/testing 发生原因 : 每个 Token 只能验证一次。Turnstile Token 是单次使用的 - 验证后(无论成功或失败),Token 即被消耗,无法重新验证。开发者必须显式调用 turnstile.reset() 以为后续提交生成新的 Token。 预防措施 : 模板记录了单次使用限制和 Token 刷新模式
// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)
async function handleSubmit(e) {
e.preventDefault()
const token = formData.get('cf-turnstile-response')
const result = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ token })
})
// Reset widget regardless of success/failure
// Token is consumed either way
if (turnstileRef.current) {
turnstile.reset(turnstileRef.current)
}
}
<Turnstile
ref={turnstileRef}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={setToken}
/>
错误 : 106010 - 在 Chrome/Edge 浏览器中首次加载小部件时出现"通用参数错误" 来源 : Cloudflare 错误代码, 社区报告 发生原因 : 未知的浏览器特定问题,影响 Chrome 和 Edge 在首次页面加载时。控制台显示对 https://challenges.cloudflare.com/cdn-cgi/challenge-platform 的 400 错误。Firefox 不受影响。后续页面重新加载工作正常。 预防措施 : 实现带有自动重试逻辑的错误回调
turnstile.render('#container', {
sitekey: SITE_KEY,
retry: 'auto',
'retry-interval': 8000,
'error-callback': (errorCode) => {
if (errorCode === '106010') {
console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
// Auto-retry will handle it
}
}
})
解决方法 : 页面重新加载后小部件工作正常。自动重试设置在大多数情况下可以解决。在无痕模式下测试以排除浏览器扩展的影响。检查 CSP 规则以确保允许 Cloudflare Turnstile 端点。
错误 : 即使在成功生成 Token 后,小部件仍显示"Pending..."状态 来源 : GitHub Issue #119 发生原因 : 在单个页面上渲染多个 <Turnstile/> 组件时的 CSS 重绘问题。仅在全高清桌面屏幕上可重现。Token 确实成功生成(验证有效),但视觉状态未更新。将鼠标悬停在小部件上会触发重绘并显示正确状态。 预防措施 : 在成功回调中强制 CSS 重绘
<Turnstile
siteKey={KEY}
onSuccess={(token) => {
setToken(token)
// Force repaint by toggling display
const widget = document.querySelector('.cf-turnstile')
if (widget) {
widget.style.display = 'none'
setTimeout(() => widget.style.display = 'block', 0)
}
}}
/>
注意 : 这只是一个视觉问题,不是验证失败。Token 已正确生成且功能正常。
错误 : 导入 @marsidev/react-turnstile 时出现 Jest encountered an unexpected token 来源 : GitHub Issue #114, GitHub Issue #112 发生原因 : Jest 30.2.0(截至 2025年12月最新版)的 ESM 模块解析问题。维护者将 Issue #112 关闭为"未计划"。Jest 用户陷入困境;迁移到 Vitest 有效。 预防措施 : 在 Jest 设置中模拟 Turnstile 组件 或 迁移到 Vitest
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(@marsidev/react-turnstile)/)'
]
}
// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking
状态 : 维护者将问题关闭为"未计划"。建议新项目迁移到 Vitest。
wrangler.jsonc:
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}
必需的 CSP:
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
">
import { Hono } from 'hono'
type Bindings = {
TURNSTILE_SECRET_KEY: string
TURNSTILE_SITE_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.post('/api/login', async (c) => {
const body = await c.req.formData()
const token = body.get('cf-turnstile-response')
if (!token) {
return c.text('Missing Turnstile token', 400)
}
// Validate token
const verifyFormData = new FormData()
verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token.toString())
verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '') // CRITICAL - always pass client IP
const verifyResult = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
body: verifyFormData,
}
)
const outcome = await verifyResult.json<{ success: boolean }>()
if (!outcome.success) {
return c.text('Invalid Turnstile token', 401)
}
// Process login
return c.json({ message: 'Login successful' })
})
export default app
使用场景 : 使用 Hono 框架的 Cloudflare Workers 中的 API 路由
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export function ContactForm() {
const [token, setToken] = useState<string>()
const [error, setError] = useState<string>()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!token) {
setError('Please complete the challenge')
return
}
const formData = new FormData(e.currentTarget)
formData.append('cf-turnstile-response', token)
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
})
if (!response.ok) {
setError('Submission failed')
return
}
// Success
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onSuccess={setToken}
onError={() => setError('Challenge failed')}
onExpire={() => setToken(undefined)}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={!token}>
Submit
</button>
</form>
)
}
使用场景 : Next.js 中使用 React hooks 的客户端表单
测试 Sitekeys (客户端):
1x00000000000000000000AA2x00000000000000000000AB3x00000000000000000000FF测试密钥 (服务器端):
1x0000000000000000000000000000000AA2x0000000000000000000000000000000AA3x0000000000000000000000000000000AA脚本: check-csp.sh - 验证 CSP 是否允许 Turnstile
参考文档:
widget-configs.md - 所有配置选项error-codes.md - 错误代码故障排除 (100*/200*/300*/400*/600*)testing-guide.md - 测试策略,测试密钥react-integration.md - React/Next.js 模式模板: Hono、React、隐式/显式渲染、验证的完整示例
预清除 (SPA): 颁发在页面导航间持久化的 cookie
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})
自定义操作和数据: 跟踪挑战类型,传递自定义数据(最多 255 个字符)
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})
错误处理: 使用 retry: 'auto' 和 error-callback 以提高弹性
turnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})
必需: 无(从 CDN 加载) React: @marsidev/react-turnstile@1.4.1 (Cloudflare 推荐), turnstile-types@1.2.3 其他: vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile
mcp__cloudflare-docs__search_cloudflare_documentation 工具解决方案 : 在 Cloudflare 仪表板中将你的域名(包括用于开发的 localhost)添加到小部件的允许域名列表中。对于本地开发,请使用测试 sitekey 1x00000000000000000000AA 代替。
解决方案 : 实现带有重试逻辑的错误回调。这是一个已知的 Cloudflare 端问题 (2025)。如果重试失败,则回退到替代验证方法。
success: false解决方案 :
解决方案 : 添加 CSP 指令:
<meta http-equiv="Content-Security-Policy" content="
frame-src https://challenges.cloudflare.com;
script-src https://challenges.cloudflare.com;
">
解决方案 : 在错误消息中告知用户应禁用 Safari 的"隐藏 IP 地址"设置 (Safari → 设置 → 隐私 → 隐藏 IP 地址 → 关闭)
解决方案 : 在 Jest 设置中模拟 Turnstile 组件(或迁移到 Vitest,它能正确处理 ESM 模块):
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required
注意 : 此问题在 Jest 30.2.0 (2025年12月) 中仍然存在。维护者将其关闭为"未计划"。完整详情请参见问题 #15。
已预防的错误 : 15 个已记录的问题(Safari 18 隐藏 IP、Brave 彩带、Next.js Jest、CSP 阻止、Token 重复使用、过期、主机名白名单、小部件崩溃 300030、配置错误 600010、缺少验证、GET 请求、密钥暴露、Chrome/Edge 106010、多个小部件渲染、Token 重新生成模式)
每周安装次数
347
仓库
GitHub 星标数
650
首次出现
Jan 20, 2026
安全审计
安装于
claude-code285
gemini-cli232
opencode227
cursor213
codex205
antigravity204
Status : Production Ready ✅ Last Updated : 2026-01-21 Dependencies : None (optional: @marsidev/react-turnstile for React) Latest Versions : @marsidev/react-turnstile@1.4.1, turnstile-types@1.2.3
Recent Updates (2025) :
rerenderOnCallbackChange prop for React closure issues# 1. Create widget: https://dash.cloudflare.com/?to=/:account/turnstile
# Copy sitekey (public) and secret key (private)
# 2. Add widget to frontend
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<form>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>
<button type="submit">Submit</button>
</form>
# 3. Validate token server-side (Cloudflare Workers)
const formData = await request.formData()
const token = formData.get('cf-turnstile-response')
const verifyFormData = new FormData()
verifyFormData.append('secret', env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token)
verifyFormData.append('remoteip', request.headers.get('CF-Connecting-IP')) // REQUIRED - see Critical Rules
const result = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{ method: 'POST', body: verifyFormData }
)
const outcome = await result.json()
if (!outcome.success) return new Response('Invalid', { status: 401 })
CRITICAL:
Implicit (auto-render on page load):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-callback="onSuccess"></div>
Explicit (programmatic control for SPAs):
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
const widgetId = turnstile.render('#container', { sitekey: 'YOUR_SITE_KEY' })
turnstile.reset(widgetId) // Reset widget
turnstile.getResponse(widgetId) // Get token
React (using @marsidev/react-turnstile):
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />
✅ Call Siteverify API - Server-side validation is mandatory ✅ Use HTTPS - Never validate over HTTP ✅ Protect secret keys - Never expose in frontend code ✅ Handle token expiration - Tokens expire after 5 minutes ✅ Implement error callbacks - Handle failures gracefully ✅ Use dummy keys for testing - Test sitekey: 1x00000000000000000000AA ✅ Set reasonable timeouts - Don't wait indefinitely for validation ✅ Validate action/hostname - Check additional fields when specified ✅ Rotate keys periodically - Use dashboard or API to rotate secrets ✅ Monitor analytics - Track solve rates and failures ✅ Always pass client IP to Siteverify - Use CF-Connecting-IP header (Workers) or X-Forwarded-For (Node.js). Cloudflare briefly enforced strict remoteip validation in Jan 2025, causing widespread failures for sites not passing correct IP
❌ Skip server validation - Client-side only = security vulnerability ❌ Proxy api.js script - Must load from Cloudflare CDN ❌ Reuse tokens - Each token is single-use only ❌ Use GET requests - Siteverify only accepts POST ❌ Expose secret key - Keep secrets in backend environment only ❌ Trust client-side validation - Tokens can be forged ❌ Cache api.js - Future updates will break your integration ❌ Use production keys in tests - Use dummy keys instead ❌ Ignore error callbacks - Always handle failures
This skill prevents 15 documented issues:
Error : Zero token validation in Turnstile Analytics dashboard Source : https://developers.cloudflare.com/turnstile/get-started/ Why It Happens : Developers only implement client-side widget, skip Siteverify call Prevention : All templates include mandatory server-side validation with Siteverify API
Error : success: false for valid tokens submitted after delay Source : https://developers.cloudflare.com/turnstile/get-started/server-side-validation Why It Happens : Tokens expire 300 seconds after generation Prevention : Templates document TTL and implement token refresh on expiration
Error : Security bypass - attackers can validate their own tokens Source : https://developers.cloudflare.com/turnstile/get-started/server-side-validation Why It Happens : Secret key hardcoded in JavaScript or visible in source Prevention : All templates show backend-only validation with environment variables
Error : API returns 405 Method Not Allowed Source : https://developers.cloudflare.com/turnstile/migration/recaptcha Why It Happens : reCAPTCHA supports GET, Turnstile requires POST Prevention : Templates use POST with FormData or JSON body
Error : Error 200500 - "Loading error: The iframe could not be loaded" Source : https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes Why It Happens : CSP blocks challenges.cloudflare.com iframe Prevention : Skill includes CSP configuration reference and check-csp.sh script
Error : Generic client execution error for legitimate users Source : https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 Why It Happens : Unknown - appears to be Cloudflare-side issue (2025) Prevention : Templates implement error callbacks, retry logic, and fallback handling
Error : Widget fails with "configuration error" Source : https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 Why It Happens : Missing or deleted hostname in widget configuration Prevention : Templates document hostname allowlist requirement and verification steps
Error : Error 300010 when Safari's "Hide IP address" is enabled Source : https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 Why It Happens : Privacy settings interfere with challenge signals Prevention : Error handling reference documents Safari workaround (disable Hide IP)
Error : Verification fails during success animation Source : https://github.com/brave/brave-browser/issues/45608 (April 2025) Why It Happens : Brave shields block animation scripts Prevention : Templates handle success before animation completes
Error : @marsidev/react-turnstile breaks Jest tests Source : https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025) Why It Happens : Module resolution issues with Jest Prevention : Testing guide includes Jest mocking patterns and dummy sitekey usage
Error : Error 110200 - "Unknown domain: Domain not allowed" Source : https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes Why It Happens : Production widget used in development without localhost in allowlist Prevention : Templates use dummy test keys for dev, document localhost allowlist requirement
Error : success: false with "token already spent" error Source : https://developers.cloudflare.com/turnstile/troubleshooting/testing Why It Happens : Each token can only be validated once. Turnstile tokens are single-use - after validation (success OR failure), the token is consumed and cannot be revalidated. Developers must explicitly call turnstile.reset() to generate a new token for subsequent submissions. Prevention : Templates document single-use constraint and token refresh patterns
// CRITICAL: Reset widget after validation to get new token
const turnstileRef = useRef(null)
async function handleSubmit(e) {
e.preventDefault()
const token = formData.get('cf-turnstile-response')
const result = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify({ token })
})
// Reset widget regardless of success/failure
// Token is consumed either way
if (turnstileRef.current) {
turnstile.reset(turnstileRef.current)
}
}
<Turnstile
ref={turnstileRef}
siteKey={TURNSTILE_SITE_KEY}
onSuccess={setToken}
/>
Error : 106010 - "Generic parameter error" on first widget load in Chrome/Edge browsers Source : Cloudflare Error Codes, Community Report Why It Happens : Unknown browser-specific issue affecting Chrome and Edge on first page load. Console shows 400 error to https://challenges.cloudflare.com/cdn-cgi/challenge-platform. Firefox is not affected. Subsequent page reloads work correctly. Prevention : Implement error callback with auto-retry logic
turnstile.render('#container', {
sitekey: SITE_KEY,
retry: 'auto',
'retry-interval': 8000,
'error-callback': (errorCode) => {
if (errorCode === '106010') {
console.warn('Chrome/Edge first-load issue (106010), auto-retrying...')
// Auto-retry will handle it
}
}
})
Workaround : Widget works correctly after page reload. Auto-retry setting resolves in most cases. Test in Incognito mode to rule out browser extensions. Review CSP rules to ensure Cloudflare Turnstile endpoints are allowed.
Error : Widget displays "Pending..." status even after successful token generation Source : GitHub Issue #119 Why It Happens : CSS repaint issue when rendering multiple <Turnstile/> components on a single page. Only reproducible on full HD desktop screens. Token IS successfully generated (validation works), but visual status doesn't update. Hovering over widget triggers repaint and shows correct status. Prevention : Force CSS repaint in success callback
<Turnstile
siteKey={KEY}
onSuccess={(token) => {
setToken(token)
// Force repaint by toggling display
const widget = document.querySelector('.cf-turnstile')
if (widget) {
widget.style.display = 'none'
setTimeout(() => widget.style.display = 'block', 0)
}
}}
/>
Note : This is a visual-only issue, not a validation failure. The token is correctly generated and functional.
Error : Jest encountered an unexpected token when importing @marsidev/react-turnstile Source : GitHub Issue #114, GitHub Issue #112 Why It Happens : ESM module resolution issues with Jest 30.2.0 (latest as of Dec 2025). Issue #112 closed as "not planned" by maintainer. Jest users are stuck; Vitest migration works. Prevention : Mock the Turnstile component in Jest setup OR migrate to Vitest
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: transformIgnorePatterns in jest.config.js
module.exports = {
transformIgnorePatterns: [
'node_modules/(?!(@marsidev/react-turnstile)/)'
]
}
// Option 3 (Recommended): Migrate to Vitest
// Vitest handles ESM modules correctly without mocking
Status : Maintainer closed issue as "not planned". Recommend migrating to Vitest for new projects.
wrangler.jsonc:
{
"vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
"secrets": ["TURNSTILE_SECRET_KEY"] // Run: wrangler secret put TURNSTILE_SECRET_KEY
}
Required CSP:
<meta http-equiv="Content-Security-Policy" content="
script-src 'self' https://challenges.cloudflare.com;
frame-src 'self' https://challenges.cloudflare.com;
">
import { Hono } from 'hono'
type Bindings = {
TURNSTILE_SECRET_KEY: string
TURNSTILE_SITE_KEY: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.post('/api/login', async (c) => {
const body = await c.req.formData()
const token = body.get('cf-turnstile-response')
if (!token) {
return c.text('Missing Turnstile token', 400)
}
// Validate token
const verifyFormData = new FormData()
verifyFormData.append('secret', c.env.TURNSTILE_SECRET_KEY)
verifyFormData.append('response', token.toString())
verifyFormData.append('remoteip', c.req.header('CF-Connecting-IP') || '') // CRITICAL - always pass client IP
const verifyResult = await fetch(
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
{
method: 'POST',
body: verifyFormData,
}
)
const outcome = await verifyResult.json<{ success: boolean }>()
if (!outcome.success) {
return c.text('Invalid Turnstile token', 401)
}
// Process login
return c.json({ message: 'Login successful' })
})
export default app
When to use : API routes in Cloudflare Workers with Hono framework
'use client'
import { Turnstile } from '@marsidev/react-turnstile'
import { useState } from 'react'
export function ContactForm() {
const [token, setToken] = useState<string>()
const [error, setError] = useState<string>()
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
if (!token) {
setError('Please complete the challenge')
return
}
const formData = new FormData(e.currentTarget)
formData.append('cf-turnstile-response', token)
const response = await fetch('/api/contact', {
method: 'POST',
body: formData,
})
if (!response.ok) {
setError('Submission failed')
return
}
// Success
}
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" required />
<textarea name="message" required />
<Turnstile
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
onSuccess={setToken}
onError={() => setError('Challenge failed')}
onExpire={() => setToken(undefined)}
/>
{error && <div className="error">{error}</div>}
<button type="submit" disabled={!token}>
Submit
</button>
</form>
)
}
When to use : Client-side forms in Next.js with React hooks
Dummy Sitekeys (client):
1x00000000000000000000AA2x00000000000000000000AB3x00000000000000000000FFDummy Secret Keys (server):
1x0000000000000000000000000000000AA2x0000000000000000000000000000000AA3x0000000000000000000000000000000AAScripts: check-csp.sh - Verify CSP allows Turnstile
References:
widget-configs.md - All configuration optionserror-codes.md - Error code troubleshooting (100*/200*/300*/400*/600*)testing-guide.md - Testing strategies, dummy keysreact-integration.md - React/Next.js patternsTemplates: Complete examples for Hono, React, implicit/explicit rendering, validation
Pre-Clearance (SPAs): Issue cookie that persists across page navigations
turnstile.render('#container', {
sitekey: SITE_KEY,
callback: async (token) => {
await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
}
})
Custom Actions & Data: Track challenge types, pass custom data (max 255 chars)
turnstile.render('#container', {
action: 'login', // Track in analytics
cdata: JSON.stringify({ userId: '123' }), // Custom payload
})
Error Handling: Use retry: 'auto' and error-callback for resilience
turnstile.render('#container', {
retry: 'auto',
'retry-interval': 8000, // ms between retries
'error-callback': (error) => { /* handle or show fallback */ }
})
Required: None (loads from CDN) React: @marsidev/react-turnstile@1.4.1 (Cloudflare-recommended), turnstile-types@1.2.3 Other: vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile
mcp__cloudflare-docs__search_cloudflare_documentation toolSolution : Add your domain (including localhost for dev) to widget's allowed domains in Cloudflare Dashboard. For local dev, use dummy test sitekey 1x00000000000000000000AA instead.
Solution : Implement error callback with retry logic. This is a known Cloudflare-side issue (2025). Fallback to alternative verification if retries fail.
success: falseSolution :
Solution : Add CSP directives:
<meta http-equiv="Content-Security-Policy" content="
frame-src https://challenges.cloudflare.com;
script-src https://challenges.cloudflare.com;
">
Solution : Document in error message that users should disable Safari's "Hide IP address" setting (Safari → Settings → Privacy → Hide IP address → Off)
Solution : Mock the Turnstile component in Jest setup (or migrate to Vitest, which handles ESM modules correctly):
// Option 1: Jest mocking (jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
Turnstile: () => <div data-testid="turnstile-mock" />,
}))
// Option 2: Migrate to Vitest (recommended for new projects)
// Vitest handles ESM modules without mocking required
Note : This issue persists in Jest 30.2.0 (Dec 2025). Maintainer closed as "not planned". See Issue #15 for full details.
Errors Prevented : 15 documented issues (Safari 18 Hide IP, Brave confetti, Next.js Jest, CSP blocking, token reuse, expiration, hostname allowlist, widget crash 300030, config error 600010, missing validation, GET request, secret exposure, Chrome/Edge 106010, multiple widgets rendering, token regeneration pattern)
Weekly Installs
347
Repository
GitHub Stars
650
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykWarn
Installed on
claude-code285
gemini-cli232
opencode227
cursor213
codex205
antigravity204
React 组合模式指南:Vercel 组件架构最佳实践,提升代码可维护性
106,200 周安装