Skip to content

fix(daemon-server): await express listen() so RPC port is bound before startup completes#52

Merged
Rinse12 merged 3 commits into
masterfrom
fix/daemon-server-listen-await
May 24, 2026
Merged

fix(daemon-server): await express listen() so RPC port is bound before startup completes#52
Rinse12 merged 3 commits into
masterfrom
fix/daemon-server-listen-await

Conversation

@Rinse12
Copy link
Copy Markdown
Member

@Rinse12 Rinse12 commented May 24, 2026

Summary

  • Root-cause fix for the intermittent ECONNREFUSED 127.0.0.1:39138 failure in test/cli/daemon.test.ts > bitsocial daemon webui > 5chan webui does not contain the hash redirect script (most recent occurrence: run 26333018289).
  • src/webui/daemon-server.ts was calling webuiExpressApp.listen(port) fire-and-forget — no await on 'listening', no 'error' handler. startDaemonServer could return (and the daemon could log Communities in data path, which the test helper treats as readiness) before the OS had finished binding the RPC port. Worse, a bind failure surfaced asynchronously as an unhandled 'error' event → uncaughtException → daemon crashed after it had already advertised itself as ready.
  • Wrap listen() in a promise that resolves on 'listening' and rejects on 'error'. Now startDaemonServer only returns once the port is bound, and bind failures propagate through the normal rejection path instead of killing the process.
  • Adds test/webui/daemon-server.test.ts with a deterministic reproducer that pre-binds the port and asserts startDaemonServer rejects with EADDRINUSE. Fails on the old code (resolves successfully with broken server), passes after the fix in ~12ms.

Test plan

  • New reproducer (test/webui/daemon-server.test.ts) fails on master, passes on this branch.
  • Full test/cli/daemon.test.ts suite (22 tests, includes the flaky webui suite) passes locally.
  • CI green on ubuntu/macOS/Windows.

Fixes #42.

Summary by CodeRabbit

  • Bug Fixes

    • Daemon server startup now waits for the HTTP server to finish binding so port conflicts are detected and reported immediately instead of causing uncaught exceptions.
  • Tests

    • Added a regression test ensuring startup rejects when required ports are in use and that no uncaught exceptions are emitted.

Review Change Stack

…artup completes

`webuiExpressApp.listen(port)` was fire-and-forget — no await on 'listening',
no 'error' handler. Under load `startDaemonServer` could return (and the
daemon could log "Communities in data path") before the OS had finished
binding the RPC port, and a bind failure showed up later as an
uncaughtException that crashed the daemon *after* it had already advertised
itself as ready. The webui suite's first fetch sometimes hit the open window
and failed with ECONNREFUSED.

Wrap listen() in a promise that resolves on 'listening' and rejects on
'error'. Adds a unit test that pre-binds the port and asserts the function
rejects with EADDRINUSE.

Fixes issue #42.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 24, 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: 8260a078-4a01-49e0-8015-d080a4b8c94e

📥 Commits

Reviewing files that changed from the base of the PR and between 0d90f0f and 333ece2.

📒 Files selected for processing (1)
  • test/webui/daemon-server.test.ts

📝 Walkthrough

Walkthrough

This PR makes daemon startup await the HTTP server bind (listening event) so failures reject during startup; it adds a Vitest regression test that verifies startDaemonServer rejects when the RPC/WebSocket port is already bound.

Changes

HTTP Server Startup Race Fix

Layer / File(s) Summary
HTTP server bind contract
src/webui/daemon-server.ts
The httpServer creation is wrapped in a Promise that resolves on the listening event and rejects on the error event, ensuring startup only completes after bind success or failure.
Bind failure regression test
test/webui/daemon-server.test.ts
Adds a test that blocks the target port with a temporary TCP server and asserts startDaemonServer rejects with an EADDRINUSE / "address already in use" error, and that no uncaughtException was emitted.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I sat and watched the socket gate,
Waiting for the bind to state.
If someone else had staked a claim,
We fail aloud and call its name.
No stray exceptions left to roam. 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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: awaiting express listen() completion to ensure the RPC port is bound before daemon startup completes.
Linked Issues check ✅ Passed The PR fully addresses issue #42's Flake 1 objective by awaiting server.listen() completion and adding a regression test that validates EADDRINUSE rejection when the port is pre-bound.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing issue #42 Flake 1: the daemon-server.ts fix ensures port binding completes before returning, and the test validates this behavior; no unrelated modifications present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/daemon-server-listen-await

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

test/webui/daemon-server.test.ts

Oops! Something went wrong! :(

ESLint: 8.27.0

Error: ESLint configuration in --config » eslint-config-oclif is invalid:

  • Unexpected top-level property "__esModule".

Referenced from: /.eslintrc
at ConfigValidator.validateConfigSchema (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2156:19)
at ConfigArrayFactory._normalizeConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2998:19)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2963:21)
at ConfigArrayFactory._loadExtendedShareableConfig (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3264:21)
at ConfigArrayFactory._loadExtends (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3135:25)
at ConfigArrayFactory._normalizeObjectConfigDataBody (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3074:25)
at _normalizeObjectConfigDataBody.next ()
at ConfigArrayFactory._normalizeObjectConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:20)
at _normalizeObjectConfigData.next ()
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2829:16)


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.

Rinse12 added 2 commits May 24, 2026 05:43
…acOS/Windows

Previous version bound the blocker to 127.0.0.1, but express.listen(port)
binds to 0.0.0.0 (no host arg). On BSD-derived stacks (macOS, Windows) a
127.0.0.1 bind and a 0.0.0.0 bind on the same port can coexist, so the
daemon's wildcard listen succeeded and the test asserted rejection but got
a resolved promise. Linux is stricter and treats this as a conflict, which
is why ubuntu passed but the other runners failed.
…atch the daemon

Previous attempt bound the blocker to 0.0.0.0, but Node's default when no
host is passed to server.listen() is the IPv6 wildcard `::` with
IPV6_V6ONLY=true. On macOS and Windows, `::` (IPv6-only) and `0.0.0.0`
(IPv4) do not conflict, so the daemon's bind succeeded and the assertion
failed. Drop the explicit host on the blocker too so it lands on the same
default as express.listen(port) — `::` on dual-stack runners.
@Rinse12 Rinse12 merged commit ce572d7 into master May 24, 2026
4 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.

Test flakiness: daemon webui port-bind race and community-create near-timeout on Windows

1 participant