Replay refused top-level navigations in the browser VM - #414
Closed
ulziibay-kernel wants to merge 1 commit into
Closed
ulziibay-kernel wants to merge 1 commit into
ulziibay-kernel wants to merge 1 commit into
Conversation
A client asks for a page, the site answers 429, and the client is left holding a block page. A person reloads; an agent reads the block page and concludes the site is unavailable. pagerecovery intercepts main-frame document responses at the Fetch response stage and answers a refusal with a 307 back to the same URL rather than letting it through. Chromium treats that as one more hop in the navigation already in flight, so the caller's goto resolves once, on the page it asked for. Cookies the refusal set are still applied, since the network stack processes them before the request is paused. Replays are bounded by attempts and wall clock, honour Retry-After when it fits the budget, and back off with full jitter. Settled answers (403), Kernel's own branded proxy errors, subresources, iframe documents and non-GET navigations are passed through untouched. Off by default behind PAGE_RECOVERY_ENABLED. Counters are served label-free on the existing /metrics endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Author
|
Superseded by kernel/kernel-images-private#406 — working around a site's refusals belongs with the rest of the evasion surface rather than in the public image. The private version also carries two fixes found after this PR was opened: a replay that cannot be delivered now passes the refusal through instead of leaving the navigation paused, and the concurrency test's tab-coverage gate was wrong (it counted the pre-existing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A client asks for a page, the site answers
429 Too Many Requests, and the client is left holding a block page. A person in that position hits reload. An agent usually does not — it reads the document it was handed, concludes the site is unavailable, and reports back.This adds
server/lib/pagerecovery, which does that reload early enough that the caller never sees the refusal.It intercepts main-frame document responses at the Fetch response stage and answers a refusal with a
307back to the same URL instead of letting it through. Chromium treats that as one more hop in the navigation already in flight, so apage.goto()resolves once, on the page it asked for, having waited out the retries. It is not a second navigation, so nothing the client is waiting on is interrupted.Off by default behind
PAGE_RECOVERY_ENABLED.What is replayed
408,429,502,503,504,507ConnectionReset,ConnectionClosed,ConnectionFailed,ConnectionAborted,TimedOut403502withX-Kernel-Proxy-Errorcdpmonitoras a typedproxy_errorGETnavigationsNon-
GETis excluded because the replay preserves method and body: a POST a gateway refused may still have been recorded upstream.Retries are bounded by attempts (default 2) and wall clock (default 8s) per session and URL, sized to fit inside a caller's
gototimeout with the real page load still to come.Retry-Afteris honoured when it fits what the budget has left; otherwise backoff is exponential with full jitter from 300 ms. Past the budget the refusal goes through, so a block never turns into a navigation that looks hung.Notes from building it
Two things were verified against real Chromium rather than assumed:
Set-Cookiebefore the request is paused, so a clearance cookie handed out by a block page is present on the retry — which is the mechanism that makes a manual reload work at all.page.routeinstalls interception on its own session; both interceptors see every request. There is a test for it.A fulfillment must carry a body, even an empty one: Chromium treats a fulfillment without one as no fulfillment at all and lets the original response through. That silently produced a no-op until the e2e test caught it.
Scope
This handles only the half of the problem that can be decided from the response itself. A block that answers
200and puts an interstitial in the document needs a reading of the rendered page and a real reload after the document has run; that is deliberately not here.There is no API-level or SDK-level surface yet — this is the in-VM mechanism plus an env flag.
Testing
Both pass locally, including the real-Chromium suite: the transparent case, an exhausted budget landing the caller on the site's own answer, an unrefused navigation going untouched, a
403left alone, a client interceptor still seeing every request, and six tabs refused at once each recovering independently.lib/devtoolsproxy'sTestUpstreamManagerDetectsChromiumAndRestartflakes on TempDir cleanup under-count=3on this machine. It does so identically on a clean tree and is unrelated to this change.🤖 Generated with Claude Code