Skip to content

fix(layout): run the fallback script when the primary attempt hangs - #77

Merged
chelentos merged 1 commit into
gravity-ui:mainfrom
chelentos:fix/fallback-hanging-primary
Oct 23, 2026
Merged

chelentos merged 1 commit into
gravity-ui:mainfrom
chelentos:fix/fallback-hanging-primary

Conversation

@chelentos

Copy link
Copy Markdown
Contributor

What was wrong

With publicPathFallbacks configured, the start-asset loader only recovered when the primary base failed fast. When the CDN hung and answered later than assetRetry.timeoutMs, the fallback script was downloaded but never executed, the loader gave up after the second timeout, and the remaining entry scripts were never requested. See #76 for the full analysis.

Why

  • createAssetElement created scripts with async = false, so every attempt entered the browser's "list of scripts that will execute in order as soon as possible". The fallback attempt queued behind the hanging primary one and waited for it although it was already downloaded.
  • On timeout the loader only called element.remove(). That neither cancels the in-flight request nor removes the element from that list, so Chromium still executed the stale primary script when its response finally arrived (a second execution of runtime.js, after the fallback copy).
  • When the fallback attempt also timed out, finish(false) set stopped = true and the queue stopped; app.js was never requested.

What changed

  • Scripts are created with async = true. Ordering does not depend on async = false: processScriptQueue already awaits each loadAsset before taking the next script, so entry scripts still run one after another.
  • On timeout the abandoned script is removed and then moved into a detached document (document.implementation.createHTMLDocument('').adoptNode(el)). Per the HTML spec a script is not executed when its node document differs from the one it was prepared in, so the late response is ignored. This was verified in Chromium (see below) rather than assumed: with remove() alone the late script still runs; after adoptNode it does not.
  • Styles, the fast-fail (404) path, data-app-layout-preload handling and the app-layout:asset-load-error event are unchanged.
  • New jest test script timeout neutralizes the hanging primary so the fallback runs first: primary times out, fallback is inserted as an async script, the stale primary is detached from the document, the fallback's load lets the next script start, and a late load on the primary changes nothing. jsdom does not execute scripts, so the "never executed" part is asserted through the node-document condition and covered by the browser run below.

The request to the hung CDN is not cancelled (a <script> cannot abort its fetch); it just can no longer execute. Cancelling would require fetch + AbortController + an inline script, which changes caching and sourcemap behaviour, so it was not done.

How it was verified

  • npm run lint, npm run typecheck, npm test (44 tests), npm run build on Node 18.
  • End to end with https://github.com/chelentos/app-layout-cdn-hang-repro (real createLayoutPlugin, /cdn/runtime.js answers after 22 s, /build/ answers at once, default retry options) driven by Playwright with Chromium 130, the repro's dependency pointed at npm pack of this branch.

Before (published 2.8.0), CDN_MODE=hang:

12 ms  DOMContentLoaded
20016 ms  app-layout:asset-load-error {"type":"script","originalUrl":"/cdn/runtime.js","attempts":["/cdn/runtime.js","/build/runtime.js"]}  -> loader stopped
22017 ms  EXEC /cdn/runtime.js
22017 ms  EXEC /build/runtime.js

After (this branch), CDN_MODE=hang, page observed for 25 s:

14 ms  DOMContentLoaded
10027 ms  EXEC /build/runtime.js
10029 ms  EXEC /build/app.js

No EXEC /cdn/runtime.js at 22 s, no asset-load-error. Server log: /build/runtime.js and /build/app.js requested at 10 s, nothing else.

After, CDN_MODE=404 control (unchanged behaviour):

12 ms  DOMContentLoaded
20 ms  EXEC /build/runtime.js
21 ms  EXEC /build/app.js

Fixes #76

I hereby agree to the terms of the CLA available at: https://yandex.ru/legal/cla/?lang=en

🤖 Generated with Claude Code

@chelentos chelentos changed the title fix(layout): run the fallback script when the primary attempt hangs [WIP] fix(layout): run the fallback script when the primary attempt hangs Oct 23, 2026
@chelentos
chelentos marked this pull request as draft October 23, 2026 08:29
@chelentos chelentos changed the title [WIP] fix(layout): run the fallback script when the primary attempt hangs fix(layout): run the fallback script when the primary attempt hangs Oct 23, 2026
@chelentos
chelentos marked this pull request as ready for review October 23, 2026 09:14
Scripts were created with async = false, so a fallback attempt queued
behind a hanging primary one in the browser's in-order execution list
and was not executed although already downloaded. On timeout the stale
element was only removed, which neither cancels the request nor stops
the late response from executing, so the primary copy ran a second time
after the fallback once the CDN finally answered.

Create scripts with async = true (the loader already serializes them
itself) and, on timeout, move the abandoned script into a detached
document so that its late response is never executed.

Fixes gravity-ui#76

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@chelentos
chelentos force-pushed the fix/fallback-hanging-primary branch from a308974 to f49bffa Compare October 23, 2026 09:18
@DanilaBeseda
DanilaBeseda self-requested a review October 23, 2026 09:29
@chelentos
chelentos merged commit d095cfb into gravity-ui:main Oct 23, 2026
2 checks passed
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.

Start-asset fallback (publicPathFallbacks) does not work when the CDN hangs instead of failing fast

2 participants