Skip to content

Bind the site's dev and preview servers to IPv4 - #276

Merged
fstubner merged 1 commit into
mainfrom
fix/preview-binds-ipv4
Aug 28, 2026
Merged

fstubner merged 1 commit into
mainfrom
fix/preview-binds-ipv4

Conversation

@fstubner

Copy link
Copy Markdown
Owner

npm run preview printed a URL that did not answer. The server was running — bound to the IPv6 loopback only:

TCP    [::1]:4321    [::]:0    LISTENING    44248

http://[::1]:4321/      -> 200
http://127.0.0.1:4321/  -> 000   (connection refused)

Node 17+ resolves a bare localhost IPv6-first and Astro passes that straight to listen(), so nothing was reachable over IPv4 — which is what a browser opening http://localhost:4321 can end up trying, and what every tool here using an explicit 127.0.0.1 was trying.

The repo already knew

Both other places that start these servers pass the flag:

  • .claude/launch.json → astro dev --host 127.0.0.1 --port 4322
  • scripts/a11y.mjs:163 → spawn(node, [astroCli, 'preview', '--host', host, ...]), host defaulting to 127.0.0.1

The two scripts a person runs by hand were the only ones without it. That is why the a11y gate has always worked while npm run preview looked broken.

After

bind: 127.0.0.1:4321
http://127.0.0.1:4321/  -> 200
http://localhost:4321/  -> 200

One thing that made this harder to see

The local wrapper around astro preview daemonises it and returns immediately, and its own astro preview status reported "No preview server is running" while the process was alive and listening on [::1]:4321. The bind address in netstat -ano is the thing to trust, not the wrapper's status — noted in the commit message so the next person does not chase it.

astro check 0 errors · CSS budget 24 · changelog check · file-size guard.

`npm run preview` printed a URL that did not answer. The server was running,
but bound to the IPv6 loopback only:

    TCP    [::1]:4321    [::]:0    LISTENING    44248

    http://[::1]:4321/      -> 200
    http://127.0.0.1:4321/  -> 000   (connection refused)

Node 17 and later resolve a bare `localhost` IPv6-first, and Astro passes
that straight to `listen()`, so nothing was reachable over IPv4 -- which is
what a browser opening `http://localhost:4321` can end up trying, and what
every tool here that uses an explicit `127.0.0.1` was trying.

Both npm scripts now pass `--host 127.0.0.1`, which is what the two other
places in this repo that start these servers already do:
`.claude/launch.json` passes it to `astro dev`, and `scripts/a11y.mjs:163`
passes it to `astro preview`. The scripts a person runs by hand were the only
ones without it.

After: bound to `127.0.0.1:4321`, with both `127.0.0.1` and `localhost`
returning 200.

Worth knowing when debugging this again: the local wrapper around `astro
preview` daemonises it and returns immediately, and its `astro preview
status` reported "No preview server is running" while the process was alive
and listening. The bind address in `netstat -ano` is the thing to trust.
@github-actions

Copy link
Copy Markdown
Contributor

Site preview: https://pr-276.netscli-site-preview.pages.dev

Built from 75b114f with NETSCLI_PREVIEW=1 — noindex, and analytics disabled so it does not report into netscli.com's numbers.

Production is unaffected: netscli.com is served from GitHub Pages via pages.yml, which is manual-only.

@fstubner
fstubner merged commit 7bbf812 into main Aug 28, 2026
14 checks passed
@fstubner
fstubner deleted the fix/preview-binds-ipv4 branch August 28, 2026 22:46
fstubner added a commit that referenced this pull request Aug 28, 2026
#276 made `npm run dev` and `npm run preview` pass `--host 127.0.0.1`. That
was wrong, and it broke the thing it was meant to fix.

Windows resolves `localhost` to the IPv6 loopback first:

    > ping -n 1 localhost
    Pinging Workstation [::1] with 32 bytes of data

    > node -e "dns.lookup('localhost',{all:true},...)"
    ::1 (IPv6), 127.0.0.1 (IPv4)

So binding IPv4-only means a browser asking for `localhost` gets
ERR_CONNECTION_REFUSED -- which is exactly what happened, in Edge, on the
server I had just "fixed". Astro's default passes `localhost` to `listen()`
and Node resolves it per-OS: `::1` on Windows, `127.0.0.1` on most Linux.
That adapts to whatever the browser on that machine will ask for, which is
the behaviour worth having.

The original symptom was mine, not the repo's: my checks curled `127.0.0.1`
while the server was on `::1`. The fix for that is to check `localhost` and
let it resolve, not to pin the server to one family. Scripts that genuinely
need a fixed address already pass their own -- scripts/a11y.mjs:163 and
.claude/launch.json both do, and both are unaffected by this revert.

Verified after reverting: a fresh `npm run preview` binds `[::1]:4321`, and
`http://localhost:4321/` returns 200. The a11y gate still passes, 0
violations in both themes.
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