Troubleshooting

Cloudflare Turnstile 错误和故障排除

CaptchaAI 明明返回了 token,页面却始终不放行?这几乎是每个接入 Cloudflare Turnstile 的开发者都撞过的墙。好消息是:Turnstile 的失败很少是随机的,绝大多数都能归到三个阶段——提交阶段(你发给 API 的请求被拒)、轮询阶段(取结果时超时或报错)、验证阶段(API 返回了合法 token,但目标页面拒绝它)。

而真正拖垮 Turnstile 集成的,通常就三件事:

  1. pageurl 不精确 —— 在 Cloudflare 全屏验证页上尤其致命,上下文校验更严
  2. sitekey 取错了 —— 从错误的元素、或另一个组件实例上抓到的值
  3. token 走错了应用路径 —— 页面要的是 cf-turnstile-response、回调,或者两者都要

CaptchaAI 识别 Turnstile 通常在 10 秒内完成,成功率稳定。所以当集成失败时,问题几乎总是出在你发送的参数,或你如何把返回的 token 写回页面。把下面三个阶段逐一对照,就能快速定位到底卡在哪一环。


按症状定位:先看你卡在哪一步

不想从头读到尾,就先用这张表对号入座——找到最接近你现状的那一行,直接跳到对应章节:

你观察到的现象 卡在哪个阶段 跳到
提交任务时接口直接返回 ERROR_* 提交阶段 「提交阶段错误码」
一直返回 CAPCHA_NOT_READY 或轮询报错 轮询阶段 「轮询阶段错误码」
拿到了 token,页面却不认 验证阶段 「验证阶段排查」
分不清是 Turnstile 还是全屏拦截 定位错方向 「先分清 Cloudflare 产品」

三个阶段互相独立:提交阶段的报错不会牵连轮询,验证阶段的失败也和 API 无关。先确认阶段,再查具体原因,能省掉一大半无用功。


第一步:先分清你面对的是哪个 Cloudflare 产品

排查前先确认一件事:你遇到的到底是页面内嵌的 Turnstile 组件,还是整页拦你的 Cloudflare 全屏验证(Cloudflare Challenge)?两者都是 Cloudflare 的产品,长得像,但用的 API method 不同、排查思路也完全不同——方向搞错,后面每一步都是白费。

判断信号 Turnstile Cloudflare 验证流程
你看到的 页面内嵌的组件(复选框或不可见) 全屏 Cloudflare 验证界面
CaptchaAI 返回什么 一个注入表单的 token 一个 qa_session_cookie cookie
API method turnstile cloudflare_challenge
需要代理吗 可选 需要(必填)

一眼分辨的方法:

  • 页面主体内容照常显示、只在表单某处出现一个小控件 —— Turnstile
  • 整页被一屏「正在验证你是否是真人」拦住、内容全部加载不出来 —— Cloudflare 全屏验证

如果你面对的是全屏 Cloudflare 验证(而非内嵌组件),要改用 Cloudflare 验证流程求解器,它返回 qa_session_cookie cookie,并且必须配代理。本文接下来只讲内嵌 Turnstile 组件的排查——确认是它,就继续往下。


Turnstile 为什么和别的验证码不一样

在逐条看错误码之前,先记住 Turnstile 区别于其他验证码类型的三个特点——很多排查思路都由它们而来。

1. pageurl 必须精确到路径

Turnstile 的 token 和页面上下文强绑定。在 Cloudflare 全屏验证页上,只要 URL 有一点出入(哪怕只是路径不同),token 就会被判为无效。

2. token 有两条应用路径

返回的 token 可以用两种方式写回页面,用错了会静默失败:

