cloudflare-turnstile

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cloudflare Turnstile

Cloudflare Turnstile

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):
  • December 2025: @marsidev/react-turnstile v1.4.1 fixes race condition in script loading
  • August 2025: v1.3.0 adds
    rerenderOnCallbackChange
    prop for React closure issues
  • March 2025: Upgraded Turnstile Analytics with TopN statistics (7 dimensions: hostnames, browsers, countries, user agents, ASNs, OS, source IPs), anomaly detection, enhanced bot behavior monitoring
  • January 2025: Brief remoteip validation enforcement (resolved, but highlights importance of correct IP passing)
  • 2025: WCAG 2.1 AA compliance, Free plan (20 widgets, 7-day analytics), Enterprise features (unlimited widgets, ephemeral IDs, any hostname support, 30-day analytics, offlabel branding)

状态: 已就绪可用于生产环境 ✅ 最后更新: 2026-01-21 依赖: 无(可选:React项目可使用@marsidev/react-turnstile) 最新版本: @marsidev/react-turnstile@1.4.1, turnstile-types@1.2.3
2025年近期更新:
  • 2025年12月: @marsidev/react-turnstile v1.4.1修复了脚本加载中的竞态条件
  • 2025年8月: v1.3.0新增
    rerenderOnCallbackChange
    属性,解决React闭包问题
  • 2025年3月: 升级Turnstile分析功能,新增TopN统计(7个维度:主机名、浏览器、国家、用户代理、ASN、操作系统、源IP)、异常检测、增强型机器人行为监控
  • 2025年1月: 短暂强制执行remoteip验证(已解决,但凸显了传递正确IP的重要性)
  • 2025年: 符合WCAG 2.1 AA合规标准,免费计划(20个小部件、7天分析数据),企业版功能(无限小部件、临时ID、支持任意主机名、30天分析数据、自定义品牌)

Quick Start (5 Minutes)

快速入门(5分钟)

bash
undefined
bash
undefined

Copy sitekey (public) and secret key (private)

复制站点密钥(公开)和密钥(私有)

2. Add widget to frontend

2. 将小部件添加到前端

<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>
<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)

3. 服务器端验证令牌(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:**
- Token expires in 5 minutes, single-use only
- ALWAYS validate server-side (Siteverify API required)
- Never proxy/cache api.js (must load from Cloudflare CDN)
- Use different widgets for dev/staging/production
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 })

**重要提示**:
- 令牌有效期为5分钟,仅限单次使用
- 必须执行服务器端验证(需调用Siteverify API)
- 切勿代理/缓存api.js(必须从Cloudflare CDN加载)
- 开发/预发布/生产环境使用不同的小部件

Rendering Modes

渲染模式

Implicit (auto-render on page load):
html
<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):
typescript
<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):
tsx
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />

隐式渲染(页面加载时自动渲染):
html
<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>
显式渲染(单页应用的程序化控制):
typescript
<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):
tsx
import { Turnstile } from '@marsidev/react-turnstile'
<Turnstile siteKey={TURNSTILE_SITE_KEY} onSuccess={setToken} />

Critical Rules

关键规则

Always Do

必须执行的操作

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
调用Siteverify API - 服务器端验证是强制要求 ✅ 使用HTTPS - 切勿通过HTTP进行验证 ✅ 保护密钥 - 切勿在前端代码中暴露 ✅ 处理令牌过期 - 令牌5分钟后过期 ✅ 实现错误回调 - 优雅处理失败情况 ✅ 测试使用虚拟密钥 - 测试站点密钥:
1x00000000000000000000AA
设置合理超时 - 不要无限等待验证结果 ✅ 验证操作/主机名 - 指定时检查额外字段 ✅ 定期轮换密钥 - 使用控制台或API轮换密钥 ✅ 监控分析数据 - 跟踪解决率和失败情况 ✅ 始终将客户端IP传递给Siteverify - 使用
CF-Connecting-IP
头(Workers)或
X-Forwarded-For
(Node.js)。Cloudflare曾在2025年1月短暂强制执行严格的remoteip验证,导致未传递正确IP的站点出现大量失败

Never Do

切勿执行的操作

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

跳过服务器端验证 - 仅客户端验证会导致安全漏洞 ❌ 代理api.js脚本 - 必须从Cloudflare CDN加载 ❌ 重复使用令牌 - 每个令牌仅限单次使用 ❌ 使用GET请求 - Siteverify仅接受POST请求 ❌ 暴露密钥 - 密钥仅保留在后端环境中 ❌ 信任客户端验证 - 令牌可能被伪造 ❌ 缓存api.js - 未来的更新会破坏你的集成 ❌ 测试中使用生产密钥 - 改用虚拟密钥 ❌ 忽略错误回调 - 始终处理失败情况

Known Issues Prevention

