Skip to content

fix: resolve the port shorthand through localhost, not 127.0.0.1 - #31

Merged
alsey89 merged 1 commit into
mainfrom
fix/shorthand-tries-both-families
Aug 1, 2026
Merged

alsey89 merged 1 commit into
mainfrom
fix/shorthand-tries-both-families

Conversation

@alsey89

@alsey89 alsey89 commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes #28.

switchboard add app 3000 recorded 127.0.0.1:3000. That is one of two addresses meaning this machine, and a server listening on ::1 does not answer on it. Node dev servers commonly bind IPv6 loopback only, so the route was dead the moment it was added.

Measured on a real IPv6-only dev server:

127.0.0.1:3000       FAIL  connection refused
localhost:3000       OK    -> [::1]:3000
[::1]:3000           OK    -> [::1]:3000

The shorthand is now localhost:<port>. Go's dialer tries both families for a name and takes whichever answers (RFC 6555), which is what the browser was doing all along when the user checked localhost:3000, saw their app, and concluded Switchboard was broken.

Why the old failure was expensive

The app plainly worked. ls said down. That is the same word ls uses for a server that is not running at all, so the only diagnostic available pointed away from the cause.

Before and after, against the same live server:

- https://app.test → 127.0.0.1:3000
-   (nothing is listening on 127.0.0.1:3000 yet — the route goes live when your server starts)
+ https://app.test → localhost:3000

Scope

Nothing on disk changes. port = 3000 stays exactly as written. Only the address derived from it moves, so every existing route gets this without anyone editing a config.

ls and doctor come along for free, since both probe through UpstreamAddr.

--upstream is untouched. Naming an address is how you say you meant that address.

Only the outbound dial changes. Every socket Switchboard listens on stays pinned to 127.0.0.1. The security claim is unaffected.

Testing

All packages pass, GOOS=linux and GOOS=windows build clean. Two existing tests asserted the old address and were updated — the behaviour they pinned is the behaviour being fixed.

Worth flagging: go test had these cached as passing after the change. go clean -testcache was needed to see the failures.

No test binds a real IPv6 socket. CI runners cannot be assumed to have IPv6, and a test that silently skips there would give the least coverage exactly where a regression is likeliest to slip through. TestUpstreamAddr pins the returned string instead, which is what a regression would actually change.

Not included

The diagnostic for someone who writes --upstream 127.0.0.1:3000 explicitly against an IPv6-only server. They still get a bare down. Rarer now that the default path works, and worth its own issue if you want it.

Also no CHANGELOG entry, to avoid conflicting with #30, which is still open and rewrites that file. Happy to add one once it merges.

`switchboard add app 3000` recorded 127.0.0.1:3000. That is one of two
addresses meaning this machine, and a server listening on ::1 does not
answer on it. Node dev servers commonly bind IPv6 loopback only, so the
route was dead from the moment it was added.

The failure gave nothing to go on. The app plainly worked at
localhost:3000, because browsers try both families, while `ls` said
`down` — the same word it uses for a server that is not running, so the
one diagnostic pointed away from the cause.

The shorthand is now localhost:<port>. Go's dialer tries both families
for a name and takes whichever answers (RFC 6555), which is what the
browser was doing all along. Verified against a live IPv6-only server:
127.0.0.1:3000 refused, localhost:3000 connected to [::1]:3000.

Nothing on disk changes. `port = 3000` stays exactly as written; only the
address derived from it moves, so every existing route gets this without
anyone editing a config. `ls` and `doctor` come along for free, since
both probe through UpstreamAddr.

--upstream is untouched. Naming an address is how you say you meant that
address.

Only the outbound dial to the user's own dev server is affected. Every
socket Switchboard listens on stays pinned to 127.0.0.1.

No test binds a real IPv6 socket: CI runners cannot be assumed to have
IPv6, and a test that skips itself there would give the least coverage
exactly where regressions are likeliest. TestUpstreamAddr pins the
returned string instead, which is what a regression would change.

Closes #28
@alsey89
alsey89 merged commit 36b36e4 into main Aug 1, 2026
2 checks passed
@alsey89
alsey89 deleted the fix/shorthand-tries-both-families branch August 1, 2026 07:32
@alsey89 alsey89 mentioned this pull request Aug 1, 2026
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.

add <name> <port> assumes IPv4, so IPv6-only dev servers get a dead route

1 participant