Skip to content

Give Screen.open enough budget to retry a slow page load - #291

Draft
evnchn wants to merge 1 commit into
mainfrom
fix/screen-open-retry-budget
Draft

Give Screen.open enough budget to retry a slow page load#291
evnchn wants to merge 1 commit into
mainfrom
fix/screen-open-retry-budget

Conversation

@evnchn

@evnchn evnchn commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Drafted by Claude Code on evnchn's behalf.

Fork staging PR — for review before anything goes upstream.

Motivation

Screen.open() has a retry loop, but it can never retry the failure it most needs to: a slow page load.

Two numbers live in different files and are silently coupled the wrong way round:

  • screen_plugin.pydriver_.set_page_load_timeout(4)
  • screen.pydef open(self, path: str, timeout: float = 3.0)

A single selenium.get() may block for the full 4 s page-load timeout. By the time the except branch checks if time.time() > deadline, the 3 s budget has already expired — so open() re-raises on the first attempt. The loop below it is dead code for this failure mode:

deadline = time.time() + timeout          # 3 s
while True:
    try:
        self.selenium.get(self.url + path)  # may block 4 s
        ...
    except Exception as e:
        if time.time() > deadline:          # always true after a page-load timeout
            raise

This is not hypothetical — it is the mechanism behind two separate merge-queue flakes:

Failure why the page load exceeded 4 s
tests/test_add_html.py::test_add_head_html first test in the whole suite — cold browser, nothing warm or cached
tests/test_alternate_ui_frameworks.py::test_element_plus blocked on an external CDN fetch

Both died with timeout: Timed out receiving message from renderer, each after exactly one attempt.

Implementation

Make the coupling explicit and give the retry loop room to actually run:

class Screen:
    PAGE_LOAD_TIMEOUT = 4

    def open(self, path: str, timeout: float = 3 * PAGE_LOAD_TIMEOUT) -> None:

and in screen_plugin.py, driver_.set_page_load_timeout(Screen.PAGE_LOAD_TIMEOUT) so the two can no longer drift apart silently. The docstring states the invariant: the budget must stay above PAGE_LOAD_TIMEOUT, or there is no room to retry.

A page that is genuinely unreachable now takes ~12 s to fail instead of ~4 s. That cost is paid only by failing tests.

Verification

Instrumented selenium.get() to count attempts, with CDP network latency forcing every navigation past the page-load timeout:

selenium.get() attempts before open() gave up
before 1 — the retry loop never runs
after 5

No regressions: full suite on macOS / py3.14 — 9 failed, 1003 passed, 3 skipped, 2 xfailed. The 9 are the pre-existing macOS failures (test_dark_mode[*] oklch assertions, test_module_access_does_not_import_others), identical to a run on unmodified main.

Gates: pre-commit all Passed · mypy clean over 245 source files · pylint 10.00/10.

Relationship to the other flake fixes

This is the shared root cause underneath the renderer-timeout family. It is independent of, and complementary to, the CDN fix: removing the external fetch stops test_element_plus from needing >4 s, while this gives every test a second chance when a page load runs long for any reason.

It is also why test_add_head_html — a test with no network dependency at all — failed once in CI: being first in the suite, it pays the cold-browser cost with no retry to absorb it.

Progress

  • The PR title is a short phrase starting with a verb like "Add ...", "Fix ...", "Update ...", "Remove ...", etc.
  • The implementation is complete.
  • This PR does not address a security issue.
  • Pytests have been added/updated — not added. The defect is a timing interaction with the browser's own page-load timeout; a test for it would have to stall a real page load past 4 s (I verified it with CDP latency injection, which is too heavy and too timing-dependent to commit). The before/after attempt counts are in the verification fold.
  • Documentation is not necessary — internal test-harness behaviour.
  • No breaking changes to the public API. open() keeps its signature; only the default timeout grows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant