工具大全
踩坑实录作者:Coocon2026年9月27日7 次阅读约 10 分钟阅读

Claude Code 报错 Invalid API key · Fix external API key 怎么排查:Not logged in、Credit balance is too low、API Error 401/429/529 原文与重试实测

问题背景

把 Claude Code 接到 DeepSeek 这类兼容端点、换了一把 key、或者额度用完之后,跑 claude -p 常会看到下面几句中的一句:

Invalid API key · Fix external API key
Not logged in · Please run /login
Credit balance is too low
API Error: Request rejected (429) · …
API Error: 529 Overloaded. This is a server-side issue, usually temporary — …

报错本身不难看懂,难的是这几个问题:

  • 这句话是 Claude Code 本地判断出来的,还是后端返回的?请求到底发出去了没有?
  • 为什么 key 错了,命令要卡将近 3 分钟才报错?
  • 写脚本时,报错在 stdout 还是 stderr?退出码是多少?--output-format json 里该看哪个字段?
  • 如果你是第三方网关,应该读 x-api-key 还是 Authorization: Bearer?

本文的报错原文和数字,全部来自 2026-09-27 在本机 Claude Code 2.1.280 上的实测:28 组 case,共 61 次 claude -p。后端不是真实 API,而是一个本地 stub,这样可以任意指定返回码,并记下每个请求到达的时间和携带的 header。

问题分析

先在 2.1.280 的原生二进制(claude.exe,217 MB)里找到了这几句文案的定义:

var _Ye="Not logged in \xB7 Please run /login",
    Gke="Invalid API key \xB7 Fix external API key",
    pat="Invalid auth token \xB7 Fix external auth token",
    mat="Invalid ANTHROPIC_CUSTOM_HEADERS \xB7 Fix the environment variable"

还有一条校验:function JM(e){return/^[a-zA-Z0-9-_]+$/.test(e)},不通过就抛 Invalid API key format. API key must contain only alphanumeric characters, dashes, and underscores.。

光看静态代码,很容易得出「key 带特殊字符会在本地被拦」的结论。实测推翻了这一点:ANTHROPIC_API_KEY='bad key!@#' 被原样放进 x-api-key 发给了后端,stub 返回正常响应后 exit 0。回头再查代码,edo() 的唯一调用点在 OAuth 创建 API key 的流程里(r.data?.raw_key → edo(s,n)),环境变量里的 key 根本不经过它。所以本文所有结论都以实测为准,静态代码只用来解释现象。

读代码时还看到一处决定文案的逻辑,后面实测也印证了:401 会不会显示 Invalid API key,取决于错误 message 里是否包含 x-api-key 这个字符串:

function uat(e){return e instanceof Error&&e.message.toLowerCase().includes("x-api-key")}
// ……
if(uat(e)){ …; return Ro({error:"authentication_failed",
  content: M==="ANTHROPIC_API_KEY"||M==="apiKeyHelper" ? Gke : _Ye}) }

技术方案与选型

目标是断言「请求发没发、发了几次、隔多久、带了哪个 header」,所以需要能控制后端返回什么,并记录每一个到达的请求。

方案 结论 理由
本地 stub(零依赖 Node) ✅ 采用 返回码和 body 可以任意指定;每个请求按毫秒时间戳和全量 header 写进 jsonl;不花钱,也不打扰真实服务
用真实 key 打 api.anthropic.com ❌ 排除 429/529/5xx 造不出来;错 key 反复重试,等于对官方接口做无意义的压测
接真实第三方端点(DeepSeek 等) ❌ 排除 错误体的格式由对方决定,无法控制变量;同一个错误在不同时间的返回可能不一样
mitmproxy 抓包 ❌ 排除 需要改证书信任链,还会引入一个额外变量;而 ANTHROPIC_BASE_URL 可以直接指向 http 的 stub,用不上
只读二进制里的字符串 ❌ 不能单独用 上面的格式校验就是反例:代码里有,但这条路径上不生效

具体做法:

  • lab/stub.mjs 监听 127.0.0.1,按 case 配置对 POST /v1/messages 返回合法 SSE、指定的状态码和 body,或者直接断开连接。
  • 每次运行用 env -i 清空环境,配一个全新的空 CLAUDE_CONFIG_DIR,避免复用本机的 OAuth 登录态,工作目录也单独建。
  • HTTPS_PROXY 同样指向 stub:遇到 CONNECT 就记录目标主机并返回 403。这样任何外连都会被记录并拦截,保证整个实验没有请求打到真实的 api.anthropic.com。
  • 统一的调用方式:claude -p 'reply with exactly OK' --model haiku --output-format text|json < /dev/null。

实测过程

**对照组先跑通。**stub 返回合法的 Messages 流(message_start → content_block_delta: "OK" → message_stop)。3 次都是 exit 0,stdout 逐字节为 4f 4b 0a(OK\n),stub 各收到 1 个请求,带 x-api-key、anthropic-version: 2023-06-01、user-agent: claude-cli/2.1.280 (external, sdk-cli)。stub 可信,后面的失败才能归因。

**所有 case 都跑了至少 2 次,报错文本、退出码和请求数全部一致。**下面这张表是本文的核心,「stub 请求数」是 stub 实际记到的数,不是推断:

报错原文(stdout) 触发条件 本地还是后端 stub 请求数 是否重试 墙钟
Not logged in · Please run /login 没设 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,也没登录 本地 0 — 0.28 / 0.30 s
Invalid API key · Fix external API key · Invalid X-Api-Key header value from ANTHROPIC_API_KEY: it contains a non-ASCII character at character 4 (9 characters). key 含中文(sk-密钥-123) 本地 0 否 0.28 / 0.27 s
… it contains a line break at character 7 (10 characters on 2 lines). key 中间夹换行 本地 0 否 0.27 / 0.27 s
Invalid auth token · Fix external auth token · Invalid Authorization header value from ANTHROPIC_AUTH_TOKEN: it contains a non-ASCII character at character 5 (10 characters). token 含中文 本地 0 否 0.28 / 0.28 s
Invalid API key · Fix external API key 后端 401,message 含 invalid x-api-key 后端 11 是,10 次 179.43 / 168.78 s
同上 后端 401,type 换成 invalid_request_error,message 仍含 x-api-key 后端 11 是 176.93 / 183.28 s
Failed to authenticate. API Error: 401 invalid api key 后端 401,message 为 invalid api key(不含 x-api-key) 后端 11 是 180.78 / 176.96 s
Not logged in · Please run /login 只设了 ANTHROPIC_AUTH_TOKEN,后端 401 后端 11 是 181.07 / 179.16 s
Credit balance is too low 402 billing_error,message 为官方的 credit balance 文案 后端 1 否 0.93 / 0.75 s
Credit balance is too low 400 invalid_request_error,message 同上 后端 1 否 0.99 / 0.71 s
Failed to authenticate. API Error: 403 Your API key does not have permission to use the specified resource. 403 permission_error 后端 1 否 0.98 / 0.72 s
API Error: 400 Model Not Exist 400 invalid_request_error 后端 1 否 0.96 / 0.68 s
There's an issue with the selected model (claude-haiku-4-5-20251001). It may not exist or you may not have access to it. Run --model to pick a different model. 404 not_found_error 后端 2 只追加 1 次非流式请求 0.95 / 0.77 s
API Error: Request rejected (429) · Number of request tokens has exceeded your per-minute rate limit 429 + retry-after: 1 后端 11 是,10 次 179.06 / 184.21 s
API Error: 529 Overloaded. This is a server-side issue, usually temporary — try again in a moment. If it persists, check your inference gateway (127.0.0.1:18913). 529 overloaded_error 后端 11 是 180.24 / 180.05 s
API Error: 500 Internal server error. …check your inference gateway (…) 500 api_error 后端 11 是 179.81 / 180.51 s
API Error: 502 Bad Gateway. …check your inference gateway (…) 502 + nginx HTML 页 后端 11 是 178.63 / 171.14 s
API Error: API returned an empty or malformed response (HTTP 200) — check for a proxy or gateway intercepting the request. … 200,但 body 是 HTML 或空 后端 2 只追加 1 次非流式请求 0.96 / 0.78 s
API Error: Connection dropped (ECONNRESET) stub 收到请求后直接断开 后端(网络) 11 是 185.36 / 171.91 s
API Error: Connection refused — a firewall or proxy may be blocking it (ECONNREFUSED) base URL 端口没人监听 本地网络层 0 是,照样重试满 184.27 / 182.76 s