已知问题预防

This skill prevents 15 documented issues:
本技能可预防15个已记录的问题:

Issue #1: Missing Server-Side Validation

问题#1:缺少服务器端验证

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
错误: Turnstile分析仪表板中显示令牌验证为零 来源: https://developers.cloudflare.com/turnstile/get-started/ 原因: 开发者仅实现了客户端小部件,跳过了Siteverify调用 预防: 所有模板都包含使用Siteverify API的强制服务器端验证

Issue #2: Token Expiration (5 Minutes)

问题#2:令牌过期(5分钟)

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
错误: 延迟提交的有效令牌返回
success: false
来源: https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因: 令牌生成300秒后过期 预防: 模板中记录了TTL,并实现了令牌过期时的刷新逻辑

Issue #3: Secret Key Exposed in Frontend

问题#3:密钥在前端暴露

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
错误: 安全绕过 - 攻击者可自行验证令牌 来源: https://developers.cloudflare.com/turnstile/get-started/server-side-validation 原因: 密钥硬编码在JavaScript中或在源码中可见 预防: 所有模板展示了仅在后端使用环境变量的验证方式

Issue #4: GET Request to Siteverify

问题#4:对Siteverify使用GET请求

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
错误: API返回405 Method Not Allowed 来源: https://developers.cloudflare.com/turnstile/migration/recaptcha 原因: reCAPTCHA支持GET,而Turnstile要求POST 预防: 模板使用POST请求,搭配FormData或JSON主体

Issue #5: Content Security Policy Blocking

问题#5:内容安全策略阻止加载

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
错误: 错误200500 - "加载错误:无法加载iframe" 来源: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因: CSP阻止了challenges.cloudflare.com的iframe 预防: 本技能包含CSP配置参考和check-csp.sh脚本

Issue #6: Widget Crash (Error 300030)

问题#6:小部件崩溃(错误300030)

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
错误: 合法用户遇到通用客户端执行错误 来源: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因: 未知 - 似乎是Cloudflare端的问题(2025年) 预防: 模板实现了错误回调、重试逻辑和降级处理

Issue #7: Configuration Error (Error 600010)

问题#7:配置错误(错误600010)

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
错误: 小部件因"配置错误"失败 来源: https://community.cloudflare.com/t/repeated-cloudflare-turnstile-error-600010/644578 原因: 小部件配置中缺少或删除了主机名 预防: 模板记录了主机名白名单要求和验证步骤

Issue #8: Safari 18 / macOS 15 "Hide IP" Issue

问题#8:Safari 18 / macOS 15 "隐藏IP"问题

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)
错误: 启用Safari的"隐藏IP地址"时出现错误300010 来源: https://community.cloudflare.com/t/turnstile-is-frequently-generating-300x-errors/700903 原因: 隐私设置干扰了挑战信号 预防: 错误处理参考文档中记录了Safari的解决方法(禁用隐藏IP)

Issue #9: Brave Browser Confetti Animation Failure

问题#9:Brave浏览器庆祝动画失败

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
错误: 验证在成功动画期间失败 来源: https://github.com/brave/brave-browser/issues/45608 (April 2025) 原因: Brave护盾阻止了动画脚本 预防: 模板在动画完成前处理成功逻辑

Issue #10: Next.js + Jest Incompatibility

问题#10:Next.js + Jest不兼容

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
错误: @marsidev/react-turnstile导致Jest测试失败 来源: https://github.com/marsidev/react-turnstile/issues/112 (Oct 2025) 原因: Jest的模块解析问题 预防: 测试指南包含Jest模拟模式和虚拟站点密钥的使用方法

Issue #11: localhost Not in Allowlist

问题#11:localhost不在白名单中

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
错误: 错误110200 - "未知域名:域名不被允许" 来源: https://developers.cloudflare.com/turnstile/troubleshooting/client-side-errors/error-codes 原因: 开发环境中使用了生产小部件,且未将localhost加入白名单 预防: 模板在开发环境使用虚拟测试密钥,并记录了localhost白名单要求

Issue #12: Token Reuse Attempt

问题#12:尝试重复使用令牌

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
typescript
// 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}
/>
错误:
success: false
并提示"token already spent" 来源: https://developers.cloudflare.com/turnstile/troubleshooting/testing 原因: 每个令牌只能验证一次。Turnstile令牌为单次使用 - 验证后(成功或失败),令牌即被消耗,无法重新验证。开发者必须显式调用
turnstile.reset()
来生成新令牌用于后续提交。 预防: 模板记录了单次使用限制和令牌刷新模式
typescript
// 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}
/>

Issue #13: Error 106010 - Chrome/Edge First-Load Failure