方式 适用场景
隐藏字段 —— 写入 cf-turnstile-response(有时还有 g-recaptcha-response 页面用的是带隐藏输入的标准表单
回调函数 —— 调用 turnstile.render()data-callback 里定义的函数 页面用程序化校验,而不是表单提交

3. token 是一次性的

一个 Turnstile token 只能校验一次。如果自动化脚本不小心提交了两遍,或存在竞态条件,第二次一定失败。

顺带一提:Turnstile 由 Cloudflare 托管,不依赖 Google 脚本,因此在国内网络下比 reCAPTCHA 更容易正常加载——不少面向海外站点的采集或 QA 项目,接触到的第一个 Cloudflare 验证往往就是它。


提交阶段错误码(in.php)

https://ocr.captchaai.com/in.php 提交任务、请求还没被接受时,会遇到这些错误。大部分是账户或参数问题,一张表就能对号入座:

错误码 原因 处理方式
ERROR_WRONG_USER_KEY API Key 格式不对(正确长度为 32 个字符) captchaai.com/api.php 核对密钥
ERROR_KEY_DOES_NOT_EXIST 密钥格式正确,但没关联到有效账户 打开控制台,确认账户已激活、密钥无误
ERROR_ZERO_BALANCE 当前套餐没有空闲线程 等线程释放、降低并发,或升级套餐(见下方说明)
HTML 或 500 / 502 响应 服务端临时错误 等 5–10 秒再重试

关于 ERROR_ZERO_BALANCE 有个常见误会:CaptchaAI 按并发线程计费,这个报错指的是线程被占满,而不是账户余额为零——两者很容易混淆。账户明明还有钱却报这个错,八成是并发开太高,把线程占满了。

下面两个错误码需要单独展开,因为它们和参数格式直接相关。

ERROR_PAGEURL

缺少 pageurl 参数时报这个错。补上完整 URL——协议、域名、路径都要有:

pageurl=https://staging.example.com/qa-login

ERROR_BAD_PARAMETERS

必填参数缺失或格式错误时报这个错。Turnstile 的必填参数如下:

参数 类型 是否必填 说明
key 字符串 你的 CaptchaAI API Key
method 字符串 必须为 turnstile
sitekey 字符串 Turnstile 组件的 sitekey
pageurl 字符串 完整页面 URL

可选、但常用的参数:

参数 类型 说明
action 字符串 data-actionturnstile.render()action 参数的值
proxy 字符串 格式:login:password@IP:PORT
proxytype 字符串 HTTPHTTPSSOCKS4SOCKS5

逐一核对必填字段是否齐全、类型是否正确,通常就能消掉这个报错。关于 proxyproxytype 什么时候要带上:

  • 独立的内嵌 Turnstile 组件:一般不用,留空即可
  • 组件挂在有额外风控的页面、或你需要固定出口 IP 做 QA 复现:按上表格式补上

如何正确提取 Turnstile sitekey

sitekey 是最常被填错的参数。按从易到难,有三种取值方式。

方式 1:data-sitekey 属性

最常见,直接从组件的 DOM 属性里读:

<div class="cf-turnstile" data-sitekey="0x4AAAAAAAB1example"></div>

方式 2:turnstile.render() 调用

如果页面用脚本渲染组件,sitekey 就在 render() 的参数里:

turnstile.render('#captcha-container', {
  sitekey: '0x4AAAAAAAB1example',
  callback: function(token) {
    document.getElementById('cf-turnstile-response').value = token;
  }
});

方式 3:拦截渲染调用(进阶)

如果 sitekey 是运行时动态注入的,可以在组件初始化前重新定义 turnstile.render,把参数截下来:

// Inject this before the Turnstile script loads
const originalRender = window.turnstile.render;
window.turnstile.render = function(container, params) {
  console.log('Sitekey:', params.sitekey);
  console.log('Action:', params.action);
  return originalRender.call(this, container, params);
};

轮询阶段错误码(res.php)

轮询 https://ocr.captchaai.com/res.php 取结果时,会遇到这些返回值。除了 ERROR_EMPTY_ACTION(下面单独讲),其余都能照表处理:

返回值 含义 / 原因 处理方式
CAPCHA_NOT_READY 不是错误,识别还在进行中(通常不到 10 秒) 等 5 秒再轮询一次
ERROR_WRONG_ID_FORMAT 验证码 ID 里混入了非数字字符 原样使用 in.php 返回的 ID,不要改动
ERROR_WRONG_CAPTCHA_ID ID 和任何已提交的任务都对不上 确认轮询的是提交响应里返回的那个 ID
ERROR_CAPTCHA_UNSOLVABLE 识别失败:可能 sitekey 取错,或页面配置暂不支持 核对 sitekey,刷新请求后重试
ERROR_INTERNAL_SERVER_ERROR 服务端问题 等 10 秒再重试

CAPCHA_NOT_READY 是最常见的返回值,它根本不是报错——只是识别还没完成。别把它当异常抛出,按固定间隔继续轮询即可。

ERROR_EMPTY_ACTION

轮询请求里缺少 action 参数时报这个错。每次轮询都带上 action=get

https://ocr.captchaai.com/res.php?key=YOUR_KEY&action=get&id=CAPTCHA_ID&json=1

提示: 轮询 Turnstile 结果时建议加上 json=1。带上它,接口返回结构化的 {"status": 1, "request": "<token>"},脚本解析更省事;不加则返回 OK|<token> 这样的纯文本。两种都能用,挑你解析起来顺手的那种即可。


token 拿到了,页面却拒绝:验证阶段排查

这一类最难查,因为 API 明明成功返回了 token,目标页面却不认。它不会给你错误码,只会静默失败或跳回原页,所以要按下面四种情况逐一排除。

情况 1:token 写进了错误的字段

现象: 表单提交了,但页面报校验错误、或直接刷新。

Turnstile 页面可能在不同字段里等这个 token:

  • cf-turnstile-response —— Turnstile 主隐藏输入
  • g-recaptcha-response —— 部分页面拿它作兜底

解决: 两个字段都检查一遍。浏览器自动化里可以同时写入:

# Selenium — inject into both fields for safety
driver.execute_script("""
    var cfField = document.querySelector('[name="cf-turnstile-response"]');
    var gField = document.querySelector('[name="g-recaptcha-response"]');
    if (cfField) cfField.value = arguments[0];
    if (gField) gField.value = arguments[0];
""", token)

情况 2:回调没被触发

现象: token 已经在字段里了,表单还是不让提交。

原因: 页面用的是回调函数,而不是(或者不只是)隐藏字段。回调往往还负责别的逻辑,比如激活提交按钮、或发起 AJAX 请求。

解决: 找到并手动调用回调:

// Check data-callback attribute
const callbackName = document.querySelector('.cf-turnstile').getAttribute('data-callback');
if (callbackName && window[callbackName]) {
  window[callbackName](token);
}

// Or if it was passed in turnstile.render()
// You may need to intercept the render call to capture it

情况 3:pageurl 上下文对不上

现象: sitekey 正确、token 也是新解出来的,却仍被拒。

原因: API 请求里用的 pageurl 和页面真实上下文不一致。这在以下场景尤其常见:

  • Cloudflare 全屏验证页 —— URL 里可能带着关键的查询参数或路径片段
  • 单页应用(SPA) —— 地址栏看到的 URL,未必是加载 Turnstile 组件的那个 URL

解决: 用 DevTools 的 Network 面板找到 Turnstile 组件实际加载的 URL,拿它当 pageurl

情况 4:token 被重复使用

现象: 第一次识别有效,之后就失败。

原因: Turnstile token 是一次性的。一旦被 Cloudflare 服务器校验过,就立即失效。

解决: 每次提交表单都重新请求一次识别,不要缓存或复用 token。

验证阶段这四种情况没有统一的错误码,只能靠现象反推,这里汇总成一张速查表:

现象 根因 处理方式
表单提交后报校验错误或刷新 token 写进了错误字段 cf-turnstile-responseg-recaptcha-response 都写一遍
token 已在字段里,仍无法提交 回调没被触发 data-callback,手动调用回调函数
sitekey、token 都对,仍被拒 pageurl 上下文对不上 用组件实际加载的 URL 当 pageurl
首次有效、之后失败 token 被复用 每次提交都请求新 token

Python 完整示例

import time
import requests

API_KEY = "YOUR_CAPTCHAAI_API_KEY"
SITEKEY = "0x4AAAAAAAB1example"
PAGE_URL = "https://staging.example.com/qa-login"

SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"


def solve_turnstile(api_key, sitekey, pageurl):
    """Submit a Turnstile challenge and return the solved token."""

    # Submit
    submit_resp = requests.post(
        SUBMIT_URL,
        data={
            "key": api_key,
            "method": "turnstile",
            "sitekey": sitekey,
            "pageurl": pageurl,
            "json": 1,
        },
        timeout=30,
    )
    submit_resp.raise_for_status()
    submit_data = submit_resp.json()

    if submit_data.get("status") != 1:
        raise RuntimeError(f"Submit failed: {submit_data}")

    captcha_id = submit_data["request"]
    print(f"Task created — captcha ID: {captcha_id}")

    # Wait before first poll (Turnstile is fast — 10 seconds is usually enough)
    time.sleep(10)

    # Poll for result
    for _ in range(60):
        result_resp = requests.get(
            RESULT_URL,
            params={
                "key": api_key,
                "action": "get",
                "id": captcha_id,
                "json": 1,
            },
            timeout=30,
        )
        result_resp.raise_for_status()
        result_data = result_resp.json()

        if result_data.get("request") == "CAPCHA_NOT_READY":
            time.sleep(5)
            continue

        if result_data.get("status") == 1:
            return result_data["request"]

        raise RuntimeError(f"Polling error: {result_data}")

    raise TimeoutError("Turnstile solve timed out")


# Usage
token = solve_turnstile(API_KEY, SITEKEY, PAGE_URL)
print(f"Solved token: {token[:80]}...")

# Inject into cf-turnstile-response and/or g-recaptcha-response
# Then submit the form

Node.js 完整示例

const API_KEY = "YOUR_CAPTCHAAI_API_KEY";
const SITEKEY = "0x4AAAAAAAB1example";
const PAGE_URL = "https://staging.example.com/qa-login";

const SUBMIT_URL = "https://ocr.captchaai.com/in.php";
const RESULT_URL = "https://ocr.captchaai.com/res.php";

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function solveTurnstile(apiKey, sitekey, pageurl) {
  // Submit
  const submitResp = await fetch(SUBMIT_URL, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      key: apiKey,
      method: "turnstile",
      sitekey: sitekey,
      pageurl: pageurl,
      json: "1",
    }),
  });

  const submitData = await submitResp.json();
  if (submitData.status !== 1) {
    throw new Error(`Submit failed: ${JSON.stringify(submitData)}`);
  }

  const captchaId = submitData.request;
  console.log(`Task created — captcha ID: ${captchaId}`);

  // Turnstile is fast — wait 10 seconds before first poll
  await sleep(10_000);

  // Poll for result
  for (let i = 0; i < 60; i++) {
    const resultResp = await fetch(
      `${RESULT_URL}?${new URLSearchParams({
        key: apiKey,
        action: "get",
        id: captchaId,
        json: "1",
      })}`
    );

    const resultData = await resultResp.json();

    if (resultData.request === "CAPCHA_NOT_READY") {
      await sleep(5_000);
      continue;
    }

    if (resultData.status === 1) {
      return resultData.request;
    }

    throw new Error(`Polling error: ${JSON.stringify(resultData)}`);
  }

  throw new Error("Turnstile solve timed out");
}

