Enable dual-stack IPv4/IPv6 listening for backend and Vite#401
Enable dual-stack IPv4/IPv6 listening for backend and Vite#401stephenfeather wants to merge 1 commit intositeboon:mainfrom
Conversation
📝 WalkthroughWalkthroughThe changes implement IPv6-first server binding with IPv4 fallback. The Node server now attempts to listen on IPv6 (::) initially, falling back to IPv4 (0.0.0.0) if IPv6 is unavailable. The Vite configuration defaults to IPv6, and the CLI host flag is removed as redundant. Changes
Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@server/index.js`:
- Around line 1939-1948: The startup log currently prints the raw bind address
(listenOptions.host / listenHostDisplay) which can be wildcards like '::' or
'0.0.0.0' and is not a usable URL; update the logging to detect wildcard
addresses and substitute a loopback equivalent before building the URL (e.g.,
map '::' -> '[::1]' and '0.0.0.0' -> '127.0.0.1') so the printed Server URL
(constructed with listenHostDisplay and PORT) is a usable URL; modify the code
that computes listenHostDisplay (or the final URL string) to perform this
mapping and keep existing bracketed IPv6 formatting for display.
- Around line 1893-1900: The IPv6 fallback condition in the handleError error
handler needs to include 'EADDRNOTAVAIL' so systems with IPv6 disabled fall back
to IPv4; update the conditional in handleError (the one checking error.code ===
'EAFNOSUPPORT' || error.code === 'EINVAL') to also check for error.code ===
'EADDRNOTAVAIL' before calling tryListen(fallbackOptions, null) and returning,
preserving the existing server.off('error', handleError) and reject/return
behavior.
| const handleError = (error) => { | ||
| server.off('error', handleError); | ||
|
|
||
| if (fallbackOptions && (error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL')) { | ||
| console.warn(`${c.warn('[WARN]')} IPv6 bind unavailable, falling back to IPv4`); | ||
| tryListen(fallbackOptions, null); | ||
| return; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, read the code at lines 1890-1920 in server/index.js
cd /repo && head -1920 server/index.js | tail -31 | cat -nRepository: siteboon/claudecodeui
Length of output: 120
🏁 Script executed:
# Also get context around the listenWithBestAvailableHost function
cd /repo && sed -n '1880,1920p' server/index.js | cat -nRepository: siteboon/claudecodeui
Length of output: 120
🏁 Script executed:
# Run the verification script to check the actual error code
node -e "
const http = require('http');
const s = http.createServer();
s.listen({ port: 0, host: '::', ipv6Only: false }, () => {
console.log('bind succeeded (IPv6 is enabled)');
s.close();
}).on('error', (e) => {
console.log('error.code:', e.code);
console.log('error.message:', e.message);
process.exit(0);
});
// Timeout in case bind succeeds (IPv6 is available)
setTimeout(() => {
console.log('IPv6 appears to be enabled on this system');
process.exit(0);
}, 2000);
"Repository: siteboon/claudecodeui
Length of output: 139
🏁 Script executed:
# Find server/index.js
find . -name "index.js" -path "*/server/*" 2>/dev/nullRepository: siteboon/claudecodeui
Length of output: 82
🏁 Script executed:
# List repository structure
git ls-files | grep -E "(server|index)" | head -20Repository: siteboon/claudecodeui
Length of output: 500
🏁 Script executed:
# Get file count to understand repo
git ls-files | head -1Repository: siteboon/claudecodeui
Length of output: 77
🏁 Script executed:
# Read lines 1890-1920 from server/index.js
sed -n '1890,1920p' ./server/index.js | cat -nRepository: siteboon/claudecodeui
Length of output: 1310
🏁 Script executed:
# Also check lines 1893-1900 mentioned in the review
sed -n '1893,1900p' ./server/index.js | cat -nRepository: siteboon/claudecodeui
Length of output: 518
🏁 Script executed:
# Search for any existing handling of EADDRNOTAVAIL in the codebase
rg "EADDRNOTAVAIL" --type jsRepository: siteboon/claudecodeui
Length of output: 47
🏁 Script executed:
# Check Node.js version and net module documentation references in code
grep -r "engine\|node" ./package.json | head -5Repository: siteboon/claudecodeui
Length of output: 244
🏁 Script executed:
# Look for any comments or discussions about IPv6 fallback
rg "IPv6\|ipv6\|EAFNOSUPPORT\|EINVAL" --type js --context 3Repository: siteboon/claudecodeui
Length of output: 47
🌐 Web query:
Node.js net.Server bind EADDRNOTAVAIL IPv6 disabled sysctl
💡 Result:
When you call server.listen() (or omit host), Node will prefer the IPv6 “any” address :: when IPv6 is available, and on many OSes that single IPv6 socket also accepts IPv4 via dual‑stack. Node also exposes an ipv6Only option (default false) to control that dual‑stack behavior. [1]
If IPv6 is disabled at the kernel / interface level (common culprits: boot param ipv6.disable=1, or sysctls like net.ipv6.conf.all.disable_ipv6=1 / net.ipv6.conf.<if>.disable_ipv6=1), then binding to :: can fail and surface as EADDRNOTAVAIL (or sometimes EAFNOSUPPORT depending on how IPv6 is disabled). This is a known pain point for Node apps expecting dual‑stack from ::. [2][3]
Practical fixes
- Force IPv4 bind in Node (works even if IPv6 is fully disabled):
server.listen({ port: 3000, host: '0.0.0.0' }); // or '127.0.0.1' for local-only
- If you want “bind
::but effectively no IPv6 traffic”: keep the IPv6 socket API available, but disable IPv6 on interfaces via sysctl (so apps can still bind::/ dual-stack, while IPv6 addressing is off). Example sysctls often used:net.ipv6.conf.all.disable_ipv6=1net.ipv6.conf.default.disable_ipv6=1
(Sometimes you may need an exception forlo.) [4]
- Fallback logic (robust across hosts where IPv6 may be disabled):
- try
:: - if error is
EAFNOSUPPORT/EADDRNOTAVAIL, retry0.0.0.0(this workaround is commonly recommended). [3]
- try
If you share the exact listen(...) call and the output of sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.default.disable_ipv6, I can tell you which path applies.
Sources: Node.js net.Server listen() behavior and ipv6Only option [1]; Linux disable_ipv6 sysctl semantics [2]; Node issue describing failures when IPv6 is disabled (ipv6.disable=1) [3]; dual-stack binding strategy with IPv6 disabled on interfaces [4].
🌐 Web query:
Node.js error codes EADDRNOTAVAIL bind IPv6 :: Docker container
💡 Result:
EADDRNOTAVAIL when you do server.listen(port, '::') in a Docker container usually means the container doesn’t have IPv6 available/configured, so binding to the IPv6 “any” address (::) fails.
Fix options
- Prefer IPv4 inside the container (most common)
server.listen(process.env.PORT ?? 3000, '0.0.0.0');In containers you generally bind to 0.0.0.0 (all IPv4 interfaces) and publish the port with Docker (-p hostPort:containerPort). This avoids depending on IPv6 being present. [1], [2]
- Let Node choose automatically
If you omit the host, Node will listen on::when IPv6 is available, otherwise it falls back to0.0.0.0. [1]
server.listen(process.env.PORT ?? 3000);- If you truly need IPv6, enable it in Docker
Docker IPv6 requires daemon/network configuration (and is supported on Linux Docker daemons). For example, enable IPv6 on the daemon or create an IPv6-enabled network / Composeenable_ipv6: true. [3]
Sources: Node.js server.listen() behavior and ipv6Only notes [1]; container binding guidance (0.0.0.0) [2]; Docker IPv6 enablement docs [3].
Add EADDRNOTAVAIL to the IPv6 fallback condition — server crashes on IPv6-disabled systems instead of falling back to IPv4.
When net.ipv6.conf.all.disable_ipv6=1 is set (common in Docker containers without IPv6 networking, many CI runners, Kubernetes pods), the IPv6 socket type is available but bind() on :: fails with EADDRNOTAVAIL — not EAFNOSUPPORT or EINVAL. The current fallback condition only checks those two codes, so the error is never caught. The handler calls reject(error), startServer() catches it, and the process exits instead of falling back to 0.0.0.0.
🐛 Proposed fix
- if (fallbackOptions && (error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL')) {
+ if (fallbackOptions && (error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL' || error.code === 'EADDRNOTAVAIL')) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/index.js` around lines 1893 - 1900, The IPv6 fallback condition in the
handleError error handler needs to include 'EADDRNOTAVAIL' so systems with IPv6
disabled fall back to IPv4; update the conditional in handleError (the one
checking error.code === 'EAFNOSUPPORT' || error.code === 'EINVAL') to also check
for error.code === 'EADDRNOTAVAIL' before calling tryListen(fallbackOptions,
null) and returning, preserving the existing server.off('error', handleError)
and reject/return behavior.
| const listenHostDisplay = listenOptions.host.includes(':') | ||
| ? `[${listenOptions.host}]` | ||
| : listenOptions.host; | ||
|
|
||
| console.log(''); | ||
| console.log(c.dim('═'.repeat(63))); | ||
| console.log(` ${c.bright('Claude Code UI Server - Ready')}`); | ||
| console.log(c.dim('═'.repeat(63))); | ||
| console.log(''); | ||
| console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + listenHostDisplay + ':' + PORT)}`); |
There was a problem hiding this comment.
Startup log shows the wildcard binding address, not a usable server URL.
When the server binds to ::, the log prints http://[::]:3001 — this is the wildcard any-address, not a loopback you can open. The PR's own validation used http://127.0.0.1:3001 and http://[::1]:3001, neither of which appears in the log. 0.0.0.0 in the IPv4 fallback case has the same problem.
💡 Suggested improvement — show loopback addresses instead
- const listenHostDisplay = listenOptions.host.includes(':')
- ? `[${listenOptions.host}]`
- : listenOptions.host;
-
- console.log('');
- console.log(c.dim('═'.repeat(63)));
- console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
- console.log(c.dim('═'.repeat(63)));
- console.log('');
- console.log(`${c.info('[INFO]')} Server URL: ${c.bright('http://' + listenHostDisplay + ':' + PORT)}`);
+ const isIPv6 = listenOptions.host.includes(':');
+
+ console.log('');
+ console.log(c.dim('═'.repeat(63)));
+ console.log(` ${c.bright('Claude Code UI Server - Ready')}`);
+ console.log(c.dim('═'.repeat(63)));
+ console.log('');
+ console.log(`${c.info('[INFO]')} Server (IPv4): ${c.bright('http://127.0.0.1:' + PORT)}`);
+ if (isIPv6) {
+ console.log(`${c.info('[INFO]')} Server (IPv6): ${c.bright('http://[::1]:' + PORT)}`);
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@server/index.js` around lines 1939 - 1948, The startup log currently prints
the raw bind address (listenOptions.host / listenHostDisplay) which can be
wildcards like '::' or '0.0.0.0' and is not a usable URL; update the logging to
detect wildcard addresses and substitute a loopback equivalent before building
the URL (e.g., map '::' -> '[::1]' and '0.0.0.0' -> '127.0.0.1') so the printed
Server URL (constructed with listenHostDisplay and PORT) is a usable URL; modify
the code that computes listenHostDisplay (or the final URL string) to perform
this mapping and keep existing bracketed IPv6 formatting for display.
|
@stephenfeather Thanks for this PR as well. Can you address the conflicts and also comments from CodeRabbit and Copilot here too? |
There was a problem hiding this comment.
Pull request overview
This PR updates local server binding defaults so both the backend (Node/Express HTTP server) and the Vite dev server are reachable via IPv4 and IPv6 by default, aligning with issue #399’s dual-stack expectation.
Changes:
- Backend: add a helper to prefer
host: '::'withipv6Only: false, with IPv4 fallback to0.0.0.0when IPv6 bind isn’t supported. - Frontend: set Vite dev server
server.hostdefault to::(overridable viaVITE_HOST). - Tooling: remove the
vite --hostscript override sovite.config.jshost configuration is respected.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
vite.config.js |
Sets dev server host to :: (or VITE_HOST) for IPv4+IPv6 reachability. |
server/index.js |
Replaces hardcoded IPv4 wildcard listening with dual-stack-first binding plus IPv4 fallback and updates startup URL logging accordingly. |
package.json |
Removes CLI --host override from the client script so Vite config controls host binding. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
Validation
Related issue: #399
Summary by CodeRabbit