Skip to content

fix: refuse Node 26+ until qdrant-js gains undici 7 compat#59

Merged
giancarloerra merged 3 commits into
giancarloerra:mainfrom
shaitourchin:fix/refuse-node-26-until-qdrant-undici-compat
May 12, 2026
Merged

fix: refuse Node 26+ until qdrant-js gains undici 7 compat#59
giancarloerra merged 3 commits into
giancarloerra:mainfrom
shaitourchin:fix/refuse-node-26-until-qdrant-undici-compat

Conversation

@shaitourchin
Copy link
Copy Markdown
Contributor

@shaitourchin shaitourchin commented May 12, 2026

Problem

Under Node 26+, every socraticode tool that talks to Qdrant crashes on its first request with:

UND_ERR_INVALID_ARG: invalid onError method

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-rest pins undici: ^6.23.0 and constructs an undici.Agent which it passes as the dispatcher option to Node's built-in fetch() (see packages/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 validates onError and 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:

  1. Runtime pre-flight check in src/index.ts: parse process.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 opaque UND_ERR_INVALID_ARG later.

  2. engines.node tightened in package.json from >=18.0.0 to >=18.0.0 <26.0.0. This makes npm install / npx emit a warning before the runtime check fires — covers the --ignore-engines and asdf/nvm cases too. package-lock.json gains a one-line mirror of the same field (regenerated by npm 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.ts calls this out so the cleanup isn't forgotten.)

Testing

Tested locally on macOS:

  • Node 26.0.0 + the change → exits with the new error message ✓
  • Node 22.x + the change → starts normally, MCP handshake + tool calls work ✓
  • Node 18.x + the change → starts normally (lower bound unchanged) ✓

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

  • Node 26 users are blocked from installing socraticode until upstream lands. Given the bug is a hard crash on first request and they can't use the package anyway, blocking install with a clear message is the better failure mode.
  • engines.node can be overridden with npm config set engine-strict false or npm install --force for users who want to experiment, but the runtime check would still fire.

Refs

Summary by CodeRabbit

  • Chores
    • Constrained supported Node.js versions to >=18.0.0 and <26.0.0.
    • Added a startup compatibility check that halts the application on unsupported Node.js releases and displays an explanatory error message.

Review Change Stack

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>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 12, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1416ec35-0e5d-48c2-a1fa-bb312086b32b

📥 Commits

Reviewing files that changed from the base of the PR and between 5cd9db0 and 69a6b74.

📒 Files selected for processing (1)
  • src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/index.ts

📝 Walkthrough

Walkthrough

Adds a Node.js v26 compatibility guard: runtime pre-flight exits on Node >=26 with a stderr message and package.json engines.node is tightened to >=18.0.0 <26.0.0.

Changes

Node.js v26 Compatibility Guard

Layer / File(s) Summary
Node.js v26 compatibility guard
src/index.ts, package.json
Adds a startup check in src/index.ts that parses process.versions.node and exits with code 1 after writing an explanatory message to stderr when Node major >= 26; updates package.json engines.node to >=18.0.0 <26.0.0.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble bytes and check the score,
"Node twenty-six? No room for more."
A gentle hop, a warning spat,
"Use eighteen to twenty-five, and that's a fact." 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a Node 26+ rejection guard pending upstream qdrant-js undici compatibility fixes.
Description check ✅ Passed The description covers all required template sections: Problem, Root cause, Testing, and provides thorough context. A Testing checklist section is present but checkboxes are not marked.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c842ff6 and c23120e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • package.json
  • src/index.ts

Comment thread src/index.ts Outdated
shaitourchin and others added 2 commits May 12, 2026 11:06
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>
@giancarloerra giancarloerra self-assigned this May 12, 2026
@giancarloerra giancarloerra merged commit fc63d14 into giancarloerra:main May 12, 2026
5 checks passed
@giancarloerra
Copy link
Copy Markdown
Owner

Merged, thanks for this, very timely!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants