fix(node): handle EADDRINUSE port conflict on serve#197
Conversation
Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughNodeServer.serve now returns Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsTimed out fetching pipeline failures after 30000ms 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: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/adapters/node.ts`:
- Around line 134-137: The onError handler currently clears
this.#listeningPromise before rejecting, letting ready() treat a failed startup
as successful; instead preserve the failure until the next serve() attempt by
not clearing this.#listeningPromise in onError and storing the Error in a
dedicated field (e.g., this.#startupError = error) before calling reject(error);
ensure the success path (onListening) clears both this.#listeningPromise and
this.#startupError, and reset/clear this.#startupError at the start of serve()
so a subsequent serve() attempt can overwrite it.
- Around line 127-152: The fire-and-forget call to serve() is causing unhandled
promise rejections when the server fails to listen; locate the place where
serve() is invoked without awaiting (the callsite that currently relies on
ready()/explicit serve() as the public failure surface) and attach rejection
handling to absorb errors (e.g., call this.serve().catch(() => {}) or otherwise
handle the rejected promise) so the process won't terminate, while keeping
ready() and explicit serve() semantics unchanged; the change touches the
callsite that invokes serve() (not the serve() implementation or
`#listeningPromise` internals).
In `@test/node-adapters.test.ts`:
- Line 3: The test currently imports getRandomPort() and relies on passing a
callback to server.listen(...) which only fires on successful bind and creates a
TOCTOU race; change the test to call server.listen(0) (let the OS pick an
ephemeral port) instead of getRandomPort(), remove usage of getRandomPort(), and
wait for the server's 'listening' and 'error' events (e.g., by awaiting a
Promise that resolves on 'listening' and rejects on 'error') so bind failures
like EADDRINUSE are reported via the 'error' event rather than an absent
callback; update any code referencing the chosen port to read
server.address().port after the 'listening' event.
🪄 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: 9081940d-0000-4432-a567-14861113b5b4
📒 Files selected for processing (5)
src/adapters/node.tstest/fixtures/node-port-conflict/src/server.tstest/fixtures/node-port-conflict/src/worker-helper.tstest/fixtures/node-port-conflict/src/worker.tstest/node-adapters.test.ts
Remove worker-based test infrastructure and replace with a direct in-process test. Also fix stray `r;` typo in node adapter.
Suppress unhandled rejection in fire-and-forget serve() call and preserve rejected promise so ready() properly surfaces the error.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/adapters/node.ts`:
- Around line 131-134: The current code casts this.node?.server to a server type
but doesn't guard for undefined, so calling serve() on a loader-created instance
will make server.once() throw a TypeError; update the serve() implementation
(the location where const server = this.node?.server as NodeHttp.Server |
NodeHttps.Server | NodeHttp2.Http2SecureServer and where server.once() is
called) to check if this.node?.server is undefined and throw a clear,
descriptive Error (or return early) when no server exists (e.g., "No server
available: loader mode without a bound server"), so callers get a meaningful
message instead of a runtime TypeError.
🪄 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: 579d3c69-8851-486f-b1a1-3a3ce2e5d5f4
📒 Files selected for processing (2)
src/adapters/node.tstest/node-adapters.test.ts
✅ Files skipped from review due to trivial changes (1)
- test/node-adapters.test.ts
There was a problem hiding this comment.
♻️ Duplicate comments (1)
test/node-adapters.test.ts (1)
3-3:⚠️ Potential issue | 🟡 MinorPrefer
listen(0)+'error'/'listening'wiring for blocker setup.Line 265/Line 284 introduces a TOCTOU race (
getRandomPort→ later bind), and Line 268/Line 287 has no reject path for blocker bind errors. This can make the test flaky and can still leak uncaught startup errors during setup.Proposed patch
-import { getRandomPort } from "get-port-please"; @@ - const port = await getRandomPort("localhost"); const blocker = createServer((_req, res) => res.end("blocked")); - - await new Promise<void>((resolve) => blocker.listen(port, "127.0.0.1", () => resolve())); + await new Promise<void>((resolve, reject) => { + const onError = (error: Error) => { + blocker.off("listening", onListening); + reject(error); + }; + const onListening = () => { + blocker.off("error", onError); + resolve(); + }; + blocker.once("error", onError); + blocker.once("listening", onListening); + blocker.listen(0, "127.0.0.1"); + }); + const { port } = blocker.address() as import("node:net").AddressInfo; @@ - const port = await getRandomPort("localhost"); const blocker = createServer((_req, res) => res.end("blocked")); - - await new Promise<void>((resolve) => blocker.listen(port, "127.0.0.1", () => resolve())); + await new Promise<void>((resolve, reject) => { + const onError = (error: Error) => { + blocker.off("listening", onListening); + reject(error); + }; + const onListening = () => { + blocker.off("error", onError); + resolve(); + }; + blocker.once("error", onError); + blocker.once("listening", onListening); + blocker.listen(0, "127.0.0.1"); + }); + const { port } = blocker.address() as import("node:net").AddressInfo;#!/bin/bash # Verify race-prone preselection + callback-only blocker listen pattern rg -n -C2 'getRandomPort\(|blocker\.listen\([^)]*\(\)\s*=>\s*resolve\(\)\)' test/node-adapters.test.tsAlso applies to: 265-269, 284-288
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/node-adapters.test.ts` at line 3, Tests currently preselect ports with getRandomPort and call blocker.listen(callback), which creates a TOCTOU race and lacks an error path; change the setup to bind with listen(0) (letting the OS pick a free port) and wire both 'listening' and 'error' events on blocker (use blocker.once('listening', () => resolve(...)) and blocker.once('error', err => reject(err))) so startup errors reject the Promise and avoid races; update any teardown to close the blocker on both success and error. Ensure you remove references to getRandomPort and replace blocker.listen(callback) usage with event-based wiring for reliable startup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@test/node-adapters.test.ts`:
- Line 3: Tests currently preselect ports with getRandomPort and call
blocker.listen(callback), which creates a TOCTOU race and lacks an error path;
change the setup to bind with listen(0) (letting the OS pick a free port) and
wire both 'listening' and 'error' events on blocker (use
blocker.once('listening', () => resolve(...)) and blocker.once('error', err =>
reject(err))) so startup errors reject the Promise and avoid races; update any
teardown to close the blocker on both success and error. Ensure you remove
references to getRandomPort and replace blocker.listen(callback) usage with
event-based wiring for reliable startup.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5e84f3c1-6f04-4ed2-b432-ba0bce0bf623
📒 Files selected for processing (2)
src/adapters/node.tstest/node-adapters.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- src/adapters/node.ts
- Guard for undefined server in serve() - Preserve startup error in ready() via #listenError field - Allow serve() retry after failure by clearing #listeningPromise - Replace getRandomPort with listen(0) to eliminate TOCTOU race - Use proper listening/error events instead of listen callback
commit: |
Restore unrelated blank-line removals and add server.close() after failed serve in tests.
Close #196
Similar setup of the test from h3js/crossws#183.
Summary by CodeRabbit
Improvements
Tests