Skip to content

fix(dev): use runtime-agnostic websocket adapter for dev runner#4376

Open
productdevbook wants to merge 1 commit into
nitrojs:mainfrom
productdevbook:fix/dev-websocket-bun
Open

fix(dev): use runtime-agnostic websocket adapter for dev runner#4376
productdevbook wants to merge 1 commit into
nitrojs:mainfrom
productdevbook:fix/dev-websocket-bun

Conversation

@productdevbook

Copy link
Copy Markdown
Contributor

Problem

Closes #3939

The dev runtime entry (src/presets/_nitro/runtime/nitro-dev.ts) hardcoded crossws/adapters/node for WebSocket support. When the dev worker runs under Bun (e.g. bun --bun vite dev with preset: "bun"), the Node.js adapter throws:

[crossws] Using Node.js adapter in an incompatible environment.

Fix

Instead of selecting the adapter manually, expose the WebSocket hooks via the websocket AppEntry field. env-runner already attaches the runtime-appropriate crossws adapter per runner (node-worker, bun-process, deno-process) through crossws/server, so the correct adapter is chosen automatically:

websocket: import.meta._websocket
  ? ({ resolve: resolveWebsocketHooks } as AppEntry["websocket"])
  : undefined,

Tests

  • Added a WebSocket regression test to the nitro-dev preset suite (echo handler in test/fixture, real WebSocket connection in dev mode), enabled features.websocket on the fixture.
  • Verified manually with the bun-process dev runner (Bun 1.4.0): fails before the fix (reproduces Websocket does not work in vite dev mode with bun runtime #3939), passes after.
  • nitro-dev / bun / node suites pass on both rollup and rolldown builders; typecheck, lint, fmt clean.

🤖 Generated with Claude Code

The dev runtime entry hardcoded `crossws/adapters/node`, which throws
`[crossws] Using Node.js adapter in an incompatible environment` when the
dev worker runs under Bun (e.g. `bun --bun vite dev`).

Expose the websocket hooks via the `websocket` AppEntry field so env-runner
attaches the correct crossws adapter per runtime (node/bun/deno) through
`crossws/server`, instead of selecting the Node.js adapter unconditionally.

Closes nitrojs#3939

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@productdevbook productdevbook requested a review from pi0 as a code owner June 23, 2026 15:29
@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

@productdevbook is attempting to deploy a commit to the Nitro Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The Nitro dev entry (nitro-dev.ts) removes the hardcoded Node.js crossws upgrade handler and replaces it with a websocket property delegating to resolveWebsocketHooks, allowing the dev runner to attach the runtime-appropriate crossws adapter. A WebSocket fixture route and integration test are added to verify the new behavior.

Changes

Nitro Dev WebSocket Refactor and Tests

Layer / File(s) Summary
Dev entry: replace Node upgrade handler with websocket hooks
src/presets/_nitro/runtime/nitro-dev.ts
Removes the Node crossws adapter ws variable and upgrade handler from the default export. Adds a websocket property set to { resolve: resolveWebsocketHooks } (cast to AppEntry["websocket"]) when import.meta._websocket is enabled, with comments directing the dev runner to attach the runtime-appropriate adapter via crossws/server.
Test fixture route and nitro-dev integration test
test/fixture/nitro.config.ts, test/fixture/server/routes/ws.ts, test/presets/nitro-dev.test.ts
Enables features: { websocket: true } in the fixture config. Adds a defineWebSocketHandler route that sends "connected" on open and replies "pong" or "echo:<text>" on message. Adds a websocket test that connects via WebSocket, sends "ping", and asserts the received sequence is ["connected", "pong"].

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

  • #3939 (Websocket does not work in vite dev mode with bun runtime): This PR directly addresses the root cause by removing the hardcoded Node.js crossws adapter (nodeAdapter) that was throwing [crossws] Using Node.js adapter in an incompatible environment in non-Node runtimes like Bun, replacing it with a runtime-agnostic resolveWebsocketHooks mechanism.
  • Websockets feature does not work with vite running in through bun #4146: The same hardcoded Node adapter removal resolves this issue's reported failure for non-Node runtimes by allowing the appropriate crossws adapter to be resolved at runtime.

Possibly related PRs

  • nitrojs/nitro#3836: Touches the same dev entry upgrade handler path, refactoring the underlying upgrade(req, socket, head) delegation to getEnvRunner/NodeEnvRunner, directly related to the plumbing this PR replaces.
  • nitrojs/nitro#3964: Replaces WebSocket feature gating with import.meta._websocket and crossws hook/adapter setup in src/presets/_nitro/runtime/nitro-dev.ts, the same file and mechanism this PR builds on.
  • nitrojs/nitro#4317: Also wires import.meta._websocket into resolveWebsocketHooks via crossws adapters in a different preset, following the same pattern introduced here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows conventional commits format with 'fix' prefix and clearly describes the runtime-agnostic WebSocket adapter change for the dev runner.
Description check ✅ Passed The description comprehensively explains the problem, solution, and testing approach, directly addressing the WebSocket issue in dev mode with non-Node runtimes.
Linked Issues check ✅ Passed The PR fully addresses issue #3939 by replacing the hardcoded Node.js WebSocket adapter with a runtime-agnostic approach that delegates to env-runner for selecting the appropriate adapter.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing WebSocket support for runtime-agnostic dev mode: runtime entry modification, fixture configuration, WebSocket handler, and corresponding tests.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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/presets/_nitro/runtime/nitro-dev.ts`:
- Around line 24-25: The multi-line comments on lines 24-25 in the nitro-dev.ts
file that explain the crossws adapter behavior are restating what the code does
inline, which violates the repo's style guidelines for src/**/*.{ts,js} files
that specify not to add comments explaining what lines do unless explicitly
prompted. Remove these comment lines entirely to align with the coding
standards.

In `@test/presets/nitro-dev.test.ts`:
- Around line 30-42: The websocket is not being closed in the failure paths when
a timeout or error occurs. In the setTimeout callback that rejects with
"websocket timeout" and in the ws.addEventListener("error") handler, ensure you
call ws.close() before calling reject. This guarantees that the socket is
properly cleaned up in both error and timeout scenarios, preventing dangling
connections and test flakiness.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c857a932-4f46-495d-b375-6351cf973c41

📥 Commits

Reviewing files that changed from the base of the PR and between de9708f and 10b6edd.

📒 Files selected for processing (4)
  • src/presets/_nitro/runtime/nitro-dev.ts
  • test/fixture/nitro.config.ts
  • test/fixture/server/routes/ws.ts
  • test/presets/nitro-dev.test.ts

Comment on lines +24 to +25
// Let the dev runner attach the runtime-appropriate crossws adapter
// (node/bun/deno) via `crossws/server` instead of hardcoding the Node.js one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Remove line-explaining comments in runtime entry.

These comments restate behavior inline and should be removed to align with repo style for src/**/*.{ts,js}.

As per coding guidelines, src/**/*.{ts,js} says: "Do not add comments explaining what the line does unless prompted."

🤖 Prompt for 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.

In `@src/presets/_nitro/runtime/nitro-dev.ts` around lines 24 - 25, The multi-line
comments on lines 24-25 in the nitro-dev.ts file that explain the crossws
adapter behavior are restating what the code does inline, which violates the
repo's style guidelines for src/**/*.{ts,js} files that specify not to add
comments explaining what lines do unless explicitly prompted. Remove these
comment lines entirely to align with the coding standards.

Source: Coding guidelines

Comment on lines +30 to +42
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("websocket timeout")), 5000);
ws.addEventListener("error", (error) => reject(error as any));
ws.addEventListener("message", (event) => {
messages.push(String(event.data));
if (messages.length === 2) {
clearTimeout(timer);
ws.close();
resolve();
}
});
ws.addEventListener("open", () => ws.send("ping"));
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Close socket on timeout/error to avoid dangling test resources.