// Usage
solveTurnstile(API_KEY, SITEKEY, PAGE_URL)
  .then((token) => {
    console.log(`Solved token: ${token.slice(0, 80)}...`);
    // Inject into cf-turnstile-response and/or g-recaptcha-response
  })
  .catch(console.error);

常见问题

Turnstile 的 token 有效期有多久?

Turnstile token 是一次性的,被校验一次即失效;即便没用过,也只有几分钟的时效窗口。所以拿到 token 后要尽快提交表单,别先缓存再慢慢用,更不要跨多次提交复用同一个 token。

sitekey 是动态加载的,怎么才能拿到?

在组件初始化前重新定义 window.turnstile.render,把传入的 params.sitekey 打印或截存下来(见上文“方式 3”)。这样即便 sitekey 由脚本在运行时注入,也能在真正渲染前捕获到。

轮询时到底要不要加 json=1

建议加。带 json=1 时接口返回结构化 JSON,statusrequest 字段一目了然,脚本判断更稳;不加则是 OK|token 这样的纯文本,要自己按 | 切分。两种都能工作,但 JSON 更不容易解析出错。

CaptchaAI 识别一个 Turnstile 大概要多久?

通常不到 10 秒。所以示例代码里首次轮询前先等 10 秒,之后每 5 秒轮询一次,多数任务在头一两次轮询内就能拿到结果。如果长期卡在 CAPCHA_NOT_READY,多半是 sitekey 或 pageurl 有问题,而不是速度问题。