问题#13:错误106010 - Chrome/Edge首次加载失败

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
typescript
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.
错误:
106010
- Chrome/Edge浏览器首次加载小部件时出现"通用参数错误" 来源: Cloudflare错误代码, 社区报告 原因: 未知的浏览器特定问题,影响Chrome和Edge的首次页面加载。控制台显示对
https://challenges.cloudflare.com/cdn-cgi/challenge-platform
的400错误。Firefox不受影响。后续页面刷新可正常工作。 预防: 实现带自动重试逻辑的错误回调
typescript
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端点。

Issue #14: Multiple Widgets Visual Status Stuck (Community-sourced)

问题#14:多小部件视觉状态卡住(社区反馈)

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
tsx
<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.
错误: 即使令牌生成成功,小部件仍显示"Pending..."状态 来源: GitHub Issue #119 原因: 在单页渲染多个
<Turnstile/>
组件时出现CSS重绘问题。仅在全高清桌面屏幕上可复现。令牌已成功生成(验证有效),但视觉状态未更新。悬停在小部件上会触发重绘并显示正确状态。 预防: 在成功回调中强制CSS重绘
tsx
<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)
    }
  }}
/>
注意: 这只是视觉问题,不影响验证功能。令牌已正确生成且可用。

Issue #15: Jest Compatibility with @marsidev/react-turnstile (Updated Dec 2025)

问题#15:Jest与@marsidev/react-turnstile兼容问题(2025年12月更新)

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
typescript
// 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.
错误: 导入@marsidev/react-turnstile时Jest遇到意外令牌 来源: GitHub Issue #114, GitHub Issue #112 原因: Jest 30.2.0(截至2025年12月为最新版本)的ESM模块解析问题。Issue #112被维护者标记为"不计划修复"。Jest用户陷入困境;迁移到Vitest可解决。 预防: 在Jest设置中模拟Turnstile组件,或迁移到Vitest
typescript
// 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。

Configuration

配置

wrangler.jsonc:
jsonc
{
  "vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
  "secrets": ["TURNSTILE_SECRET_KEY"]  // Run: wrangler secret put TURNSTILE_SECRET_KEY
}
Required CSP:
html
<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-src 'self' https://challenges.cloudflare.com;
">

wrangler.jsonc:
jsonc
{
  "vars": { "TURNSTILE_SITE_KEY": "1x00000000000000000000AA" },
  "secrets": ["TURNSTILE_SECRET_KEY"]  // Run: wrangler secret put TURNSTILE_SECRET_KEY
}
必需的CSP:
html
<meta http-equiv="Content-Security-Policy" content="
  script-src 'self' https://challenges.cloudflare.com;
  frame-src 'self' https://challenges.cloudflare.com;
">

Common Patterns

常见模式

Pattern 1: Hono + Cloudflare Workers

模式1:Hono + Cloudflare Workers

typescript
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
typescript
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
适用场景: Cloudflare Workers中使用Hono框架的API路由

Pattern 2: React + Next.js App Router

模式2:React + Next.js App Router

tsx
'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

tsx
'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>
  )
}
适用场景: 使用React Hooks的Next.js客户端表单

Testing Keys

测试密钥

Dummy Sitekeys (client):
  • Always pass:
    1x00000000000000000000AA
  • Always block:
    2x00000000000000000000AB
  • Force interactive:
    3x00000000000000000000FF
Dummy Secret Keys (server):
  • Always pass:
    1x0000000000000000000000000000000AA
  • Always fail:
    2x0000000000000000000000000000000AA
  • Token already spent:
    3x0000000000000000000000000000000AA

虚拟站点密钥(客户端):
  • 始终通过:
    1x00000000000000000000AA
  • 始终阻止:
    2x00000000000000000000AB
  • 强制交互式:
    3x00000000000000000000FF
虚拟密钥(服务器端):
  • 始终通过:
    1x0000000000000000000000000000000AA
  • 始终失败:
    2x0000000000000000000000000000000AA
  • 令牌已消耗:
    3x0000000000000000000000000000000AA

Bundled Resources

捆绑资源

Scripts:
check-csp.sh
- Verify CSP allows Turnstile
References:
  • widget-configs.md
    - All configuration options
  • error-codes.md
    - Error code troubleshooting (100*/200*/300*/400*/600*)
  • testing-guide.md
    - Testing strategies, dummy keys
  • react-integration.md
    - React/Next.js patterns
Templates: Complete examples for Hono, React, implicit/explicit rendering, validation

脚本:
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、隐式/显式渲染、验证的完整示例

Advanced Features

高级功能

Pre-Clearance (SPAs): Issue cookie that persists across page navigations
typescript
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)
typescript
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
typescript
turnstile.render('#container', {
  retry: 'auto',
  'retry-interval': 8000,  // ms between retries
  'error-callback': (error) => { /* handle or show fallback */ }
})

