fix: refuse Node 26+ until qdrant-js gains undici 7 compat#59
Conversation
Under Node 26+, the very first qdrant request crashes with `UND_ERR_INVALID_ARG: invalid onError method`. Root cause is a version mismatch: @qdrant/js-client-rest constructs an undici.Agent from its pinned undici ^6 and passes it as the dispatcher to Node's built-in fetch(), which under Node 26 uses a newer undici with stricter dispatcher-hook validation. The bug surfaces on the first real codebase_search / codebase_index call — the MCP handshake succeeds, then everything fails. The error message gives no hint about Node version, so users on Node 26+ lose significant time debugging. This change: - Adds a runtime pre-flight check at index.ts entry that prints a clear actionable error and exits 1. Per ESM the imports below evaluate first, but qdrant-js's module init is side-effect-light, so exiting at the first top-level statement is enough. - Tightens engines.node to `>=18.0.0 <26.0.0` so npm/npx warns at install time. Both can be reverted once one of qdrant/qdrant-js#123 (undici major upgrade) or qdrant/qdrant-js#128 (inject fetch) lands. Refs: qdrant/qdrant-js#134 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a Node.js v26 compatibility guard: runtime pre-flight exits on Node >=26 with a stderr message and ChangesNode.js v26 Compatibility Guard
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/index.ts`:
- Around line 19-25: The stderr warning is written async with
process.stderr.write but the code immediately calls process.exit(1), which can
truncate the message; update the exit logic in src/index.ts so the warning is
flushed before terminating: either set process.exitCode = 1 and return (letting
the event loop drain) or listen for the drain/finish of process.stderr.write and
only call process.exit(1) after the write completes; adjust the use of
process.stderr.write and remove the immediate process.exit invocation so the
full message is guaranteed to be emitted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0242cf09-c59e-4383-a04f-e83609394fac
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (2)
package.jsonsrc/index.ts
Per CodeRabbit review on giancarloerra#59: process.stderr.write() is async when stderr is piped (every MCP host captures stderr to surface server logs), so a bare `process.exit(1)` immediately after the write terminates synchronously without draining I/O — risking truncation of the compatibility warning that this guard exists to surface. Move the exit into the write callback so the message is guaranteed to flush before the process terminates. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Third-pass review caught that the callback shape from 5cd9db0 (while correctly fixing the stderr-truncation concern CodeRabbit raised) introduced an async-exit window. With process.exit(1) inside the write callback, on Node 26+ the rest of the file's top-level code runs before termination: imports' top-level evaluation, the McpServer/tool registrations, and the start of main()'s connect — the MCP host can briefly see a handshake begin before the process dies. fs.writeSync(2, msg) is the canonical Node pattern for "print fatal error then die" — blocking (no truncation when stderr is piped) AND synchronous (so process.exit(1) runs before any further top-level code). Strictly better than the callback shape on both axes. Also soften comment phrasing to reduce rot risk: - "Candidate fixes already in flight" -> "Upstream PRs under discussion" - "Once one lands" -> "If either lands -- or any other fix supersedes them" Verified: full 4-line stderr message survives piping to a file on Node 26.0.0; exit code 1 preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Merged, thanks for this, very timely! |
Problem
Under Node 26+, every socraticode tool that talks to Qdrant crashes on its first request with:
The MCP handshake succeeds, so the failure looks like a generic tool error and gives no hint about the Node version. Users (and MCP hosts like Claude Code) lose significant time debugging what looks like a config issue.
Root cause
@qdrant/js-client-restpinsundici: ^6.23.0and constructs anundici.Agentwhich it passes as thedispatcheroption to Node's built-infetch()(seepackages/js-client-rest/src/dispatcher.ts). Node 26 ships a newer undici internally. The two undicis disagree on the dispatcher hook contract — the newer one strictly validatesonErrorand rejects the shape produced by the older Agent.So the bug is upstream of socraticode (in qdrant-js, or in the version-mismatch ergonomics generally). I've filed qdrant/qdrant-js#134 with a minimal repro. Two open PRs there would likely fix it:
What this PR does
Two small changes to socraticode that don't depend on the upstream fix:
Runtime pre-flight check in
src/index.ts: parseprocess.versions.node, and if the major is ≥26, print a clear actionable error to stderr and exit 1. Per ESM semantics the imports below this block are evaluated before the check runs, but qdrant-js's module-init is side-effect-light — only an actual request hits the undici dispatcher path — so exiting at the first top-level statement is enough to spare users the opaqueUND_ERR_INVALID_ARGlater.engines.nodetightened inpackage.jsonfrom>=18.0.0to>=18.0.0 <26.0.0. This makesnpm install/npxemit a warning before the runtime check fires — covers the--ignore-enginesand asdf/nvm cases too.package-lock.jsongains a one-line mirror of the same field (regenerated bynpm install).Both should be reverted once one of qdrant/qdrant-js#123 or #128 lands and socraticode picks up a compatible client release. (The comment in
index.tscalls this out so the cleanup isn't forgotten.)Testing
Tested locally on macOS:
The bug itself is reproducible against Qdrant 1.17.0 with a 6-line script — see the issue at qdrant/qdrant-js#134 for the minimal repro.
Tradeoffs
engines.nodecan be overridden withnpm config set engine-strict falseornpm install --forcefor users who want to experiment, but the runtime check would still fire.Refs
Summary by CodeRabbit