Failure paths currently reject without guaranteed socket cleanup, which can leave open handles and make the suite flaky.

Suggested fix
           await new Promise<void>((resolve, reject) => {
-            const timer = setTimeout(() => reject(new Error("websocket timeout")), 5000);
-            ws.addEventListener("error", (error) => reject(error as any));
+            const fail = (error: unknown) => {
+              clearTimeout(timer);
+              ws.close();
+              reject(error);
+            };
+            const timer = setTimeout(() => fail(new Error("websocket timeout")), 5000);
+            ws.addEventListener("error", (error) => fail(error as any), { once: true });
             ws.addEventListener("message", (event) => {
               messages.push(String(event.data));
               if (messages.length === 2) {
                 clearTimeout(timer);
                 ws.close();
                 resolve();
               }
             });
             ws.addEventListener("open", () => ws.send("ping"));
           });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error("websocket timeout")), 5000);
ws.addEventListener("error", (error) => reject(error as any));
ws.addEventListener("message", (event) => {
messages.push(String(event.data));
if (messages.length === 2) {
clearTimeout(timer);
ws.close();
resolve();
}
});
ws.addEventListener("open", () => ws.send("ping"));
});
await new Promise<void>((resolve, reject) => {
const fail = (error: unknown) => {
clearTimeout(timer);
ws.close();
reject(error);
};
const timer = setTimeout(() => fail(new Error("websocket timeout")), 5000);
ws.addEventListener("error", (error) => fail(error as any), { once: true });
ws.addEventListener("message", (event) => {
messages.push(String(event.data));
if (messages.length === 2) {
clearTimeout(timer);
ws.close();
resolve();
}
});
ws.addEventListener("open", () => ws.send("ping"));
});
🤖 Prompt for 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.

In `@test/presets/nitro-dev.test.ts` around lines 30 - 42, The websocket is not
being closed in the failure paths when a timeout or error occurs. In the
setTimeout callback that rejects with "websocket timeout" and in the
ws.addEventListener("error") handler, ensure you call ws.close() before calling
reject. This guarantees that the socket is properly cleaned up in both error and
timeout scenarios, preventing dangling connections and test flakiness.

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.

Websocket does not work in vite dev mode with bun runtime

1 participant