同一套代理下,codex exec 连续成功,交互式 TUI 却稳定报 tls handshake eof。这次排查最有用的收获不是找到了某个“神奇节点”,而是把短连接、非交互调用和真实 TUI 传输路径拆开验证。


问题现象

环境是 WSL,使用 Clash/Mihomo 类代理,Codex CLI 版本为 0.145.0,通过 ChatGPT OAuth 登录。

最初使用默认代理出口运行 Codex 时,下面两个接口都返回 HTTP 403:

1
2
https://chatgpt.com/backend-api/codex/models
https://chatgpt.com/backend-api/codex/responses

Cloudflare 返回页提示 VPN 可能受限。切换到一个标有 GPT 解锁能力的候选节点后,非交互命令马上恢复:

1
codex exec --skip-git-repo-check "hi"

命令能够正常返回。到这里很容易以为问题已经解决,但交互式 codex 输入 hi 仍然失败,先出现:

1
2
Falling back from WebSockets to HTTPS transport.
stream disconnected before completion: tls handshake eof

随后回退到 HTTPS 仍然报错:

1
stream disconnected before completion: error sending request for url (https://api.openai.com/v1/responses)

启动日志里还有 bubblewrap 缺失警告,但它只表示当前环境没有该沙箱工具,与这次网络传输失败无关。不能因为两条信息出现在同一屏,就把它们当成因果关系。


先把 403 和 TLS EOF 分开

这两类错误发生在不同阶段,排查方向也不同。

HTTP 403 说明请求已经到达 HTTP 服务或其前置防护层,并收到明确拒绝。结合 Cloudflare 的提示,这一阶段优先检查代理出口是否被目标站点限制、OAuth 会话是否可用,以及请求是否经过了预期代理。

tls handshake eof 则发生在 TLS 握手期间。连接在完成安全会话之前被提前关闭,常见方向包括代理转发异常、中间链路主动断开、TLS 劫持、SNI 或证书处理错误,以及客户端特定传输实现与代理栈不兼容。

这次切换候选节点后,403 消失,codex exec 也能成功,说明最初的 HTTP 访问限制已经绕开。但 TUI 继续出现 TLS EOF,这是第二个问题。若把二者合并成“代理不好”,后面的测试会失去针对性。


短连接只能做第一层筛选

我先对若干 GPT 候选节点做短连接基线测试,轮流请求:

1
2
https://chatgpt.com/backend-api/codex/models
https://api.openai.com/v1/models

每个候选重复 10 次。多数候选节点是 10/10 成功,最快候选的平均耗时约为 0.52 秒;另有一个候选只有 4/10 成功。

这组测试适合快速排除明显不可用的节点,也能发现基础 HTTPS 请求的延迟和稳定性差异。但它只证明普通请求能建立连接并拿到响应,不能证明长时间流式传输、WebSocket 或 TUI 的回退路径稳定。

接着我用真实的非交互命令测试短连接排名靠前的三个候选节点:

1
codex exec --skip-git-repo-check "hi"

每个候选先跑 2 次,全部成功;再各跑 5 次,也都是 5/5 成功,平均耗时约为 11.2 秒、11.5 秒和 12.8 秒。

这个结果能证明 codex exec 在这些候选节点上稳定可用,却仍不能替代 TUI 测试。后来真正跑交互反馈环时,三者依旧全部失败。普通 curlcodex exec 都是绿的,TUI 仍然可以是红的。


从会话元数据确认两条路径确实不同

为了避免只根据终端表象猜测,我查看了 Codex 生成的会话 JSONL。这里不需要公开会话 ID 或本机路径,只比较与入口相关的字段。

失败的交互会话显示:

1
2
3
4
{
"originator": "Codex Desktop",
"source": "vscode"
}

该会话大约 49 秒后进入 task_complete,错误最终指向:

1
https://api.openai.com/v1/responses

成功的非交互会话则显示:

1
2
3
4
{
"originator": "codex_exec",
"source": "exec"
}

这组元数据不能单独解释 TLS 为什么失败,但足以确认:成功的 exec 和失败的交互请求不是同一种入口。既然入口和 source 不同,就不能用 exec 成功直接证明 TUI 的 transport 正常。


建立一个能红能绿的 TUI 反馈环

真正有用的测试必须覆盖用户实际失败的路径,而且结果要能自动分类。我使用 tmux 启动 TUI,并关闭 alternate screen,方便捕获 pane 内容:

1
codex --no-alt-screen "Reply with exactly INTERACTIVE_OK"

测试脚本对每个候选节点执行相同流程:

  • 切换到一个候选节点,并确认切换动作完成。
  • 在独立 tmux 会话中启动同一条 Codex TUI 命令。
  • 持续捕获 pane 或日志,等待明确成功标记或已知错误。
  • 出现 INTERACTIVE_OK 时记为成功。
  • 出现 tls handshake eof 时记为 TLS_EOF
  • 出现 stream disconnected 或 403 时分别归类。
  • 到达规定时间仍无结果时记为超时,而不是猜测成功或失败。
  • 无论命令成功、失败还是超时,都恢复原节点并清理 tmux 会话。

成功标记必须足够严格。这里只接受精确的 INTERACTIVE_OK,不能因为界面启动、进程仍存活或输出了部分状态文字就判定成功。

清理逻辑也不能省。批量测试中如果某次超时后没有恢复原节点,下一轮就不再是单变量实验;残留 tmux 会话还可能继续占用连接,污染后续结果。


逐个节点测试后的结果

我先测试了 7 个 GPT 候选节点。它们全部在约 2 至 3 秒内出现 TLS_EOF,没有一个返回 INTERACTIVE_OK

随后把范围扩展到订阅中的全部可用候选节点,逐一执行同样的 TUI 测试。结果仍然没有任何候选返回 INTERACTIVE_OK

  • 大多数候选节点出现 TLS_EOF
  • 少数候选节点出现 STREAM_FAIL
  • 少数测试在 65 秒后超时。
  • 某候选节点出现严重证书错配:请求 api.openai.com 时,收到的证书却匹配无关站点域名。

证书错配不能简单归入“网络慢”。它提示该候选链路可能存在错误路由、TLS 劫持或透明代理异常,因此应直接排除该候选节点,并避免在这条链路上传输敏感信息。

更重要的是,失败并不集中在单一候选节点。短 HTTPS 基线良好、codex exec 连续成功的候选节点,在真实 TUI 路径上仍然稳定 TLS EOF。继续盲目换节点已经不能解释现象。


结论

现有证据支持以下判断:

  • 最初的 403 与代理出口限制有关,切换候选节点后,基础接口和 codex exec 已恢复。
  • TUI 的 TLS EOF 不是单一候选节点造成的,换节点没有解决。
  • Codex 0.145.0 的 TUI/Codex Desktop 传输路径与当前 WSL 代理栈之间存在系统性兼容问题的可能性较高。
  • 另一种可能是该版本 TUI 自身存在传输 Bug。
  • codex exec 可作为当前可验证的临时 workaround。

但还不能宣布根因已经确定。现阶段没有完成版本二分,也没有抓取足够的网络层证据来区分客户端 Bug、代理实现兼容性和中间链路行为。文章里的“可能”必须保留,不能把相关性写成已证实因果。

这次排查留下的方法

这次最容易走偏的地方,是过早把“某一层成功”当成“整条链路正常”。以后遇到类似问题,我会按下面的顺序处理:

  • 先按协议阶段区分 HTTP 403、TLS EOF、流中断和超时,不把所有错误统称为“代理问题”。
  • 用普通 HTTPS 请求建立基线,但只把它当作第一层筛选。
  • 用真实非交互命令验证 exec 路径,再为 TUI 单独建立反馈环。
  • 让测试同时具备明确成功标记、错误分类和硬超时,结果才能稳定复现。
  • 逐个候选节点做单变量测试,并在每轮结束后恢复原节点、清理 tmux。
  • 查看会话元数据中的 sourceoriginator,确认成功与失败是否来自同一入口。
  • 不因 systemd 状态正常、进程仍存活或普通 curl 成功就提前结束排查。
  • 遇到证书与目标域名不匹配时立即停止使用该链路,而不是忽略警告继续重试。

短连接是绿的,只能证明短连接是绿的。要判断交互式工具是否正常,最终仍要测它真实使用的那条传输路径。


参考资料