有两点要特别注意:

  1. 同一句报错可能有两个来源。Invalid API key · Fix external API key 既可能是后端 401(重试 3 分钟),也可能是本地 header 校验(0.28 秒,后面多一段 · Invalid X-Api-Key header value…)。Not logged in 也一样:既可能是本地完全没有凭据,也可能是用 token 鉴权时被后端 401。后一种情况会误导你去跑 /login,其实该换 token。
  2. 格式校验没有触发。ANTHROPIC_API_KEY='bad key!@#' 两次都 exit 0、输出 OK,stub 收到的 x-api-key 值就是 bad key!@#。Invalid API key format… 这句未复现。另外,key 末尾多一个换行不会报错:stub 收到的是去掉换行后的 sk-abc123。

本地拦截:API key 含非 ASCII 字符,stub 收到 0 个请求,0.28 秒退出

重试时序:401 也会重试 10 次

stub 记下的 401 case 请求间隔(毫秒,r1):

545, 1020, 2254, 4300, 8248, 17336, 34291, 34866, 39673, 36396

也就是大约 0.5 → 1 → 2 → 4 → 8 → 16 秒逐次翻倍,从第 7 次开始封顶在 32~40 秒,重试 10 次后放弃,首尾跨度 168.5~184.8 秒。429、529、500、502、ECONNRESET 的 11 组数据走的是同一条曲线。所有请求的 x-stainless-retry-count header 都是 0,说明这是 Claude Code 应用层自己的重试,不是 SDK 层的。

429 的 retry-after: 1 会被遵守:3 次运行的首次间隔分别是 1009 / 1005 / 1006 ms,而其他错误码的首次间隔在 529~628 ms 之间。之后的间隔和其他错误码一样。

代码里的默认值和实测对得上:Vur=10(默认重试次数)、GCe=15(CLAUDE_CODE_MAX_RETRIES 的上限,超过会被夹小并告警)。另有一个 Yur=300,是某种模式下的默认值,触发条件没有查清。

后端 401 invalid x-api-key:重试 10 次,179 秒后才报 Invalid API key

把重试关掉:CLAUDE_CODE_MAX_RETRIES

同样是 401 stub,只加一个环境变量:

设置 stub 请求数 请求间隔 墙钟(2 次) stdout
默认 11 见上 179.43 / 168.78 s Invalid API key · Fix external API key
CLAUDE_CODE_MAX_RETRIES=2 3 628, 1082 / 548, 1124 ms 2.01 / 1.97 s 同上
CLAUDE_CODE_MAX_RETRIES=0 1 — 0.28 / 0.27 s 同上

stdout、stderr、退出码

61 次运行里,stderr 全部是 0 字节;所有报错都打在 stdout,末尾有 \n;失败一律 exit 1,成功 exit 0。也就是说:

  • 2>err.log 捕获不到任何报错内容,「stderr 为空就算成功」的判断方法会误判。
  • 退出码能发现失败,但不能区分 key 错、限流还是网关挂了,都是 1。

429 + retry-after: 1:同样重试 10 次,179 秒,exit 1

--output-format json 长什么样

三次 json 模式运行的关键字段:

case subtype is_error api_error_status terminal_reason result duration_ms
无凭据 "success" true null "api_error" Not logged in · Please run /login 130
401 "success" true 401 "api_error" Invalid API key · Fix external API key 171725
429 "success" true 429 "api_error" API Error: Request rejected (429) · … 182737

subtype 在 API 报错时仍然是 "success",只看它会误判为成功。判断要看 is_error 或 terminal_reason。result 键存在,内容就是报错原文。这和 Reached max turns 那篇不同:那边是 error_max_turns 子类型,result 键可能根本不存在。api_error_status 可以用来区分来源:后端错误给出 HTTP 状态码,本地凭据问题为 null。

第三方网关:到底发哪个 header

环境变量 stub 收到的鉴权 header
只设 ANTHROPIC_API_KEY x-api-key: <key>
只设 ANTHROPIC_AUTH_TOKEN authorization: Bearer <token>
两个都设 两个同时发:x-api-key: sk-stub-KEY-111 + authorization: Bearer tok-stub-TOKEN-222

两个都设时,Claude Code 不会替你选,网关收到两个 header,按它自己的优先级取。如果两个值来自不同的账号,出了问题会非常难查。

实践效果

把读数整理成可以直接照做的结论:

**1. 用户侧:ANTHROPIC_API_KEY 和 ANTHROPIC_AUTH_TOKEN 只设一个。**网关文档要求 Bearer 就只设 TOKEN,要求 x-api-key 就只设 KEY。设完先用下面的命令快速验证,不用干等 3 分钟:

CLAUDE_CODE_MAX_RETRIES=0 claude -p "reply with exactly OK" --output-format json < /dev/null \
  | jq '{is_error, api_error_status, result}'

api_error_status 为 null 且 is_error:true,说明问题出在本地凭据(没设,或者含非 ASCII 字符、中间有换行);是 401,说明 key 被后端拒绝。

**2. 看到 Not logged in · Please run /login,先看自己设的是哪个变量。**如果设的是 ANTHROPIC_AUTH_TOKEN,这句话很可能是 token 被 401 了,跑 /login 没有用。

**3. 脚本 / CI 里:**判断成败用退出码或 is_error,报错内容从 stdout 取;健康检查类的调用加 CLAUDE_CODE_MAX_RETRIES=0 或一个小值,否则一个配错的 key 会让流水线卡 3 分钟。长任务里不要设为 0,429/529 本来就应该退避重试。

4. 网关侧(给在做兼容层的人):

  • x-api-key 和 Authorization: Bearer 两个都要认;两个都带时,要明确自己的优先级。
  • key 无效时返回 401,Claude Code 会再重试 10 次,一次失败的调用你会收到 11 个请求。403 和 400 实测都不重试(1 个请求)。要不要为了这一点改状态码,取决于你的语义,这里只给出实测行为。
  • 401 的 message 里带上 x-api-key 字样,用户就会看到 Invalid API key · Fix external API key;不带的话,看到的是 Failed to authenticate. API Error: 401 <你的 message>。两种文案都实测过。
  • 余额不足时,只要 message 包含 credit balance is too low,402 和 400 都会显示成 Credit balance is too low。只返回 402、message 不含这段文案的情况没有测。
  • 5xx 报错里会带上 check your inference gateway (<你的 host:port>),用户第一眼就会怀疑你的网关。

5. 走 base URL 的更多坑:ANTHROPIC_BASE_URL 设了却不生效的情况见 这篇,接 DeepSeek 的完整配置见 Claude Code 接 DeepSeek 实测。

本轮 61 次运行全部打在本地 stub 上,真实 API 花费为 0。JSON 里对照组的 total_cost_usd: 0.000015 是按 stub 返回的 usage(10 输入 / 1 输出 token)用 haiku 标价折算出来的,不是真实计费。

踩坑

  • **设了 ANTHROPIC_BASE_URL 仍会连 api.anthropic.com。**每次运行都有 CONNECT api.anthropic.com:443:正常运行 5 次,无凭据 2 次,只设 token 2 次,重试满 10 次的运行 18~23 次。全程共 690 次,目标全是 api.anthropic.com:443,都被 stub 代理 403 拦下,对照组依然 exit 0。这些连接是做什么的,debug 日志里没写,没有归因。如果你的网络环境访问不到这个域名,这部分应该不影响主请求(本次就是被拦的状态),但只有这一种网络条件下的数据。
  • **一开始信了静态线索。**原计划把「key 格式非法 → 本地抛 Invalid API key format」当主角,实测 bad key!@# 直接成功了。静态字符串只能说明代码里有这句话,不能说明它在你的路径上会触发。
  • **本机 grep 扫大二进制太慢。**用 BSD grep 的 .{90}片段.{140} 扫 217 MB,约 35 分钟才出 2 个片段,后来改用 python mmap 按字面量找偏移再切片。这是 Bash 超时那篇 记过的坑,这次又犯了。
  • **重试类 case 每个约 3 分钟。**第一轮 4xx 组串行跑,超过 600 秒才意识到 401 也在重试,中途停掉改成每个 case 各一个 stub 并行跑。被打断的那次运行已重跑,表中是重跑的数据。
  • **截图说明。**三张图的输出文字,是实验 runner 捕获的原始 stdout 字节;real 行是 runner 记录的墙钟,不是 shell 的 time 实际输出;图 2 命令里的 sk-wrong 是展示用占位,实际打的 key 是 sk-stub-valid-123,由 stub 返回 401;图 1 的 key 通过环境变量传入(sk-密钥-123),命令行里没有显示。另外用 script(1) 真 TTY 录了 5 个快速 case,和 runner 的 stdout 逐字节比对一致(-p 的 text 模式没有颜色)。

