Skip to content

fix: Make launch-browser independent of Uvicorn log level#2317

Merged
schloerke merged 17 commits into
mainfrom
fix-launch-browser-readiness
Jul 8, 2026
Merged

fix: Make launch-browser independent of Uvicorn log level#2317
schloerke merged 17 commits into
mainfrom
fix-launch-browser-readiness

Conversation

@karangattu

@karangattu karangattu commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #569 by replacing the --launch-browser readiness signal from Uvicorn log interception with a startup callback.

This makes browser launch work when users run with --log-level=warning or disable INFO logging.

Changes

  • Add ShinyServer, a small uvicorn.Server wrapper that runs an on_started callback after startup.
  • Mirror uvicorn.run() locally so Shiny can pass that callback through reload and non-reload modes.
  • Replace LaunchBrowserHandler with direct browser opening after startup.
  • Remove the reload log-handler path and call _autoreload.reload_end() from startup instead.
  • Add regression tests and a changelog entry.

Manually tested the following cases

shiny run app.py --launch-browser --log-level=warning → browser opens
shiny run app.py --reload --launch-browser → browser opens once

Refactor server startup event handling to use callback-based approach instead of parsing uvicorn log messages. This change introduces a new ShinyServer class that extends uvicorn.Server with an on_started callback, and a _run_uvicorn function to manage uvicorn configuration and lifecycle. The callback-based approach is more reliable and decouples application startup logic from uvicorn's logging implementation.
Use functools.partial for on_started so it is picklable in reload/multiprocess modes (fixes --launch-browser when Uvicorn INFO logs are disabled). Replace cast-based attribute access with getattr checks for config/server attributes (reload, workers, uds, started), remove SHINY_BROWSER_HOST/PORT env var side-effects, and update CHANGELOG.md.
@karangattu karangattu requested a review from schloerke July 3, 2026 06:36
Replace getattr usage with typed attribute access using typing.cast. Adds cast import and introduces config_attrs = cast(Any, config) for repeated access, uses cast(Any, server).started for server checks, and casts uds to str | None before removal. This refactor improves static typing (pyright/mypy) and removes dynamic getattr calls without changing runtime behavior.

@schloerke schloerke left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clear improvement over the old log-string sniffing, and I verified the _run_uvicorn() mirror against uvicorn 0.50.2's run() — the reload/workers/uds/startup-failure logic matches. Pickling in reload mode is safe (reload_end is module-level; the partial(launch_browser, ...) path only runs in non-reload), the reload branch keeps autoreload websocket notifications working even without --launch-browser, and the port-conflict fallback (reload = False) correctly falls through to the browser-launch assignment.

A few comments inline — the dev_mode test leak is the one I'd like fixed before merge; the rest are take-it-or-leave-it.

One more thing to be aware of (not a defect): this PR stops setting SHINY_BROWSER_HOST/SHINY_BROWSER_PORT entirely. They're internal-only per a grep of the codebase, but they were visible to running user apps, so any out-of-tree code reading them will break silently.

Comment thread tests/pytest/test_main.py
Comment thread shiny/_main.py Outdated
Comment thread shiny/_main.py Outdated
schloerke added 4 commits July 6, 2026 12:22
…rkarounds

Subclassing Config as ShinyConfig with declared reload/workers/uds
attributes (and declaring started on ShinyServer) satisfies pyright's
strict mode against the generated uvicorn stubs, which omit instance
attributes. Also use a named argument for webbrowser.open(new=1).
@schloerke schloerke marked this pull request as draft July 6, 2026 16:59
schloerke added 8 commits July 8, 2026 10:42
ShinyConfig, ShinyServer, _run_uvicorn, maybe_setup_rsw_proxying,
_set_workbench_kwargs, and ReloadArgs now live in shiny/_uvicorn.py.
The reload and launch-browser branches can never both assign
on_started; explain why and assert the invariant.
The bare `import uvicorn` lost its last use when uvicorn.run() was
replaced by _run_uvicorn(); the remaining uvicorn.config.* references
are covered by `import uvicorn.config`. setup_hot_reload() had become
a pure pass-through to _autoreload.start_server() with a single call
site, so call it directly.
Bisected uvicorn 0.16.0..0.51.0 (uv run --with uvicorn==X per version):
- uvicorn.config.STARTUP_FAILURE is only importable since 0.50.0, so the
  local _UVICORN_STARTUP_FAILURE copy is required at the >=0.16 floor
- Config.load_app() was added in 0.47.0, so the hasattr guard is required
- uvicorn._types.ASGIApplication was added in 0.23.0, so the TYPE_CHECKING
  guard is required
- uvicorn.supervisors ChangeReload/Multiprocess exist at 0.16.0 (unguarded
  import is safe)
The 0.16.0 floor was fictional. On Posit Workbench, run_app() injects
ws_per_message_deflate into uvicorn's Config, a keyword that only exists
in uvicorn >= 0.17.0 -- so 'shiny run' on Workbench raised TypeError with
the floor version installed. No CI job installed the floor, so the pin
rotted unnoticed since 2021.

Why 0.23.0 (July 2023): old enough to be universally available, new
enough to cover the Workbench requirement with margin, and the first
release to ship uvicorn._types.ASGIApplication, which shiny/_uvicorn.py
references in type hints. Deliberately NOT 0.47.0/0.50.0, which would
allow deleting the Config.load_app() hasattr guard and the local
_UVICORN_STARTUP_FAILURE constant: those released 2026-05-14 and
2026-07-04 -- too recent to require. Boundaries were established by
bisecting uvicorn 0.16.0..0.51.0 with isolated 'uv run --with
uvicorn==X' probes.

The new check-uvicorn-floor CI job installs uvicorn==0.23.0 on top of
the normal dev setup and runs the unit tests, so the floor cannot
silently rot again. Verified locally: full unit suite (864 tests)
passes against uvicorn 0.23.0.
uvicorn 0.51.0 removed Multiprocess's target parameter (workers build
their own Server from config), which broke pyright in CI against
freshly generated stubs. Dispatch on the runtime signature and suppress
the stub mismatch on both arms, since only one can match at a time.
@schloerke schloerke marked this pull request as ready for review July 8, 2026 15:39
@schloerke schloerke merged commit bbc9a16 into main Jul 8, 2026
172 checks passed
@schloerke schloerke deleted the fix-launch-browser-readiness branch July 8, 2026 15:56
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.

logging.disable(logging.INFO) conflicts with Shiny's logging and also prevents it from launching browser on start ("--launch-browser")

2 participants