Skip to content

fix(node): handle EADDRINUSE port conflict on serve#197

Merged
pi0 merged 7 commits intoh3js:mainfrom
nekomeowww:neko/dev/port-conflict-fix
Apr 1, 2026
Merged

fix(node): handle EADDRINUSE port conflict on serve#197
pi0 merged 7 commits intoh3js:mainfrom
nekomeowww:neko/dev/port-conflict-fix

Conversation

@nekomeowww
Copy link
Copy Markdown
Contributor

@nekomeowww nekomeowww commented Apr 1, 2026

Close #196

Similar setup of the test from h3js/crossws#183.

Summary by CodeRabbit

  • Improvements

    • Better handling and reporting of server startup failures when a port is unavailable, including immediate error surface for subsequent readiness checks.
    • More explicit startup return behavior to improve type clarity.
  • Tests

    • Added tests that simulate port-in-use scenarios for both manual and automatic startup paths.

Co-authored-by: Codex <267193182+codex@users.noreply.github.com>
@nekomeowww nekomeowww requested a review from pi0 as a code owner April 1, 2026 19:09
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

NodeServer.serve now returns Promise<this> and its startup flow was rewritten to attach one-time "listening" and "error" handlers to the actual Node server, record listen errors in #listenError, clear #listeningPromise on failure, and return this after successful listen. Auto-start now calls this.serve().catch(() => {}); ready() immediately rejects if #listenError is set.

Changes

Cohort / File(s) Summary
Server Adapter Error Handling
src/adapters/node.ts
serve() signature changed to Promise<this>. Startup logic now verifies this.node?.server, attaches one-time listening and error handlers, captures errors into #listenError, clears #listeningPromise on error, removes paired handlers on outcome, and returns this after listening. Constructor auto-start invokes this.serve().catch(() => {}). ready() immediately rejects when #listenError exists.
Startup Error Tests
test/node-adapters.test.ts
Added tests under describe("node server startup") using a temporary blocker HTTP server (bound to 127.0.0.1 port 0) to provoke EADDRINUSE. Tests assert serve() (manual) rejects with { code: "EADDRINUSE" } and that auto-start (ready()) likewise rejects; blocker server is closed in finally.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 I hopped to bind a port at dawn,
Found it taken, but I stayed calm.
I listen once, stash every error's song,
Return myself when start is strong.
No more crashing — just a tidy calm. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: handling EADDRINUSE port conflicts in the serve method.
Linked Issues check ✅ Passed The PR implements the required fix from issue #196: EADDRINUSE errors are now handled via the server's error event and propagated as Promise rejections instead of uncaught exceptions.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the EADDRINUSE handling issue: updated serve() return type, added error event listeners, implemented error capture, and added corresponding test cases.

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

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

Warning

Review ran into problems

🔥 Problems

Timed 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.

❤️ 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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between e19649a and 602ad7e.

📒 Files selected for processing (5)
  • src/adapters/node.ts
  • test/fixtures/node-port-conflict/src/server.ts
  • test/fixtures/node-port-conflict/src/worker-helper.ts
  • test/fixtures/node-port-conflict/src/worker.ts
  • test/node-adapters.test.ts

Comment thread src/adapters/node.ts
Comment thread src/adapters/node.ts
Comment thread test/node-adapters.test.ts Outdated
pi0 added 2 commits April 1, 2026 21:20
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.
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 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

📥 Commits

Reviewing files that changed from the base of the PR and between 602ad7e and d8b87d4.

📒 Files selected for processing (2)
  • src/adapters/node.ts
  • test/node-adapters.test.ts
✅ Files skipped from review due to trivial changes (1)
  • test/node-adapters.test.ts

Comment thread src/adapters/node.ts Outdated
@pi0 pi0 changed the title fix(adapters): port conflict EADDRINUSE not handled fix(node): handle EADDRINUSE port conflict on serve Apr 1, 2026
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.

♻️ Duplicate comments (1)
test/node-adapters.test.ts (1)

3-3: ⚠️ Potential issue | 🟡 Minor

Prefer 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.ts

Also 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

📥 Commits

Reviewing files that changed from the base of the PR and between d8b87d4 and c85bcdf.

📒 Files selected for processing (2)
  • src/adapters/node.ts
  • test/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
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 1, 2026

Open in StackBlitz

npm i https://pkg.pr.new/srvx@197

commit: a7ced58

pi0 added 2 commits April 1, 2026 21:33
Restore unrelated blank-line removals and add server.close()
after failed serve in tests.
Copy link
Copy Markdown
Member

@pi0 pi0 left a comment

Choose a reason for hiding this comment

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

Thnx!

@pi0 pi0 merged commit 0e4f29b into h3js:main Apr 1, 2026
10 checks passed
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.

bug: if EADDRINUSE occurs, uncaughtException raises, causing entire Node.js process exited

2 participants