明明账户还有余额,为什么报 ERROR_ZERO_BALANCE

这个报错针对的是线程,不是余额。CaptchaAI 按并发线程计费,每个套餐有固定线程数(例如 BASIC 是 $15/月、5 个线程)。当正在处理的任务占满了线程,新任务就会收到 ERROR_ZERO_BALANCE。等已有任务完成释放线程、降低并发,或升级到线程更多的套餐即可。

Turnstile 组件是隐形的,页面上根本看不到,还能识别吗?

能。不可见模式的 Turnstile 同样通过 data-sitekeyturnstile.render() 暴露 sitekey,取值和提交流程与复选框模式完全一致。区别只在于页面不显示交互控件,token 通常靠回调写回——所以遇到不可见组件时,优先按上文“情况 2”检查回调是否被触发。

轮询多少次没结果就该判定失败?

示例代码里最多轮询 60 次、每次间隔 5 秒,也就是约 5 分钟。正常任务远用不到这么久:首次等 10 秒后,绝大多数在头一两次轮询就返回 token。如果连续十几次仍是 CAPCHA_NOT_READY,与其继续干等,不如中止本次任务、核对 sitekey 与 pageurl 后重新提交,往往比把超时拉得更长更快解决问题。


修复清单

Turnstile 集成失败时,按这个顺序排查:

  1. 确认产品类型 —— 是内嵌 Turnstile 组件,还是全屏 Cloudflare 验证?method 用错,后面全错
  2. 核对 sitekey —— 从 data-sitekeyturnstile.render() 提取,别从错误的元素上取
  3. 核对 pageurl —— 用精确到协议和路径的 URL,SPA 尤其要留意
  4. 确认 token 路径 —— 页面要的是 cf-turnstile-responseg-recaptcha-response,还是回调?
  5. 加上 json=1 —— 轮询 Turnstile 结果时用 JSON 响应
  6. 不复用 token —— 每次提交都请求一次新的识别

先从 CaptchaAI Turnstile 求解器 入手,对照 API 文档 核对参数;如果还想补一补组件的底层机制,可以读 Cloudflare Turnstile 的工作原理


相关文章

该文章已禁用评论。