fix(browser): keep draining load-delaying scripts after a page exception - #744
fix(browser): keep draining load-delaying scripts after a page exception#744ntdatt812 wants to merge 1 commit into
Conversation
An exception from an async page callback reaches drive_load_delaying_scripts as an event-loop error, and the pump answered it by returning, which abandons every still-pending load-delaying script. The error is transient. Probing this runtime, the tick carrying the exception fails and the next three poll clean, so the pump discarded the page's remaining work over a condition that had already cleared. It also discarded it silently: nothing surfaces to CDP, the scripts simply never arrive, and the run ends on the script deadline instead. Browsers report such an exception and keep processing. Log the error and keep draining until the deadline or the pending set empties. The absolute deadline above was already the real bound and stays authoritative. Only the first error warns; the rest go to debug so a page that throws every tick cannot flood the log. Fixes h4ckf0r0day#699.
|
Reviewed the patch — the shape is right. The deadline-as-only-bound tradeoff is the correct call: on a page that throws every tick, the pump now burns its remaining budget instead of abandoning still-pending work over an error that a fresh tick would clear, and that is bounded (the absolute deadline) rather than unbounded. One question, from the test comment: "Thrown during the installing script's own microtask drain it would clear the pending-script bookkeeping before the fetch even starts, which is a different bug from the one under test here." Is that second failure mode tracked anywhere? We would like to check our tree for it — if it turns out to be real on our side too, it deserves its own issue rather than riding on this PR. |
|
Obstacle course, for the CONTRIBUTING pre-PR item I could not tick from a unit test. Companion repo at the ref your CI pins (
No delta. The single failure is the same stage on both, and it fails on untouched So it is a property of this host rather than of the change, and I have not chased it further since it is outside the diff. It is deterministic rather than flaky here: re-running just that stage with Everything else in this PR is unchanged. |
|
That stage is #671, and the back-to-back table here (no delta between
Until the fixture re-observes or toggles the sentinel, I'd treat the stage as known-fail per #671 rather than a regression signal for PRs like this one. |
Fixes #699.
What changed
drive_load_delaying_scriptsanswered an event-loop error withreturn false, which abandons every still-pending load-delaying script.Ok(Err(_))now logs and keeps draining. The absolute deadline immediately above was always the real bound and stays authoritative, so this cannot extend a run. Only the first error warns; the rest go todebugso a page that throws every tick cannot flood the log.The question that decides this is whether an event-loop error means the page is finished or merely that one tick carried an exception. I measured it: four consecutive ticks per case, one fresh runtime each.
It is transient. The tick carrying the exception fails and the next ones poll clean. So the pump was discarding the page's remaining work over a condition that had already cleared, and discarding it silently: nothing surfaces to CDP, the scripts simply never arrive, and the run ends on the script deadline instead. That silence is why the reporter needed instrumented Chromium to find it.
The same probe explains why the reporter's synthetic repros did not fail.
setTimeoutis already contained atbootstrap.js:881(try { f(...args); } catch(e) { console.error("Timer error:", e); }), so a throwing timer behaves like Chromium. The throw that reaches the pump arrives through a microtask-class callback, matching their stack ending ateventLoopTick (ext:core/01_core.js).I took @yinnho's second suggestion rather than their first: the dispatch-boundary containment they described is largely present here already (timers at
bootstrap.js:881, anonunhandledrejectionatbootstrap.js:82), and the remaining path is inside deno_core rather than in our JS, so the pump is where this has to be handled.Validation
New test
load_delaying_script_driver_survives_a_page_script_exception, next to the existing deadline test and using the same harness. It installs a real pending load-delaying script against the delayed-script server, defers a throw onto a pump tick, and asserts the script is still fetched and the pump completes.Red with the old
return falserestored, green with the fix:Full four-configuration sequence from CONTRIBUTING, in a
rust:latestcontainer so the result matches CI rather than my Windows host:cargo build --release -p obscura-cli --bins --features rendercargo nextest run --release --features render --no-fail-fastcargo build --release -p obscura-cli --bins --no-default-featurescargo nextest run --release -p obscura --no-fail-fastcargo nextest run --release --workspace --exclude obscura --exclude obscura-render --no-default-features --no-fail-fastcargo check --release -p obscura-render --no-default-featuresA constraint on any test written here, which cost me two attempts
The throw has to be deferred onto a pump tick. Thrown during the installing script's own microtask drain, it clears the pending-script bookkeeping before the fetch even starts, so the pump completes trivially and the test passes for the wrong reason. That is a separate defect from this one and I have not touched it.
Rendering
Not applicable. This is the script-loading pump; no layout, paint, screenshot, screencast or PDF code is touched.
Performance
This can only shorten a run, never lengthen one. Before, a transient error ended the drive early and left the page waiting on the script deadline; now the pump keeps going and finishes when the work is actually done. The absolute deadline above the loop is unchanged and still bounds the worst case. The added cost per error is one counter increment and one log line, and the log is rate-limited to a single
warn.Two things I found and deliberately did not fold in
Promise.rejectreaches the event loop despite the handler.bootstrap.js:82setsglobalThis.onunhandledrejectionto callpreventDefault(), and the probe above shows a plainPromise.rejectstill surfacing asEvent loop error. That handler is not doing what it looks like it does. It is a real gap, it is not what breaks the reported page, and it belongs in its own issue.The bookkeeping reset described above, where a throw during the installing script's microtask drain drops the pending set. Also separate.
Happy to file both, or to fold either in if you would rather have one change. Neither needs an answer before this merges.
Checklist