NodeJS 连 MongoDB 一直 pending,把 Codex 的 Base URL 改到 TaoToken 后让 Codex 查 mongoose 的 connect 参数
2026/9/17 2:59:19 网站建设 项目流程

NodeJS mongoose 连 MongoDB 卡住时,TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end)可以充当 Codex 的兼容 API 通道:在官网创建一把 API Key,把 Codex 的 Base URL 填成 https://taotoken.net/api,剩下的分析工作交给 Codex。

这篇要拆的场景很具体:一个npm init -y出来的空项目,根目录放了index.js,里面用mongoose.connect("mongodb://localhost/mongoose_test")发起连接,紧跟着mongoose.connection.once("open", () => console.log("数据库连接成功"))。跑起来之后,控制台既没有「数据库连接成功」,也没有任何错误堆栈,进程就那么挂着。原文第 4 步还留了一行mongoose.disconnect(),这行本身就是嫌疑之一。

这类问题最难受的地方在于「没有报错」。MongoDB 驱动在选服务器阶段会反复重试,默认serverSelectionTimeoutMS是 30000 毫秒,这 30 秒里它一声不吭,超时才抛错。你盯着终端发呆的每一秒,驱动都在原地打转。

下面把整条链路拆开:先让 Codex 读懂你的连接代码,再逐条比连接串、端口、服务状态,最后确认open事件到底有没有机会被触发。整个过程里 Codex 只负责读代码、给检查命令、解释 mongoose 的语义,真正执行命令和改代码的人是你。

1. open 事件不触发,先分清「在连」和「连不上」

1.1 mongoose.connect 只是发起动作,open 是结果通知

mongoose.connect()内部干的事是这样的:解析连接串 → 让 MongoDB 驱动建立连接池 → 向目标地址发握手(hello 命令)→ 完成认证 → 拓扑进入 connected 状态 → mongoose 把这个状态映射成connection上的open事件。前面任何一步没走完,open就不会 emit。

所以「一直 pending」的准确含义是:握手阶段没有拿到返回,而不是「代码写错了」。connection本身是个 EventEmitter,同族事件还有connectingconnecteddisconnectedcloseerror。原文只监听了open,等于只等最后一声哨响,中间走了几步完全看不见。临时加几个监听,链路走到哪里一眼就清楚:

const mongoose = require('mongoose'); mongoose.connection.on('connecting', () => console.log('[state] connecting')); mongoose.connection.on('connected', () => console.log('[state] connected')); mongoose.connection.on('error', (err) => console.log('[state] error:', err.message)); mongoose.connection.on('disconnected', () => console.log('[state] disconnected'));

加上之后重新跑一次。如果连connecting都没打印,说明问题在更外层;如果打印了connecting却始终没有connected,那就是网络层或服务层没通。

1.2 原文那行 mongoose.disconnect() 是头号嫌疑

原文注释写得没错——「断开数据库:mongoose.disconnect() 一般不需要」。但示例代码里这行是真的执行了,而且紧跟在connect后面,没有await,也没有塞进任何回调。

Node 的事件循环里,connect()只是把连接任务丢给驱动,TCP 握手和后续命令都在异步阶段完成;disconnect()却在同一个 tick 里立刻去关连接池。连接还没走到open,就被自己掐断了,事件自然永远等不到。

处理办法有两个,都很直接。第一种是先把这行删掉,确认连接成功之后再决定要不要主动断开;第二种是把它挪进open回调,或者改用await的写法:

const mongoose = require('mongoose'); async function main() { await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test'); console.log('数据库连接成功'); // 需要主动断开时再调用 // await mongoose.disconnect(); } main().catch((err) => console.error('连接失败:', err.name, err.message));

1.3 「没报错」和「有报错」的排查方向完全不同

有报错的时候,栈里直接写着ECONNREFUSEDAuthentication failed或者MongooseServerSelectionError,照着名字查就行。没有报错的时候,你只能从「哪一层没返回」倒推。

一个习惯值得养成:把connect返回的 Promise 接住。mongoose.connect()返回的是 Promise,不接住的话,超时错误会以未处理拒绝的形式飘走,看起来就像什么都没发生。

mongoose.connect(uri, { serverSelectionTimeoutMS: 5000 }) .catch((err) => console.error('connect 失败:', err.name, err.message));

serverSelectionTimeoutMS从默认的 30 秒压到 5 秒,是为了让排障节奏快一点。调试阶段没必要等半分钟才知道结果。注意这只是缩短等待,不会让连接变得能通。

2. 把 Codex 的 Base URL 指到 TaoToken 的兼容通道

2.1 拿 Key 这一步在官网完成

打开 TaoToken 官网 注册账号,进控制台创建一把 API Key,复制出来先放好。文中所有配置里它都写成占位符YOUR_API_KEY,别把真实 Key 贴进任何代码仓库。

Key 是给 Codex 用的,不是给 mongoose 用的。mongoose 连的是本地 MongoDB,两件事互不相干——这一点在排障时很重要,否则很容易误以为是 Key 配错导致连接卡住。

模型 ID 不要凭记忆写。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的模型广场,看当时列表里有哪些可用项,挑一个再填进配置。

2.2 ~/.codex/config.toml 里写 model_provider 与 base_url

Codex 的配置文件在用户目录下的.codex/config.toml。要做的只有一件事:加一个自定义 provider,把base_url指向兼容通道,把 Key 放在环境变量里。

# ~/.codex/config.toml model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"

base_url末尾不要带/v1。写https://taotoken.net/api就够了,多写一层会在拼接后变成重复路径。也不要往base_url后面挂任何查询参数,UTM 只属于给人点的落地页,不属于接口地址。

环境变量按系统来设置:

# macOS / Linux export TAOTOKEN_API_KEY=YOUR_API_KEY
# Windows PowerShell $env:TAOTOKEN_API_KEY="YOUR_API_KEY"

设完重开一个终端,让变量生效,再启动 Codex。如果这一步看到 401,先回头确认环境变量名和env_key里写的名字是否完全一致,大小写都算。

2.3 让 Codex 先复述,再动手

排障场景下,最容易出问题的不是 Codex 的能力,而是它的热情。你不说清楚,它可能顺手把index.js重写一遍,把连接逻辑换成别的写法,结果你连原来的问题都看不见了。

第一条指令建议写成这样:把index.js全文和package.json一起贴过去,明确要求它只做分析,不改业务逻辑。

提示:请先复述这段代码的执行顺序,然后指出 mongoose.connect 到 connection.once('open') 之间可能被中断的位置。不要改写代码,只列疑点和需要我在本地执行的检查命令。

这句约束能让 Codex 把注意力放在连接时序上,而不是急着给你一个新版本。它给出的每一条命令,都由你在本机终端执行,再把输出贴回对话。

3. 让 Codex 逐项核对 mongoose.connect 的参数

3.1 localhost 解析成 ::1 的坑

连接串里写localhost很自然,但在 Node 17 之后,DNS 结果不再做 IPv4 优先的重排,localhost有可能先解析到::1。而 MongoDB 服务端默认的bindIp127.0.0.1,也就是只听 IPv4。两边对不上,就会出现连不上。

如果本机对::1的入站是直接丢弃而不是明确拒绝,表现就是「一直等」,连ECONNREFUSED都看不到。这也是「pending 而不报错」最常见的成因之一。

把连接串里的localhost换成127.0.0.1,是最省事的一步。想更稳一点,可以在选项里显式指定family: 4

3.2 把隐式默认值显式写出来

原文连接串省掉了端口,因为 27017 是默认值。省掉没问题,但排障阶段建议把端口、库名、关键选项都写全,让 Codex 一眼能看出你到底在连什么。

const mongoose = require('mongoose'); const uri = 'mongodb://127.0.0.1:27017/mongoose_test'; mongoose.connect(uri, { family: 4, // 强制 IPv4,绕开 ::1 解析 serverSelectionTimeoutMS: 5000, // 选服务器超时,默认 30 秒 connectTimeoutMS: 5000, // 单次连接超时 directConnection: true, // 单机 mongod 场景 });

这几个参数的含义,可以整段丢给 Codex 让它用中文解释一遍:serverSelectionTimeoutMS管的是「多久找不到可用节点就放弃」,connectTimeoutMS管的是「单次 TCP 连接多久算超时」,family管的是 DNS 解析出来的地址族。它解释完,你再对照自己的实际部署情况判断哪些该留、哪些该删。

3.3 事件注册顺序与 disconnect 的位置

once('open', ...)写在connect()之后是没问题的,因为握手是异步的,注册监听的动作会先完成。真正容易出问题的是两处:一是把监听写在await mongoose.connect()之后,那时open早就发过了,你等的是一个不会再来一次的事件;二是前面说过的disconnect()抢跑。

把这两点合并成一条指令给 Codex:请检查我的监听注册位置是否晚于 open 事件触发,以及有没有在当前 tick 内关闭连接池的调用。

3.4 版本差异交给 Codex 对照

mongoose 的版本跨度会带来行为差异。老教程里常见的useNewUrlParseruseUnifiedTopology在新版本里已经无效;strictQuery在不同大版本里的默认值变过;回调风格的 API 也被逐步移除。与其靠记忆,不如把package.json里的 mongoose 版本号贴给 Codex,让它在文档语义层面告诉你当前版本该怎么写。

注意:Codex 只能读你的代码和依赖版本,它没有办法替你去连 MongoDB,也没有办法替你看本机进程。它给出的判断,需要你用本地命令去验证。

4. 27017 端口与本地 MongoDB 服务:命令 Codex 给,执行在你机器上

4.1 先看有没有进程在听 27017

这一步判断的是「服务端到底有没有在那个端口上等客」。三套系统命令不同,选自己那套。

# Windows PowerShell Test-NetConnection -ComputerName 127.0.0.1 -Port 27017 netstat -ano | findstr :27017
# macOS / Linux nc -vz 127.0.0.1 27017 lsof -iTCP:27017 -sTCP:LISTEN ss -lntp | grep 27017

nc -vz成功时会显示 succeeded,拒绝时显示 refused,卡住不动则说明包被丢了。netstatlsof看的是有没有进程处于 LISTEN 状态。三者结合起来,端口这一层基本就定性了。

4.2 再确认 mongod 服务本身起没起

端口没人听,通常就是服务没起。按安装方式选命令:

# macOS(Homebrew 安装) brew services list | grep mongodb
# Linux(systemd) systemctl status mongod
# Windows 服务 Get-Service -Name MongoDB

如果是容器跑的,换一套:

docker ps --filter "publish=27017" docker port <容器名或ID>

容器场景有个常见疏漏:docker run时没写-p 27017:27017,容器内部服务起着,宿主机上却没有任何端口在听,Node 侧注定连不上。

4.3 用 mongosh 做一次独立验证

在怀疑 mongoose 之前,先用官方客户端确认数据库本身可用:

mongosh "mongodb://127.0.0.1:27017/mongoose_test" --eval "db.runCommand({ping:1})"

返回{ ok: 1 }就说明服务、端口、库名这条路是通的,问题就落回 Node 侧的写法上;如果 mongosh 也连不上,就别在 mongoose 代码里绕了,先把服务修好。

把这几条命令的输出原样贴回 Codex 的对话里,让它结合index.js给出下一步。它看到ECONNREFUSED会指向服务未启动,看到超时会指向地址族或防火墙,看到Authentication failed会转向凭据检查——这些判断都需要真实输出作为依据。

5. 改完之后验证 open 到底来没来

5.1 一份最小可运行的验证脚本

把上面散落的改动合到一个文件里,先跑通再谈业务:

const mongoose = require('mongoose'); const uri = 'mongodb://127.0.0.1:27017/mongoose_test'; async function main() { mongoose.connection.on('connecting', () => console.log('[state] connecting')); mongoose.connection.on('error', (err) => console.error('[state] error:', err.message)); try { await mongoose.connect(uri, { family: 4, serverSelectionTimeoutMS: 5000, connectTimeoutMS: 5000, directConnection: true, }); console.log('数据库连接成功,readyState =', mongoose.connection.readyState); const ping = await mongoose.connection.db.admin().ping(); console.log('ping 结果:', ping); } catch (err) { console.error('连接失败:', err.name, err.message); } finally { await mongoose.disconnect(); } } main();

跑之前确认一句:这是在本机终端执行,不是让 Codex 去连你的数据库。Codex 的职责到「生成这段脚本、解释每个参数」为止,运行和看输出都在你这边。

5.2 readyState 的 0/1/2/3 各自意味着什么

mongoose.connection.readyState是个数字,比你翻日志快得多:

含义排障含义
0disconnected从未开始连,或已被断开
1connected握手完成,可以发命令
2connecting正在握手,pending 就是卡在这里
3disconnecting正在关闭连接池

打印出 1,说明连接成功,open事件也一定发过了。打印出 2 并且长时间不变,说明卡在选服务器阶段,回到第 4 节的端口和服务检查。打印出 0 且脚本立刻结束,多半是disconnect()抢跑。

5.3 还在 pending 时的对照表

现象常见成因先查什么
静默十几秒后报超时mongod 未启动或端口未监听进程与服务状态
立刻报ECONNREFUSED ::1:27017localhost 解析到 IPv6连接串改 127.0.0.1,加 family: 4
Authentication failed连接串缺账号或库级权限不足用 mongosh 验证凭据
connected 出现过但 open 没触发监听注册太晚调整监听位置
从来没有输出,进程直接退出disconnect 提前关闭连接池删掉或后移 disconnect
MongooseServerSelectionError直连与副本集模式不匹配directConnection、replicaSet

这张表可以整段交给 Codex,让它对照你的实际报错挑一行出来解释。有真实报错时,排查效率比空谈高得多。

6. 这次排障的几次调用,回到控制台对一下

6.1 在用量里确认 Codex 确实走了这把 Key

配通之后,Codex 每读一次你的index.js、每解释一次参数,都是一次真实的模型调用。回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end 的用量页面,看看这几轮对话有没有被记录下来。

如果调用记录是空的,说明 Codex 根本没走到这条通道,回头检查config.toml里的base_url是不是被写成了带/v1的地址,或者环境变量没生效。这类问题和 mongoose 的连接问题是两回事,别混在一起排查。

6.2 下一步

这套流程跑顺之后,可以把它变成固定动作:本机服务状态用命令行确认,代码层面的疑点交给 Codex 分析,两边互不越界。想先试一下通道是否可用,去 模型对话 用同一把 Key 发条消息;长期要靠它读代码、查参数,可以看 Coding Plan 的额度是否够用;Key 本身在 控制台 API Keys 里创建和管理。

回到index.js本身,最该记住的还是那两件事:连接串里写127.0.0.1而不是localhost,以及别在同一个 tick 里disconnect。搞定这两点,数据库连接成功那行字就会如约出现在终端里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询