踩坑实录:OpenCode 端口 49374 占用之谜

27 阅读5分钟

前言

最近在 macOS 上升级 OpenCode 到 v2.0.32 后,遇到了一个非常迷惑的启动故障:命令行一直卡在 Starting background server...,足足两分钟后抛出一句 Managed service port 49374 on 127.0.0.1 is already in use。

端口被占用?查了一圈却发现占用端口的正是 OpenCode 自己。本文完整复盘这次排查过程,带你看清「NO_PROXY 通配符兼容性」这个隐蔽的坑,以及它是如何一步步导致服务重复拉起的。

问题现象

环境:macOS + 终端配置了 Clash Verge 代理(127.0.0.1:7890),OpenCode v2.0.32(npm @opencode/cli 安装)。

执行 opencode 命令后,终端长时间停留在启动后台服务的阶段:

Starting background server...

大约 2 分钟后报错:

Error: Managed service port 49374 on 127.0.0.1 is already in use by another process.

第一反应是有其他进程占了 49374 端口。但用 lsof -i:49374 一查,监听端口的进程正是 opencode serve --service——服务明明已经启动了,客户端却认为它没起来。

排查过程

第一步:验证服务本身是否正常

既然端口在监听,直接用 curl 访问一下看看:

curl [http://127.0.0.1:49374/](http://127.0.0.1:49374/)

返回了 502 错误。奇怪,服务在监听,怎么会 502?

加个 --noproxy '*' 参数再试:

curl --noproxy '*' [http://127.0.0.1:49374/](http://127.0.0.1:49374/)

这次正常返回 200。

线索出现了:请求被代理了。 本地回环地址的请求走到了系统代理,代理无法正确转发,返回了 502。

第二步:检查代理环境变量

查看当前终端的代理环境变量:

env | grep -i proxy

发现 NO_PROXY 和 no_proxy 的值类似这样:

NO_PROXY=172.31.,127.,localhost

这是代理工具常见的「通配写法」——用 127. 表示匹配所有 127 开头的 IP,对应 Windows 上的传统写法,Mac 端很多代理软件也沿用了这一习惯。

第三步:验证 NO_PROXY 是否生效

手动设置精确的 NO_PROXY 后再启动:

export NO_PROXY=127.0.0.1,localhost
export no_proxy=127.0.0.1,localhost
opencode

一切正常,秒启动。

至此问题已经定位:127.* 这种通配写法,在 Node/Bun 的 fetch 实现中不被识别,导致对 127.0.0.1 的健康检查请求被送进了代理。

根本原因拆解

整个故障链路其实是一连串的连锁反应:

  1. 代理配置继承:macOS 终端通过代理工具自动注入了 HTTP_PROXY、NO_PROXY 等环境变量,其中 NO_PROXY 使用了 127. 这种前缀通配格式。

  2. 运行时不兼容:OpenCode 底层使用 Bun 运行时,其内置的 fetch 实现不支持 127. 这种不带星号的前缀通配写法,只认完整 IP 或 localhost。于是 127.0.0.1 没有命中排除列表,请求被转发到代理 127.0.0.1:7890。

  3. 健康检查失败:客户端对托管服务做健康探测时,请求走了代理,代理转发失败返回 502,客户端判定「服务未启动」。

  4. 重复拉起服务:客户端认为服务 missing,再次执行 serve --service 拉起第二个实例。

  5. 端口占用报错:第二个实例绑定 49374 端口时触发 EADDRINUSE,约 2 分钟超时后抛出「端口被占用」的错误。

最迷惑人的地方在于:报错说端口被「另一个进程」占用,但实际上占用端口的就是第一个已经正常启动的服务实例——它一直好好地在监听,只是客户端的健康检查看不见它。

临时解决方案

在 macOS 终端中执行以下命令,设置精确的 NO_PROXY 后再启动 OpenCode:

# 同时设置大写和小写,兼容不同程序的读取习惯
export NO_PROXY=127.0.0.1,localhost
export no_proxy=127.0.0.1,localhost

# 重启 OpenCode 服务
opencode service stop
opencode

如果想要一劳永逸,可以把这两行 export 加到 ~/.zshrc 或 ~/.bash_profile 中:

echo 'export NO_PROXY=127.0.0.1,localhost' >> ~/.zshrc
echo 'export no_proxy=127.0.0.1,localhost' >> ~/.zshrc
source ~/.zshrc

更深层的思考与修复建议

站在开源项目的角度,这个问题其实有两个可以优化的点:

1. 本地健康探测应强制绕过代理

对 127.0.0.1 的本地服务健康检查,本质上不应该走任何代理。无论用户的系统代理如何配置,托管服务的存活探测都应当无条件直连。

实现方式也很简单:在发起 fetch 请求时显式指定 agent: null,或者在请求前临时清空代理环境变量。业界很多工具(如 VS Code 的某些组件)都是这么处理的。

2. 端口占用时应先判断是否为自身实例

当发现目标端口已被占用时,不要直接报错退出。可以先向该端口发送一次探测请求,如果能正确响应且确认是 OpenCode 自身的服务实例,就直接复用现有实例,而不是强行启动第二个。

这样既能避免 EADDRINUSE 错误,也能提升启动速度——这在 CLI 工具中是很常见的「单实例守护」设计。

总结

这是一个非常典型的「环境兼容性」问题:

  • 代理工具习惯用 127. 这种简洁的通配写法
  • Node/Bun 的 HTTP 客户端严格遵循规范,只支持完整域名/IP
  • 两者相遇就产生了「本地请求走代理」的反直觉现象

排查这类问题时,记住一个万能验证命令:

curl --noproxy '*' <地址>

如果加了 --noproxy 就正常,基本可以断定是代理相关的问题。

希望这篇踩坑记录能帮到同样遇到 49374 端口占用的同学。如果你也在用 OpenCode + 代理环境,不妨提前把 NO_PROXY 配置好,省得日后踩坑。