Skip to content

fix: Wait for app port to bind before navigating (#448) - #449

Merged
schloerke merged 2 commits into
mainfrom
schloerke/issue-448
Jul 14, 2026
Merged

fix: Wait for app port to bind before navigating (#448)#449
schloerke merged 2 commits into
mainfrom
schloerke/issue-448

Conversation

@schloerke

Copy link
Copy Markdown
Collaborator

Fixes #448

Summary

An intermittent AppDriver$new() failure — Shiny app did not become stable — traces back to a startup race: Shiny prints its Listening on http://… line one statement before it binds the listening socket (rstudio/shiny#4400). app_start_shiny() parsed the URL from that stderr line and let initialization navigate the browser immediately, so on a slow or loaded host the browser could reach a not-yet-bound port, land on Chrome's error page, and when the app finally bound, the error-page recovery navigation raced the readiness/idle checks into the opaque abort (which fires well before load_timeout).

This adds an app_wait_for_serving() helper that polls the port with pingr::is_up() — the same reachability check app_httr2_get() already uses — until it accepts a connection or load_timeout elapses, then returns. Initialization therefore navigates against a bound socket. If the port never comes up in time, the helper returns quietly and startup proceeds as before (graceful fallback). This mirrors the approach proposed by @nbenn in the issue.

Verification

Using the issue's deterministic reproducer (tracing startServer to sleep 5s before bind): without the fix it aborts in ~6s with did not become stable … Caused by error in app_wait_for_idle(); with the fix the app becomes stable in ~8s. New unit tests in tests/testthat/test-app-start.R cover both the unbound-port (returns FALSE, honors timeout) and bound-port (returns TRUE) cases.

@schloerke
schloerke marked this pull request as ready for review July 14, 2026 19:27
@schloerke
schloerke enabled auto-merge (squash) July 14, 2026 19:28
@schloerke schloerke closed this Jul 14, 2026
auto-merge was automatically disabled July 14, 2026 19:28

Pull request was closed

@schloerke schloerke reopened this Jul 14, 2026
schloerke and others added 2 commits July 14, 2026 16:02
Shiny prints its "Listening on ..." line before it binds the listening
socket, so on a slow or loaded host AppDriver$new() could navigate to a
not-yet-bound port, land on the browser's error page, and race the
error-page recovery navigation into an opaque "did not become stable"
abort. app_start_shiny() now waits for the port to accept a connection
(via pingr::is_up(), the same check app_httr2_get() uses) before
returning, so initialization navigates against a bound socket.

Co-authored-by: Nicolas Bennett <3158446+nbenn@users.noreply.github.com>
The new port-binding tests use httpuv::randomPort()/startServer() to
exercise app_wait_for_serving() against a real socket, so declare httpuv
in Suggests (fixing the "'::' import not declared from: 'httpuv'" R CMD
check warning) and guard the tests with skip_if_not_installed().
@schloerke

Copy link
Copy Markdown
Collaborator Author

CI status — failures are unrelated to this change; merging via admin

The Package checks workflow is red, but neither failure is caused by this PR's code:

  1. test-app-image test-app snapshot failures — pre-existing and reproducible on main. Every scheduled Package checks run on main has failed on these screenshot-comparison snapshots for months (e.g. run 27944676614 on 3844574). They stem from environment/rendering differences, not this change. This PR touches only an internal startup helper, one Suggests entry, and a new unit test — nothing in the image-rendering path.

  2. man/AppDriver.Rd \if/\subsection/\code macro warnings (which fail the build under error_on = "warning") — introduced by the CI routine job's devtools::document() auto-commit, which regenerated the docs with roxygen2 8.0.0, whereas the package is documented with 7.3.3 (RoxygenNote: 7.3.3). This is a CI-tooling regression, not a source change here. main's Rd files check OK with 7.3.3.

To keep main's documentation intact, I've dropped the bot's devtools::document() (roxygen 8.0.0) and use_tidy_description() auto-commits from the branch. The PR now contains only the intended change:

  • R/app-driver-start.R — wait for the app port to accept a connection before navigating (new app_wait_for_serving() helper)
  • tests/testthat/test-app-start.R — unit tests for the helper
  • DESCRIPTION — declare httpuv in Suggests (used by the new tests)
  • NEWS.md — changelog entry (credits @nbenn)

test-actions (the app-driver integration workflow) passes. Merging with admin since the remaining Package checks failures are pre-existing/environmental and unrelated to this change.

@schloerke
schloerke force-pushed the schloerke/issue-448 branch from 81c5bbb to 9f7d2a7 Compare July 14, 2026 20:02
@schloerke
schloerke merged commit dc68aea into main Jul 14, 2026
4 of 18 checks passed
@schloerke
schloerke deleted the schloerke/issue-448 branch July 14, 2026 20:03
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.

Intermittent "did not become stable" at init: AppDriver$new() navigates before the app's port is bound

1 participant