Fix #11308: update server.port after binding to port 0#14372
Fix #11308: update server.port after binding to port 0#14372sfc-gh-lwilby merged 4 commits intostreamlit:developfrom
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
|
Thanks for contributing to Streamlit! 🎈 Please make sure you have read our Contributing Guide. You can find additional information about Streamlit development in the wiki. The review process:
We're receiving many contributions and have limited review bandwidth — please expect some delay. We appreciate your patience! 🙏 |
|
Nice fix — the approach is clean and the test covers the happy path well. The core logic is correct: read the actual bound port from the socket and update the config so downstream consumers (browser.serverPort, auth redirect URIs, CLI output) all reflect reality. One thing I notices is that this uses The getattr(http_server, "_sockets", None) approach works, but it's reaching into Tornado's internals. Tornado has a public API for this — you can split listen() into bind_sockets() + add_sockets(), which gives you direct access to the bound sockets: |
When Streamlit is started with server.port=0, the OS assigns an ephemeral port. However, the configured server.port value remained 0, which caused the CLI to display URLs such as http://localhost:0. This patch updates server.port with the actual bound port after the socket is created, so the displayed URLs reflect the real listening port. A unit test was added to verify that server.port is updated when binding to port 0.
d9fb8ea to
65e7fe0
Compare
Thanks that makes sense. I updated the implementation to use Tornado's public socket API via I also updated the existing tests in server_test.py to reflect this change:
I also reran the relevant tests:
For manual testing, I ran:
Before the fix, Streamlit displayed I also confirmed that the process was listening on the same port via |
|
The AI review job failed to complete. Please check the workflow run for details. You can retry by adding the 'ai-review' label again or manually triggering the workflow. |
There was a problem hiding this comment.
Summary
This PR fixes #11308 where Streamlit displays http://localhost:0 in the CLI when server.port=0 is used to request an OS-assigned ephemeral port. The fix replaces http_server.listen(port, address) with the two-step tornado.netutil.bind_sockets(port, address) + http_server.add_sockets(sockets) pattern, which allows inspecting the actual bound port and updating server.port in the config so that displayed URLs reflect the real listening port. Existing port rotation tests are refactored to mock bind_sockets instead of http_server.listen, and a new DynamicPortTest validates the port-0 behavior with a real socket.
Reviewer consensus: All three reviewers (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high) approved the PR unanimously. No critical or blocking issues were raised.
Code Quality
The code is clean and follows Streamlit's existing patterns. All three reviewers agreed that the bind_sockets() + add_sockets() pattern is the idiomatic Tornado approach for port discovery — Tornado's HTTPServer.listen() internally calls exactly these two functions, so the refactor is safe and functionally equivalent.
The test refactoring is a net improvement: the old PortRotateOneTest had a stale class-level which_port mock that was never exercised by the code under test. The new tests are more focused, correctly assert against bind_sockets call arguments, and verify that add_sockets is not called when binding fails.
One minor cleanup opportunity was identified by one reviewer: start_listening_unix_socket (around line 241 of server.py) still contains a local import tornado.netutil that is now redundant since the module-level import was added on line 24. This is harmless but could be cleaned up for consistency.
Test Coverage
All reviewers agreed that test coverage is adequate:
DynamicPortTest.test_updates_configured_port_when_binding_to_zero— Uses a realHTTPServerand socket to verify end-to-end thatserver.portis updated to a real ephemeral port > 0 after binding to port 0. Good integration-style test with proper cleanup.PortRotateAHundredTest— Correctly updated to assert againstmock_bind_sockets.call_countand thatadd_socketsis never called when all binds fail.PortRotateOneTest— Now verifies the actual port values passed to successivebind_socketscalls, confirming the increment logic.PortPermissionDeniedTest— Updated to mockbind_socketsand verify the retry-then-succeed flow.
No E2E tests are needed — this change affects internal server startup logic and CLI output only.
Backwards Compatibility
No breaking changes. All three reviewers agreed the change is fully backwards compatible. The only new behavior — updating server.port when port 0 is used — is additive and only activates when users explicitly opt into port 0. All existing port rotation, permission error retry, and manual port behaviors are preserved.
Security & Risk
No security concerns. All reviewers agreed the change is limited to socket binding mechanics and config updating. No new endpoints, routes, dependencies, auth/session handling, CORS, or external interactions are introduced. The config.set_option call uses ConfigOption.STREAMLIT_DEFINITION, correctly marking the value as set by Streamlit internals.
External test recommendation
- Recommend external_test: No
- Triggered categories: None
- Evidence:
lib/streamlit/web/server/server.py: Change is confined to thestart_listening_tcp_socketfunction's socket binding mechanism. No routes, handlers, headers, auth, embedding, or cross-origin behavior is affected.lib/tests/streamlit/web/server/server_test.py: Only test file changes — no impact on external behavior.
- Suggested external_test focus areas: N/A
- Confidence: High
- Assumptions and gaps: Assumes port 0 usage is a local development scenario, not a production/external deployment pattern.
Reviewer note: Two of three reviewers (claude, gemini) recommended against external tests. One reviewer (gpt) recommended external tests with medium confidence, citing URL behavior changes. After evaluation, the change only updates an internal config value used for CLI display and does not alter any routing, handler, or network-facing behavior, so external tests are not warranted.
Accessibility
No frontend changes — not applicable.
Recommendations
- Remove redundant local import (optional):
start_listening_unix_socketstill has a localimport tornado.netutilinside the function body. Sincetornado.netutilis now imported at module level, this local import is redundant and can be removed for consistency.
Verdict
APPROVED: Clean, well-tested fix that correctly resolves the port-0 URL display issue using an idiomatic Tornado pattern, with unanimous approval from all three reviewers and no backwards compatibility or security concerns.
Consolidated review by claude-4.6-opus-high-thinking from 3/3 expected reviews (claude-4.6-opus-high-thinking, gemini-3.1-pro, gpt-5.3-codex-high). No reviews failed to complete.
This review also includes 2 inline comment(s) on specific code lines.
| # When binding to port 0, Tornado asks the OS for an ephemeral port. | ||
| # Update server.port with the actual bound port so displayed URLs are correct. | ||
| if port == 0: | ||
| actual_port = sockets[0].getsockname()[1] |
There was a problem hiding this comment.
thought: When server.address is None (the default) and port is 0, bind_sockets may return multiple sockets (e.g., IPv4 + IPv6) each with a different ephemeral port. Using sockets[0] picks just the first one. This is strictly better than the previous behavior (displaying port 0), and in practice Streamlit users typically have a specific address or use localhost, so this is fine. Worth noting as a future enhancement opportunity if multi-interface port consistency is ever needed.
There was a problem hiding this comment.
This is actually already handled by Tornado. bind_sockets tracks a bound_port variable — after the first socket binds to port 0 and gets an ephemeral port, subsequent sockets have their port replaced with that value before bind() is called (source). So all returned sockets are guaranteed to share the same port, and sockets[0].getsockname()[1] will be correct.
| finally: | ||
| http_server.stop() | ||
|
|
||
| sockets = getattr(http_server, "_sockets", None) |
There was a problem hiding this comment.
nitpick: http_server._sockets is a private Tornado attribute. Consider relying solely on http_server.stop() (line 788) for cleanup, which should suffice for test teardown. If explicit socket closure is needed for reliability, this approach is acceptable but could break if Tornado renames the internal attribute.
There was a problem hiding this comment.
@joanaarnauth this one looks like it could be good to address just for defensibility.
Would be nice to clean this up as well. Otherwise looks good, will approve after these small nits! Marking "changes requested" for clarity. |
sfc-gh-lwilby
left a comment
There was a problem hiding this comment.
Looks good to me, will approve once the two small changes are made, thanks!
|
Thanks I addressed the requested changes:
|
sfc-gh-lwilby
left a comment
There was a problem hiding this comment.
LGTM, thank you for the contribution @joanaarnauth
Describe your changes
This PR fixes an issue where Streamlit displays incorrect URLs when
server.port=0.When port 0 is used, Tornado binds the server to an ephemeral port chosen by the operating system. However, the configuration value
server.portremained 0, which caused the CLI to display URLs such ashttp://localhost:0.This patch updates
server.portwith the actual bound port after the TCP socket is created so that the displayed URLs reflect the real listening port.Screenshot or video (only for visual changes)
Not applicable.
GitHub Issue Link (if applicable)
Closes #11308
Testing Plan
The bug occurs specifically when
server.portis set to 0 and the OS assigns an ephemeral port. A unit test was added to verify that the configuration is updated with the real port once the socket is bound.Unit Tests (JS and/or Python)
Added the test:
test_updates_configured_port_when_binding_to_zeroin:
lib/tests/streamlit/web/server/server_test.pyThis test verifies that when
server.port=0, the configuration value is updated to the actual assigned port.E2E Tests
No E2E tests are required because this change only affects the internal server configuration logic and CLI output.
Contribution License Agreement
By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.