前言
最近在 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 的健康检查请求被送进了代理。
根本原因拆解
整个故障链路其实是一连串的连锁反应:
-
代理配置继承:macOS 终端通过代理工具自动注入了
HTTP_PROXY、NO_PROXY等环境变量,其中NO_PROXY使用了127.这种前缀通配格式。 -
运行时不兼容:OpenCode 底层使用 Bun 运行时,其内置的 fetch 实现不支持
127.这种不带星号的前缀通配写法,只认完整 IP 或localhost。于是127.0.0.1没有命中排除列表,请求被转发到代理 127.0.0.1:7890。 -
健康检查失败:客户端对托管服务做健康探测时,请求走了代理,代理转发失败返回 502,客户端判定「服务未启动」。
-
重复拉起服务:客户端认为服务 missing,再次执行
serve --service拉起第二个实例。 -
端口占用报错:第二个实例绑定 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 配置好,省得日后踩坑。