支付页上多一道 Turnstile,坏处不是多点一下,而是失败很难还原。客户只会说「付款失败」,后台却可能只剩一条没有创建 Checkout Session 的日志。

Cloudflare 的关键规则很硬:token 生成后 300 秒失效,而且只能成功校验一次。真实案例里,Bricks Builder 论坛 2026 年有用户因为两个 Turnstile 集成同时存在,token 被先消费掉,后面的请求拿到 timeout-or-duplicate。支付页的 AJAX 预校验也会踩同一个坑。

##把投诉变成 6 个字段

表 1:Turnstile 支付页误伤日志表

后台字段从哪里拿看到什么才算正常
cf-turnstile-response前端表单或请求体每次提交都有新 token,不复用旧 token
successCloudflare siteverify 响应创建 Checkout Session 前必须是 true
error-codesCloudflare siteverify 响应重点看 timeout-or-duplicatemissing-input-responseinvalid-input-secret
challenge_tsCloudflare siteverify 响应距离服务端校验不超过 300 秒
hostname / actionCloudflare siteverify 响应与支付页域名、前端 action 完全对应
checkout.session.idStripe Dashboard 或服务端日志只有 Turnstile 校验成功后才创建

这张表比「用户网络不好」有用。只要 siteverify 成功而 Stripe 没有 session,问题多半在你自己的异步顺序。只要 Stripe session 没创建且 error-codestimeout-or-duplicate,查重复提交和 token 被预校验消费。

Turnstile checkout failed:最短处理路径

在服务端记录 siteverify 的完整结果,但不要记录完整 token。保留 successerror-codeschallenge_tshostnameaction、用户 session id 和请求时间就够了。

第二步,把创建 Stripe Checkout Session 的代码放到 Turnstile 服务端校验之后。前端显示通过只能说明浏览器拿到 token,不能说明后端已经信任这次请求。

第三步,检查支付页有没有多个请求共享同一个 token。常见位置是优惠券校验、价格刷新、试用资格检查、邮箱预注册和付款按钮本身。Turnstile token 是单次使用,预校验请求先验证一次,正式付款请求就会失败。

第四步,给按钮加提交态。客户连续点两次,或者移动端 WebView 自动重试,都可能让第二个请求拿到重复 token。第二次请求要重新 turnstile.reset,不能继续用第一次 token。

第五步,给 300 秒超时做提示。客户在支付页停留 8 分钟才点付款,失败不是误杀,而是 token 过期。提示用户刷新验证,比直接跳错误页更好。

哪些错误码最像支付页误伤?

表 2:siteverify 错误码与处理动作

error-codes支付页现象处理动作
timeout-or-duplicate第一次点没反应,第二次失败;或预校验后正式提交失败找重复校验;提交失败后 turnstile.reset
missing-input-response前端没把 token 传给后端检查表单字段名 cf-turnstile-response 或 JSON 字段
invalid-input-responsetoken 格式异常、过期或来自错误环境区分 staging/prod site key,重新渲染 widget
invalid-input-secret所有用户都失败比对后端环境变量里的 secret key
bad-request后端请求偶发失败检查 Content-Type、JSON/form 编码和超时
internal-error少量短时失败idempotency_key 重试,别让用户重复下单

Cloudflare 还提供 remoteipidempotency_keyremoteip 不是必须字段,隐私和代理链复杂时可以不传;idempotency_key 更适合服务端安全重试,避免网络抖动时重复消费 token。

Stripe Checkout 跳转顺序怎么改?

正确顺序应该是:用户点付款,前端拿新 Turnstile token,服务端调用 Cloudflare siteverify,服务端确认 hostnameaction,然后创建 Stripe Checkout Session,最后把 session.url 返回前端。

不要在前端先创建 Checkout Session,再补 Turnstile 结果。这样坏请求已经进了 Stripe,后面只能靠取消 session 或清理未完成支付。对小 SaaS 来说,这会把风控、转化率和客服解释搅在一起。

也不要把 token 校验塞进 webhook。Stripe webhook 到达时,客户已经离开你的站点,Turnstile token 也可能过期。Turnstile 应该挡在创建 Checkout Session 之前。

移动端和 WebView 怎么单独看?

移动端失败率高,按 user agent、浏览器、系统版本和支付方式分组。Safari、Chrome、应用内 WebView、邮件客户端内置浏览器,脚本生命周期不一样。

表 3:移动端排查表

场景典型问题修法
iOS Safari 停留很久再付款token 超过 300 秒付款按钮前重新执行验证
应用内 WebView回调丢失或脚本被延迟提供外部浏览器打开按钮
网络切换请求重试导致重复校验后端用 idempotency_key,前端禁用重复点击
低端 Androidwidget 加载慢先加载支付表单,再渲染 Turnstile
广告落地页跳转参数和 session 丢失把 Checkout 创建放在同域服务端接口

如果你同时在 Cloudflare、Vercel、GitHub Actions 和 Stripe 后台查日志,最怕临时 Wi-Fi 断开导致后台会话掉线。排查支付事故时,可以把部署和支付后台固定在一台工作机,并用海外服务跑 GitHub Actions / Cloudflare 的稳定线路承载这些操作。

还没恢复时,单独查 Turnstile checkout failed

把 Turnstile 从所有支付请求上撤下来,改成只拦高风险动作:同一邮箱多次失败、同一会话频繁换卡、优惠券滥用、注册后立即高额购买。这样能保留防护,又不会让所有客户都过验证。

给被误伤客户发一个新的 Checkout 链接,说明刚才验证失败,不要让客户重新填一遍资料。B2B 客户可以直接发 invoice,尤其是年付订单。

最后把修复记录写进事故时间线:从几点开始失败、错误码是什么、改了哪段代码、回滚点在哪里。下次转化率突然掉,看这张表。

相关阅读