Skip to content

fix(web): check and bind free ports on the same host - #527

Merged
Schleuse merged 1 commit into
mainfrom
fix/server-host
Sep 29, 2026
Merged

Schleuse merged 1 commit into
mainfrom
fix/server-host

Conversation

@Schleuse

@Schleuse Schleuse commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Alternative to #523. The port check and the bind now use the same address, and that address is set by a new web.server.host option.

 start()
+  host = config.host || undefined
-  findPorts(port, sync)                 # getPort({ host: '127.0.0.1' })
+  findPorts(port, sync, host)           # getPort({ host }) or, unset, every local address
-  express.listen(port)                  # wildcard
+  express.listen(port, host)
-  browserSync({ port })                 # wildcard
+  browserSync({ port, listen: host })   # only when host is set
-  urls: http://localhost:<port>
+  urls: http://<host or localhost>:<port>
  • host unset (default): get-port checks every local address and the server binds the wildcard address. Default behaviour matches fix(web): check free ports on all addresses, not only 127.0.0.1 #523, which fixes the EADDRINUSE when styleguides start in parallel.
  • host set, e.g. '127.0.0.1': the port check and both binds use that address only. The dev server isn't reachable from the LAN.

Explicitly configured ports (web.server.port) are still not checked. If one is taken, listen() fails with EADDRINUSE. Checking would make get-port fall back to a random port without any error.

Evidence

  • Before: a new test run against main's server.js. The configured host is ignored and the server binds the wildcard address:

    × binds the configured host, the same address the free port was checked on
      Expected: "127.0.0.1"
      Received: "::"
    

    After: Tests 4 passed (4) in packages/web/test/server.spec.js.

  • Manual check with BrowserSync (sync: true), using ss -ltn:

    host: '127.0.0.1'  →  127.0.0.1:3000 (sync), 127.0.0.1:3001 (server), no external URL
    host unset         →  *:3002 (sync), *:3003 (server), external http://192.168.1.59:3002
    
  • Tested on Windows by starting two styleguides in parallel. Both start on their own ports, with no EADDRINUSE.

Merge Danger

Door: two-way

The new option is additive and defaults to null. Reverting it only removes the option.

Blast Radius: dev-server

Only fractal start / fractal-serve goes through findPorts, so the build path is unaffected. With host unset, the port check is stricter than before: a port has to be free on every address, not only on 127.0.0.1. The first port picked can therefore move up a slot if another process holds it on a different interface. Anyone who sets syncOptions.listen directly still overrides the configured host for BrowserSync.

🤖 Generated with Claude Code

findPorts() checked candidate ports on 127.0.0.1 while the express
server and BrowserSync bound the wildcard address, so a port reported
as free could fail with EADDRINUSE on listen() (seen on Windows when
starting a second styleguide).

Add a `web.server.host` option and use it for the port check, the
express listen(), BrowserSync's `listen` and the generated URLs. When
unset, getPort checks every local address and the server binds the
wildcard address as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Schleuse
Schleuse merged commit c9a1d20 into main Sep 29, 2026
6 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.

1 participant