fix(webserver): refuse to boot when a fixed port is already bound - #799
Conversation
startWebServer binds two fixed ports: 8080 for the REST and WebSocket API, and
4001 for the API docs. main() wrapped the call in a bare catch that logged
"failed to start web server" and carried on, so a bind failure left the app
running with no API at all.
That failure is close to invisible. The WebUI binds an ephemeral port and is
unaffected, so the skin still loads and the app looks normal; only every REST
call and every WebSocket behind it is gone. BootTiming.mark('webserver_up')
fires either way. The user sees an app that opened correctly and then
misbehaves, with the reason only in the log.
Two Decaid-family apps installed on one device share those fixed ports, so the
common cause is that the other one is already running.
- serveOrReportPortInUse raises a typed WebServerPortInUse for EADDRINUSE and
rethrows every other failure unchanged, so a permission denial keeps its own
error path and is never reported to the user as a port clash.
- isAddressInUse matches the OS code per platform -- 98 on Linux and Android,
48 on macOS and iOS, 10048 on Windows -- because Dart reports the raw code
and does not normalise it.
- main() stops on WebServerPortInUse and shows a screen naming the port instead
of booting without an API.
The screen offers "Check again", which re-probes the port, and "Close this app".
It does not try to close the other app: Android does not let one app stop
another, and killBackgroundProcesses needs its own permission and only ever
touches background processes.
tadelv
left a comment
There was a problem hiding this comment.
There is one required fix before this should merge:
startWebServer() binds the real REST/WS server on 8080 first and only then calls startApiDocsServer() on 4001. This PR converts both binds to WebServerPortInUse, and main() treats either one as fatal. That means a conflict on 4001 only now aborts normal app startup even though the actual API on 8080 has already started successfully. It also leaves that successfully-created 8080 HttpServer alive while the conflict UI is shown, because the server reference is local to startWebServer() and never gets closed on the later 4001 exception.
That broadens the fix beyond the stated failure mode and can create a confusing self-inflicted port conflict. Please make only failure to bind the required 8080 REST/WS API fatal, or otherwise make startup transactional and explicitly clean up 8080 if 4001 is intentionally required too. Given that 4001 serves API docs, I would strongly prefer keeping a docs-port failure non-fatal and logging it. Please add a regression test for the 4001-conflict case so the core API/app remains usable (or, if both ports are deliberately mandatory, that 8080 is rolled back cleanly).
Aside from this, the typed EADDRINUSE handling, error propagation for non-address-in-use failures, focused tests, and rationale placement are consistent with the repository guidance.
startWebServer binds 8080 for the REST and WebSocket API, then 4001 for the API docs. Both binds reported WebServerPortInUse and main() treated either as fatal, so a conflict on 4001 alone aborted a boot whose API had already started, and left that 8080 HttpServer open behind the conflict screen: startWebServer holds the only reference to it. startApiDocsServer now catches the conflict for its own port, logs it, and returns null. The docs are unavailable and the app runs. Only a failure to bind 8080 still reaches main() and shows WebServerPortConflictApp. The function returns the HttpServer it created, or null, so the two cases are assertable. New test test/unit/services/webserver/api_docs_server_port_test.dart covers both; the first fails if the conflict is rethrown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in Your reading of the code was correct.
The function returns the Regression test:
Verification: |
tadelv
left a comment
There was a problem hiding this comment.
Re-checked the update. The requested blocker is resolved: port 4001/API-docs conflicts are now handled locally as non-fatal, while only failure to bind the required 8080 REST/WS API reaches the fatal startup path. The added regression coverage exercises both the occupied and free docs-port cases. The implementation and rationale placement remain consistent with AGENTS.md, and current CI passes format, analyze, full tests, and Linux smoke build. No further blocking issues found.
Summary
The app could boot with no API at all and say nothing.
startWebServerbinds two fixed ports: 8080 for the REST and WebSocket API, and 4001 for the APIdocs.
main()wrapped the call in a bare catch that logged "failed to start web server" andcarried on.
That failure is close to invisible. The WebUI binds an ephemeral port and is unaffected, so the
skin still loads and the app looks normal — only every REST call and every WebSocket behind it is
gone.
BootTiming.mark('webserver_up')fires either way. The user sees an app that openedcorrectly and then misbehaves, with the reason only in the log.
Two Decaid-family apps installed on one device share those fixed ports, so the common cause is
simply that the other one is already running.
Base:
main. Independent.Design notes
serveOrReportPortInUseraises a typedWebServerPortInUseforEADDRINUSEand rethrowsevery other failure unchanged, so a permission denial keeps its own error path and is never
reported to the user as a port clash.
isAddressInUsematches the OS code per platform — 98 on Linux and Android, 48 on macOS andiOS, 10048 on Windows — because Dart reports the raw code and does not normalise it.
main()stops onWebServerPortInUseand shows a screen naming the port, offering "Check again"and "Close this app".
Linked Issue
N/A
Verification
flutter analyze— clean.flutter test— full suite 3904 passed against currentmain, includingport_binding_testandwebserver_port_conflict_app_test.dart format— clean on every changed file.machine, and has been exercised in normal use rather than only under test.
Impact
without an API. That is a deliberate behaviour change.
fails.
Contributor Responsibility
AI-assisted development is allowed. The submitter remains responsible for the submitted work.