Repository navigation
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01E2b7v7smJX7nby2ZmsqXsp
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
Ran the same commands locally in the meantime, on
One caveat so it isn't mistaken for fallout from this PR: |
|
Automated PR Cleanup Thank you for contributing to opencode. Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions. This PR was closed because it matched the following cleanup criteria:
PRs created within the last month are not affected by this cleanup. If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate. Thanks again for taking the time to contribute. |
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