fix(serve): surface the real bind error instead of "Unexpected error / ServeError"#38742
Open
dzianisv wants to merge 1 commit into
Open
fix(serve): surface the real bind error instead of "Unexpected error / ServeError"#38742dzianisv wants to merge 1 commit into
dzianisv wants to merge 1 commit into
Conversation
…/ ServeError"
Every bind failure in `opencode serve` printed the same two useless lines:
$ opencode serve --hostname 100.68.120.26 --port 4096
Error: Unexpected error
ServeError
No port, no hostname, no errno. A busy port, an unavailable address, and a
privileged port were indistinguishable.
Two independent causes:
1. Effect's `HttpServerError.ServeError` is a `Data.TaggedError` whose `message`
is empty — the real failure sits on `.cause`. `errorMessage()` never read
`.cause`, so it fell through to `error.name` and printed "ServeError".
2. Under Bun, `node:http` `listen()` reports *every* failure as `EADDRINUSE`
("Failed to start server. Is port N in use?"). Node returns EADDRNOTAVAIL /
EACCES for the same binds. So errno dispatch alone cannot tell a busy port
from a hostname that is not on this machine.
Fixes:
- `errorMessage()` now falls back to `cause` before the bare name. This helps
every tagged error, not just this one.
- New `server/bind.ts`. `assertBindable()` checks a literal-IP hostname against
`os.networkInterfaces()` before binding, where an exact answer is available —
this is what works around (2). `bindError()` walks the cause chain and
translates the errno, treating EADDRINUSE below port 1024 as a privilege
problem since Bun folds EACCES into it.
- `serve` raises a `CliError` so the message prints on its own rather than
behind "Unexpected error".
Before / after:
--hostname 100.68.120.26 --port 4096 (interface down)
was: Unexpected error / ServeError
now: Cannot bind to 100.68.120.26: it is not an address on any local
network interface. Available: 127.0.0.1, ::1, 192.168.0.40, ...
Use 0.0.0.0 to listen on every interface, or bring the interface
up first (e.g. start Tailscale/VPN).
--hostname 127.0.0.1 --port 4096 (port busy)
was: Unexpected error / ServeError
now: Address 127.0.0.1:4096 is already in use. Choose a different --port,
use --port 0 to pick a free one, or stop the process holding it.
--hostname 127.0.0.1 --port 80
was: Unexpected error / ServeError
now: Cannot bind 127.0.0.1:80: ports below 1024 are privileged. Use a
port >= 1024, or run with elevated privileges.
Valid binds (0.0.0.0, 127.0.0.1, a real LAN address, --port 0) are unchanged.
Closes anomalyco#38738
Closes anomalyco#38739
Refs anomalyco#37718
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2b7v7smJX7nby2ZmsqXsp
Contributor
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
Author
|
Ran the same commands locally in the meantime, on
One caveat so it isn't mistaken for fallout from this PR: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue for this PR
Closes #38738
Closes #38739
Type of change
What does this PR do?
Every bind failure in
opencode serveprinted the same two useless lines:No port, no hostname, no errno. A busy port, an address that isn't on the machine, and a privileged port were indistinguishable.
There are two separate reasons for that.
The errno is discarded. Effect's
HttpServerError.ServeErroris aData.TaggedError, so itsmessageis""and the real failure sits on.cause.errorMessage()checkedmessagethen fell straight through toname— printing the literal stringServeError. Fixed by checkingcausein between. That helps every tagged error, not just this one.Bun reports the wrong errno anyway. Under Bun,
node:httplisten()returnsEADDRINUSEfor everything. Same script, two runtimes:Bun.serve()does the same, so it isn't thenode:httpshim specifically. This is why unwrapping the cause alone isn't enough — the errno you unwrap is wrong. So the newassertBindable()checks a literal-IP--hostnameagainstos.networkInterfaces()before callinglisten, where the answer is unambiguous. Hostnames are skipped, sincelistenresolves those.bindError()handles what's left: it walks the cause chain and maps the errno. Sub-1024 ports are reported as a privilege problem rather than a conflict, because Bun foldsEACCESintoEADDRINUSEand a privileged port is the far likelier explanation down there.Finally
serveraises aCliError, so the message prints on its own instead of behindUnexpected error.bind.tsthrows plainErrors and has no CLI dependency on purpose —Server.listenis also called by the Desktop/WSL sidecar and by tests.Worth noting the Bun errno behaviour is a Bun bug and should be reported there too. This PR doesn't depend on that being fixed.
How did you verify your code works?
Drove a real
serveprocess through every case, ondev@ f516651:--hostname 100.68.120.26 --port 4096(interface down)Unexpected error/ServeErrorCannot bind to 100.68.120.26: it is not an address on any local network interface. Available: 127.0.0.1, ::1, 192.168.0.40, … Use 0.0.0.0 to listen on every interface, or bring the interface up first (e.g. start Tailscale/VPN).--hostname 127.0.0.1 --port 4096(busy)Unexpected error/ServeErrorAddress 127.0.0.1:4096 is already in use. Choose a different --port, use --port 0 to pick a free one, or stop the process holding it.--hostname 127.0.0.1 --port 80Unexpected error/ServeErrorCannot bind 127.0.0.1:80: ports below 1024 are privileged. Use a port >= 1024, or run with elevated privileges.--hostname 192.168.0.40 --port N--hostname 0.0.0.0 --port N--hostname localhost --port 0Also reproduced the original bug on the released 1.17.9, not just on
dev.Tests:
bun test test/server/→ 306 pass, 2 skip, 0 failbun test test/cli/serve/→ 2 pass, 0 failtest/server/bind.test.ts, 13 cases — including a fixture mimickingServeError's empty-message shape, the sub-1024 case Bun mislabels, IPv6 zone ids, and a self-referential cause chainpackages/tui/test/util/error.test.tsfor cause unwrapping, and that an explicitmessagestill wins overcausetsc --noEmitclean,oxlint0 warnings / 0 errors on all changed filesScreenshots / recordings
Not a UI change. CLI output is in the table above.
Checklist