预验证(单页应用): 颁发跨页面导航持久化的Cookie
typescript
turnstile.render('#container', {
  sitekey: SITE_KEY,
  callback: async (token) => {
    await fetch('/api/pre-clearance', { method: 'POST', body: JSON.stringify({ token }) })
  }
})
自定义操作与数据: 跟踪挑战类型,传递自定义数据(最多255字符)
typescript
turnstile.render('#container', {
  action: 'login',  // Track in analytics
  cdata: JSON.stringify({ userId: '123' }),  // Custom payload
})
错误处理: 使用
retry: 'auto'
error-callback
提高韧性
typescript
turnstile.render('#container', {
  retry: 'auto',
  'retry-interval': 8000,  // ms between retries
  'error-callback': (error) => { /* handle or show fallback */ }
})

Dependencies

依赖

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

必需: 无(从CDN加载) React: @marsidev/react-turnstile@1.4.1(Cloudflare推荐), turnstile-types@1.2.3 其他: vue-turnstile, ngx-turnstile, svelte-turnstile, @nuxtjs/turnstile

Official Documentation

官方文档



Troubleshooting

故障排除

Problem: Error 110200 - "Unknown domain"

问题:错误110200 - "未知域名"

Solution: Add your domain (including localhost for dev) to widget's allowed domains in Cloudflare Dashboard. For local dev, use dummy test sitekey
1x00000000000000000000AA
instead.
解决方案: 将你的域名(包括开发环境的localhost)添加到Cloudflare控制台中小部件的允许域名列表。本地开发时,使用虚拟测试站点密钥
1x00000000000000000000AA
替代。

Problem: Error 300030 - Widget crashes for legitimate users

问题:错误300030 - 小部件为合法用户崩溃

Solution: Implement error callback with retry logic. This is a known Cloudflare-side issue (2025). Fallback to alternative verification if retries fail.
解决方案: 实现带重试逻辑的错误回调。这是Cloudflare端的已知问题(2025年)。如果重试失败,降级到其他验证方式。

Problem: Tokens always return
success: false

问题:令牌始终返回
success: false

Solution:
  1. Check token hasn't expired (5 min TTL)
  2. Verify secret key is correct
  3. Ensure token hasn't been validated before (single-use)
  4. Check hostname matches widget configuration
解决方案:
  1. 检查令牌是否已过期(5分钟TTL)
  2. 验证密钥是否正确
  3. 确保令牌未被验证过(单次使用)
  4. 检查主机名是否与小部件配置匹配

Problem: CSP blocking iframe (Error 200500)

问题:CSP阻止iframe(错误200500)

Solution: Add CSP directives:
html
<meta http-equiv="Content-Security-Policy" content="
  frame-src https://challenges.cloudflare.com;
  script-src https://challenges.cloudflare.com;
">
解决方案: 添加CSP指令:
html
<meta http-equiv="Content-Security-Policy" content="
  frame-src https://challenges.cloudflare.com;
  script-src https://challenges.cloudflare.com;
">

Problem: Safari 18 "Hide IP" causing Error 300010

问题:Safari 18 "隐藏IP"导致错误300010

Solution: Document in error message that users should disable Safari's "Hide IP address" setting (Safari → Settings → Privacy → Hide IP address → Off)
解决方案: 在错误提示中告知用户应禁用Safari的"隐藏IP地址"设置(Safari → 设置 → 隐私 → 隐藏IP地址 → 关闭)

Problem: Next.js + Jest tests failing with @marsidev/react-turnstile

问题:Next.js + Jest测试因@marsidev/react-turnstile失败

Solution: Mock the Turnstile component in Jest setup (or migrate to Vitest, which handles ESM modules correctly):
typescript
// 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)
解决方案: 在Jest设置中模拟Turnstile组件(或迁移到Vitest,它能正确处理ESM模块):
typescript
// 选项1:Jest模拟(jest.setup.ts)
jest.mock('@marsidev/react-turnstile', () => ({
  Turnstile: () => <div data-testid="turnstile-mock" />,
}))

// 选项2:在jest.config.js中设置transformIgnorePatterns
module.exports = {
  transformIgnorePatterns: [
    'node_modules/(?!(@marsidev/react-turnstile)/)'
  ]
}

// 选项3(推荐):迁移到Vitest
// Vitest无需模拟即可正确处理ESM模块
注意: 此问题在Jest 30.2.0(2025年12月)中仍存在。维护者已标记为"不计划修复"。详情见问题#15。

已预防的错误: 15个已记录的问题(Safari 18隐藏IP、Brave庆祝动画、Next.js Jest、CSP阻止、令牌重复使用、过期、主机名白名单、小部件崩溃300030、配置错误600010、缺少验证、GET请求、密钥暴露、Chrome/Edge 106010、多小部件渲染、令牌重新生成模式)