未验证清单:Invalid API key format…(未复现,只在 OAuth 建 key 流程里);Invalid ANTHROPIC_CUSTOM_HEADERS(未测);402 但 message 不含 credit 文案(未测);429 之后恢复成功的「失败→成功」序列(未测);交互式(非 -p)会话里的显示和重试(未测);Yur=300 的触发条件和那些 CONNECT 的用途(未归因)。每个 case 只跑了 2 次,重试间隔的抖动范围只基于这些样本。

相关阅读

相关文章

Claude Code 报错 Command timed out after 2m 0s 怎么解决:两条超时路径、BASH_DEFAULT_TIMEOUT_MS 与 run_in_background 实测

Claude Code 报错 Command timed out after 2m 0s 怎么解决:两条超时路径、BASH_DEFAULT_TIMEOUT_MS 与 run_in_background 实测

Claude Code 的 Bash 工具默认 120 秒超时。本文在 2.1.280 上跑了 19 次真实会话,结论是:超时分两条路径,只有首词是 sleep 的命令才会被杀,并报 Exit code 143 / Command timed out after 2m 0s;其它命令到点会被转到后台,在 claude -p 收尾 5 秒后被 kill。无论哪条路径,claude 进程都 exit 0、stderr 为 0 字节,JSON 顶层 is_error=false。BASH_DEFAULT_TIMEOUT_MS=8000 实测 8.17 秒被杀;设成 0 或 abc 会静默回落 120 秒;显式 timeout 超过 BASH_MAX_TIMEOUT_MS 会被静默夹到 15 秒。

claude-codeclaude-code-lab+4
pitfalls2026年9月26日8 min
31
Claude Code 报 Error: Reached max turns (1):无头护栏掐断时,文件可能已经写了

Claude Code 报 Error: Reached max turns (1):无头护栏掐断时,文件可能已经写了

claude -p 设了 --max-turns 或 --max-budget-usd,触顶时输出 Error: Reached max turns (1) 或 Error: Exceeded USD budget (0.01),exit 1。我在 2.1.270 和 2.1.280 上各跑了一轮:报错分别是 28 和 33 字节,不带换行,打在 stdout 而不是 stderr;json 模式下 result 字段直接缺失,jq -r .result 会打印字符串 null,而且 jq 退出码是 0。更要命的是「报失败≠没干活」:--max-turns 2 报错时 out.txt 已经写好;预算要等一次调用返回才检查,0.05 美元的预算实际花掉 0.0517,任务其实已完成却仍 exit 1;预算掐断还会留下 out.txt.tmp.* 残片。交互模式则完全不理 --max-turns。

claude-codeheadless+5
hands-on2026年9月25日10 min
73
Claude Code MCP server Failed to connect 怎么排查:CONNECTION_CLOSED、connection timed out after 30000ms、ENOENT 逐条实测

Claude Code MCP server Failed to connect 怎么排查:CONNECTION_CLOSED、connection timed out after 30000ms、ENOENT 逐条实测

claude mcp list 显示 ✘ Failed to connect,但「连不上」可能是命令不存在、PATH 缺 node、server 启动即退出、不回握手、端口没服务或配置写错。本文用自写 stub 在 Claude Code 2.1.280 上逐条复现:mcp list 失败也返回 0;CONNECTION_CLOSED 背后可能是两种完全不同的原因,真因只在 --debug-file 里;一个不回握手的 server 会让 claude -p 墙钟从 4.63s 变成 36.21s,而 duration_ms 只从 4323.5 涨到 5930。

mcpclaude-code+4
pitfalls2026年9月24日8 min
76

DeepSeek 标称 1M、Claude Code 只认 200K:两个都测了,真正卡住你的是第三个数

DeepSeek 标称 1M 上下文,而 Claude Code 接上去之后对同一个模型报告 contextWindow 200000。我把两边都测了。DeepSeek 的真实上限是 1,048,576——字面的 2 的 20 次方,不是一百万整——而且这个额度**含**你的 max_tokens 输出预算,用一组对照实验坐实。针插在第 0 位、上下文 1,039,744 token 时仍被准确捞出。Claude Code 则在远低于此处就本地拒绝,25 毫秒、零 API 调用,而且它卡的**根本不是 token**:闸门在约 48 万字符。喂高熵文本,478,000 字符顺利通过、真实 token 高达 309,567——比它刚刚自称的 20 万窗口还多 55%。而日常使用里这三个数你一个都碰不到,因为 Bash 输出超过整 30,000 字符就压根不进上下文。

claude-code长上下文+5
hands-on2026年9月20日6 min
149