如何修复 ERROR_WRONG_USER_KEY 及另外 3 个请求错误

error_wrong_user_key 的意思是:你的 API 密钥根本没有离开过你的代码。诊断结论就这一句,值得先摆到前面来说,因为几乎每个人都把它读成“我的密钥不对”,然后开始粘贴新密钥。对于密钥不对,CapSkip 另有一个错误码。error_wrong_user_key,以及和它并列的另外三个,都是在识别工具还没看过验证码之前就返回的:它们描述的是你发出的那个 HTTP 请求的形态,而不是它所针对的那个挑战。
这四个错误码,以及那个常与它们混淆的错误码
它们由提交接口或轮询接口以纯文本形式返回,取代通常的 OK 回复。每一个都是确定性的。重复发送同样的请求只会得到同样的回答,所以在这些错误码外面套一层重试循环纯属浪费时间。下面表格中的第二行是那个并不属于这四个之列的同族错误码,因为把这两者区分开来才是大部分工作所在。
| 代码 | 官方含义 | 实际含义 |
|---|---|---|
| ERROR_WRONG_USER_KEY | API 密钥缺失或为空 | key 参数没有传,或者传了但值为空 |
| ERROR_KEY_DOES_NOT_EXIST | API 密钥无效 | 密钥确实传到了,但 CapSkip 不认识它 |
| ERROR_WRONG_METHOD | HTTP 方法或 action 参数无效 | method 的值不是 CapSkip 认识的,或者 action 不是 get |
| ERROR_WRONG_ID_FORMAT | 验证码 ID 格式无效 | 你用来轮询的 id 不是一个纯数字 |
| ERROR_BAD_PARAMETERS | 缺少必需参数,或参数无效 | 该验证码类型所需的某个字段缺失或格式不对 |
完整列表,包括图片上传相关的错误码和 reCAPTCHA 专有的错误码,见 CapSkip API 文档.
ERROR_WRONG_USER_KEY:密钥是缺失,而不是不正确
缺失和无效是两个不同的 bug,修法也不同,CapSkip 特意把它们分开。如果密钥到达了服务器却没有被识别,你拿到的是 error_key_does_not_exist,相关说明写在 该错误码的指南。如果你拿到的是 error_wrong_user_key,那就说明根本没有可辨认的东西送达,所以别再盯着值本身看了,去看它究竟有没有被发出去。
# No key parameter at all. This is what produces it. curl -X POST \ -d "method=userrecaptcha" \ -d "googlekey=YOUR_SITEKEY" \ -d "pageurl=https://example.com/page-with-recaptcha" \ http://127.0.0.1:8080/in.php ERROR_WRONG_USER_KEY
有四种情况会产生它,大致按出现频率排列:
- 环境变量在代码实际运行的地方没有设置。典型情况是:你的 shell 里有这个变量,而跑代码的服务里没有。读到的值是空字符串,客户端发出的 key 后面什么都没有。
- 密钥读自某个配置文件,而这个文件没有和代码一起部署。
- 自己手写的客户端只在变量为真值时才添加 key,于是空字符串会悄悄把这个参数整个丢掉。
- 客户端把参数放到了请求根本不会携带它们的地方,比如把 JSON 请求体发给一个读取表单字段的接口。
在调用之前打印这个值的长度,而不是打印值本身。长度为零就已经告诉了你需要知道的一切,同时又不会把凭据写进日志文件。
为什么这个问题只在服务器上才开始出现
下面这一点,会让那些同一套代码已经跑了好几个月的人感到困惑。只有在应用中打开了 API 密钥校验时,CapSkip 才要求提供密钥。全新的本地安装默认是关闭的,所以完全不带密钥的请求也会被接受,永远不会有人抱怨。你的客户端很可能从你写下它的那天起,就一直在发送空密钥。
然后你把识别工具搬到了一台不在你桌上的机器上。CapSkip 有两种连接模式:Local 模式绑定 127.0.0.1,只响应本机;Server 模式绑定你的内网或公网 IP,这样另一台机器、一台 VPS 或托管的工作节点就能通过 API 访问它。固定公网 IP 可以让地址保持不变。两者的说明都在 连接设置。在搬迁的同时打开密钥校验是正确的做法,而这也正是你代码里所有潜伏的密钥 bug 一次性全部浮出水面的时刻。两种模式用的都是你自己的硬件,也都不限量,所以这只是一次配置变更,并不会改变你要付的钱。
如果你要把密钥分发给多个工作节点,就给每个节点一把自己的密钥,这样吊销其中一把也不会牵动其余的。这方面的内容参见 验证码 API 密钥分发指南.
ERROR_WRONG_ID_FORMAT:你多半把整条回复都发过去了
这一个有一个压倒性的主因,一旦知道就很容易看出来。提交接口返回的并不是一个纯数字,而是用竖线分隔的 OK 和 id,把这个字符串原样传进轮询请求的客户端,发出去的 id 自然就不是数字。
# What in.php actually returns: OK|212 # Wrong. The whole reply went into id. curl "http://127.0.0.1:8080/res.php?key=YOUR_API_KEY&action=get&id=OK|212" ERROR_WRONG_ID_FORMAT # Right. Split on the pipe and send the number. curl "http://127.0.0.1:8080/res.php?key=YOUR_API_KEY&action=get&id=212"
改成请求 JSON,解析就不会那么脆弱,因为 id 是作为一个字段返回的,而不是分隔字符串的一半。无论用哪种方式,都要在切分之前先检查前缀,因为错误码里没有竖线,闭着眼睛切分只会把错误码本身当成 id 还给你。
# A hand rolled client has to do this itself. The SDK does not,
# which is most of why it is worth using.
reply = httpx.post(IN_URL, data=payload).text # "OK|212"
if not reply.startswith("OK|"):
raise RuntimeError(reply) # it is an error code
captcha_id = reply.split("|", 1)[1] # "212"原始请求与轮询周期的完整示例,包含同样的分隔符处理,见 用 curl 识别验证码的完整演示.
ERROR_WRONG_METHOD:动词用错了,或者词写错了
两个彼此无关的错误共用这一个错误码,所以它读起来才这么含糊。第一个是 HTTP 动词:在你用 GET 调用的接口上以表单请求体形式发送的参数,哪儿也到不了。第二个是 method 参数本身,它必须是 CapSkip 针对你要识别的类型所认可的值之一:图片用 post 或 base64,两个版本的 reCAPTCHA 都用 userrecaptcha,Cloudflare 用 turnstile,极验 v3 用 geetest。
大部分情况都出在两个拼写上:一是给 reCAPTCHA 接口发送 sitekey,而它要的是 googlekey;二是把 userrecaptcha 写成了 recaptcha 或 userecaptcha。这个错误码同样覆盖轮询这一侧,那里的 action 必须是字面量字符串 get。
ERROR_BAD_PARAMETERS:类型和字段对不上
method 已经被正确理解,但它所需要的某样东西缺失或格式不对。这是按类型区分的,所以真正有用的问题永远是:CapSkip 认为你请求的是哪一种类型。
- reCAPTCHA 需要同时提供 googlekey 和 pageurl,而 pageurl 必须是小组件所在页面的完整 URL,包含协议头。
- reCAPTCHA v3 需要把 version 设为 v3。跟着一起发送 invisible,等于把一个 v2 的字段塞进了 v3 的请求里。
- Turnstile 的挑战页面除了 sitekey 之外,还需要 data 和 pagedata。小组件模式两者都不需要。
- 极验需要同时提供 gt 和 challenge,而 challenge 大约一分钟就会过期,所以过期的 challenge 会在这一步就失败,而不是拖到后面才失败。
- 图片验证码在 multipart 上传时需要 file,在 base64 方式下需要 body,绝不能两个都给。
把你即将发送的负载打进日志,密钥部分做脱敏处理,然后对照该类型的参数表逐项核对。十次里有九次,答案就摆在这一行里。
空响应不属于这几种情况
轮询什么都没返回是另一回事,值得单独点名,因为空回复常被误认成 id 有问题。空的响应体意味着结果已经被取走过,或者这个 id 不存在。CapSkip 只允许你读取结果一次。一个已经成功、却因为代码忘了 break 而又循环了一次的重试循环,第二次读到的是空字符串,于是把一个明明识别正确的验证码报告为失败。
第一次拿到结果时就把它存下来。另外,不要把空响应和 CAPCHA_NOT_READY 混为一谈,后者是正常的轮询状态而不是失败,相关说明见 该响应的指南.
从 SDK 中读取这些错误
这四个错误码都会以 ApiException 的形式抛出,错误码就是它的消息内容。这是你应该首先检查的异常,因为它意味着 CapSkip 听懂了你的请求并拒绝了它,这和根本连不上 CapSkip 是完全不同性质的问题。
# pip install capskip
from capskip import CapSkip, ApiException, NetworkException
solver = CapSkip(host="127.0.0.1", port=8080, apiKey=API_KEY)
try:
token = solver.recaptcha(sitekey=SITEKEY, url=PAGE_URL)["code"]
except ApiException as err:
# CapSkip answered and rejected the request. Do not retry.
print("Rejected:", err)
except NetworkException as err:
# CapSkip did not answer. Wrong host, wrong port, or not running.
print("Unreachable:", err)ValidationException 在这里也值得了解,因为它在任何东西发出之前就会触发。SDK 会拒绝与你所请求的类型不匹配的参数,于是一个在裸 HTTP 下本来会以 ERROR_BAD_PARAMETERS 返回的错误,改为在本地就被捕获,并给出一条点名具体参数的消息。SDK 一共有四种异常类型:ApiException、NetworkException、TimeoutException 和 ValidationException。如果你更愿意在一个地方统一处理,它们每一个都派生自一个名为 CapSkipError 的基类。Node.js、PHP 和 C# 客户端里同样有这四种异常,它们都列在 验证码识别 SDK 页面.
常见问题
ERROR_WRONG_USER_KEY 和 ERROR_KEY_DOES_NOT_EXIST 有什么区别?
区别在于密钥有没有送达。前者表示 key 参数缺失或为空,所以要去查你的配置和构造请求的代码。后者表示密钥送达了,但 CapSkip 不认识它,所以要去查这个值,以及应用实际签发的是哪一把密钥。
我到底需不需要 API 密钥?
只有在 CapSkip 应用中打开了 API 密钥校验时才需要。它默认是关闭的,在识别工具只响应回环地址的机器上,这没有问题。在识别工具开始监听网络地址之前把它打开,从那时起就要发送密钥。
这些错误值得重试吗?
不值得。这四个都是确定性的,第二次尝试的失败方式和第一次一模一样。该重试的是那些瞬时故障:连接错误、轮询超时,或者返回“无法识别”的验证码。对一个格式错误的请求做重试,只是把你的退避预算花在一个 bug 上。
本地一切正常,到了服务器上就坏了。为什么?
几乎总是因为搬迁的同时打开了密钥校验,而密钥其实从来就没有真正被发送过。第二常见的原因是某个环境变量在你的 shell 里存在,但在运行代码的那个服务里不存在。在发起调用的地方检查一下这个值的长度。
简短版结论
这四个错误码说的是你的请求,而不是验证码本身。error_wrong_user_key 表示 key 参数里什么都没送到,这是配置问题,而不是值写错了。ERROR_WRONG_ID_FORMAT 几乎总是意味着用竖线分隔的整条回复被原样传了进去。ERROR_WRONG_METHOD 意味着动词写错或拼写写错,而 ERROR_BAD_PARAMETERS 意味着某个字段不属于你所请求的类型。它们没有一个值得重试。
能把这些问题一一定论的参数表,都在 API 文档页面。那个能消除大部分出错机会的 Python 客户端见 Python 验证码识别页面.
调试时值得记住一点:CapSkip 是一款 验证码绕过 工具,运行在你自己的硬件上,所以在你摸索的过程中把一个格式错误的请求发上一百遍,除了时间之外不会花你一分钱。
