You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Clicking links on webjs.dev intermittently shows a loading spinner in the browser tab and flashes the ENTIRE document, navbar included. Reproduction (live site, real browser, roughly every other lap):
Click the top-left logo to return to the landing page
Go to /docs again
Sometimes the whole document flashes with a tab spinner, sometimes it does not
The tab spinner and the flash are ONE event: a main-frame document load. With the client router enabled, clicking a same-origin link should never do that.
The client router consumes a POISONED PREFETCH ENTRY, finds the fragment shares no boundary with the live DOM, and correctly degrades to location.href (the #1015 integrity fallback). Captured cause string, via the new webjs:navigation-fallback event, on every failure: no-shared-boundary.
How the poison is planted, from the captured request log:
/docs/getting-started have=/:/ prefetch=1 <- hover on home: correct
/docs/getting-started have=/docs/...,/docs,/:/ prefetch=1 <- fired FROM the docs page itself
NAV-REQUESTED: scriptInitiated -> /docs/getting-started <- the reload
Hover the /docs link on the landing page. The intent prefetcher starts its dwell timer.
Click. The soft nav lands; the DOM is now the docs page.
The hover's timer fires ANYWAY and prefetches /docs/getting-started, computing X-Webjs-Have from the NOW-SWAPPED DOM. The server correctly answers "you already have everything" with a near-empty fragment. (dogfood: the client router prefetches the page you are already on #1106 documented this same wasted request from the duplicate-fetch angle.)
The fragment is cached under the URL, TTL 30 s. Hovering again does NOT refresh it: the fresh-entry check skips the refetch.
Return to the landing page, click /docs again. The click consumes the poisoned entry, applySwap finds no shared boundary, and the router does the only safe thing: a full page load. Spinner, flash.
The reload wipes the in-page cache, so the next lap is clean, and the lap after re-poisons: the repro alternates, which is the "sometimes".
prefetchTake() ignored the X-Webjs-Have context an entry was fetched against. The server marks the reduced response Vary: X-Webjs-Have; the prefetch cache is a client-side cache of that response and violated its own vary dimension.
Reproduced 6-of-24 clicks before the fix, 0-of-24 after, same randomized hover dwells, via a fine-pointer + hover-dwell puppeteer rig (headless defaults report hover: none, which is why earlier rigs missed it: intent prefetch never ran).
A third defect made all of this undiagnosable in production and is fixed on the same PR: the router's full-page-load degradations were logged by a DEV-ONLY console warning (devWarnFallback), and the deploy-mismatch reload had no diagnostic at all. reportFallback now dispatches webjs:navigation-fallback on document in every environment with { cause, href, willReload }.
Ruled out along the way (measured, do not re-investigate)
Cloudflare serving a reduced fragment to a full-document request (Vary: X-Webjs-Have unhonoured): every response on the repro path is cf-cache-status: DYNAMIC, nothing cached, cannot fire. Remains a latent hazard if HTML caching is ever enabled.
The startup interception window (module boot is deferred, links clickable before the router listens): real, but 25 to 35 ms warm-cache, not human-clickable. Network-sized only with DevTools "Disable cache" or a genuine first visit; worth closing separately someday, not this bug.
Cloudflare Speed Brain (/cdn-cgi/speculation is 200 on webjs.dev): explains the second hover fetch a user sees in the Network tab; a speculation prefetch cannot hijack an intercepted click.
Problem
Clicking links on webjs.dev intermittently shows a loading spinner in the browser tab and flashes the ENTIRE document, navbar included. Reproduction (live site, real browser, roughly every other lap):
/docs/docsagainThe tab spinner and the flash are ONE event: a main-frame document load. With the client router enabled, clicking a same-origin link should never do that.
Root cause (found, captured, fixed on PR #1115)
The client router consumes a POISONED PREFETCH ENTRY, finds the fragment shares no boundary with the live DOM, and correctly degrades to
location.href(the #1015 integrity fallback). Captured cause string, via the newwebjs:navigation-fallbackevent, on every failure:no-shared-boundary.How the poison is planted, from the captured request log:
/docs/getting-started, computingX-Webjs-Havefrom the NOW-SWAPPED DOM. The server correctly answers "you already have everything" with a near-empty fragment. (dogfood: the client router prefetches the page you are already on #1106 documented this same wasted request from the duplicate-fetch angle.)applySwapfinds no shared boundary, and the router does the only safe thing: a full page load. Spinner, flash.Two composing framework defects:
prefetch()could prefetch the page the user is already on (dogfood: the client router prefetches the page you are already on #1106).prefetchTake()ignored theX-Webjs-Havecontext an entry was fetched against. The server marks the reduced responseVary: X-Webjs-Have; the prefetch cache is a client-side cache of that response and violated its own vary dimension.Reproduced 6-of-24 clicks before the fix, 0-of-24 after, same randomized hover dwells, via a fine-pointer + hover-dwell puppeteer rig (headless defaults report
hover: none, which is why earlier rigs missed it: intent prefetch never ran).A third defect made all of this undiagnosable in production and is fixed on the same PR: the router's full-page-load degradations were logged by a DEV-ONLY console warning (
devWarnFallback), and the deploy-mismatch reload had no diagnostic at all.reportFallbacknow dispatcheswebjs:navigation-fallbackondocumentin every environment with{ cause, href, willReload }.Ruled out along the way (measured, do not re-investigate)
<style>(dogfood: a page/layout <style> churns the CSSOM on every navigation, flashing preserved layouts #1109's attribution): about 4.6 ms of extra style recalc per navigation. Real waste, cannot flash a document.Vary: X-Webjs-Haveunhonoured): every response on the repro path iscf-cache-status: DYNAMIC, nothing cached, cannot fire. Remains a latent hazard if HTML caching is ever enabled.webjs:importmap-reloadflag was never set on any captured failure./cdn-cgi/speculationis 200 on webjs.dev): explains the second hover fetch a user sees in the Network tab; a speculation prefetch cannot hijack an intercepted click.Acceptance criteria
prefetch()never fetches the current page (also closes dogfood: the client router prefetches the page you are already on #1106)prefetchTake()rejects an entry whose storedhavediffers from the live onewebjs:navigation-fallbackwith its cause, in productionwebjs:navigation-error