[🐞] A layout's loader request runs the loaders of the page at the layout's path as guards: a redirect there bounces client navigations in production
Maintainers usually reply within 1 day
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 46/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- javascript, typescript
- Domain
- backend-api-design
Research direction
Start with runtime/src/route-loaders.ts to see how production loader requests choose their path, then read middleware/request-handler/handlers/loader-handler.ts and resolve-request-handlers-core.ts for guard selection. Reproduce with the linked app's check.sh and compare the requested layout loader with the /home/ request. Done means a production navigation to /about/ is not redirected by the index page loader, while the relevant blocking guards still run.
Written by the indexing model from the issue text.
Description
Which component is affected?
Qwik Router
Describe the bug
In a production build with the default strictLoaders, the client asks for a layout's loaders at the path they are recorded at: the root layout's at /, a nested layout's at its own directory. The page-path header is only sent with strictLoaders: false, or in dev (fetchRouteLoaderData in runtime/src/route-loaders.ts), so the server matches that URL itself, to the route that lives there: the index page next to the layout. runRouteGuards (middleware/request-handler/handlers/loader-handler.ts) then runs every other blocking loader of that route before the requested one. So the loaders of that index page run, and can answer, when the client only wanted the layout's data.
- A
redirectthrown by the index page's loader becomes the answer to the layout loader's request, and the client follows it. In the reproduction, clicking a<Link>to/about/on/home/ends on/home/, the redirect target of the page at/. The router navigates on the client; the page is not reloaded. - A page loader that does not redirect still runs once per layout-loader request on each navigation. In an app of mine with a data-heavy page at such a path, one sidebar click made twice the backend calls on rc.4 that it made on rc.0.
It is new in rc.4. rc.0 ran only the loaders listed before the requested one (runBlockingLoadersBeforeTarget), and a layout's loaders are listed before the page's, so the page's loaders never ran for them. ace029ca8 ("run every blocking loader before a q-loader target") changed that; its message says "Route module exports are sorted alphabetically in production, so a guard whose name sorted after the target was skipped and leaked its data". As I read it, that concerns loaders in the same module as the target. It also made the loaders of modules below the target guards of it.
I read #8850 and #8859: ignoring the page-path header outside dev is deliberate, and so is running a q-loader request through the route at its URL. Two things still differ from that:
- The router already keeps the index page's handlers out of it:
guardPageHandlerForLoader(resolve-request-handlers-core.ts) skips them for a loader request that does not name a loader of that module, and anonGetredirect does not bounce here. The page's loaders get no such treatment. - A loader cannot do it for itself with public API.
internalRequest === 'loader'is true both when the loader's own data is requested and when it only runs as a guard for another loader's request, and which loader was asked for is not exposed (my app reads the internal@loaderIdsharedMapkey).
It does not show in vite dev, where the client asks for the layout loader at the destination page's path, so it is only found after a build.
Expected
Clicking About ends on /about/, as on rc.0, in dev and with strictLoaders: false.
One way to get there: for a loader request, run as guards the other blocking loaders of the module that declares the requested loader (in any export order, which is what ace029ca8 fixes) and all blocking loaders of the modules above it, but not the loaders of a module below it. moduleHasLoader already answers "does this module declare the requested loader". The lower loaders do not guard the layout's data today anyway: check.sh shows the same loader answered with its data at /home/, where the index page's loader does not run. If running every loader is intended, exposing which loader a request asks for to the guards would let apps skip their own guard runs.
Reproduction
https://github.com/ixcans/qwik-layout-loader-guard-repro
Steps to reproduce
git clone https://github.com/ixcans/qwik-layout-loader-guard-repro
cd qwik-layout-loader-guard-repro
npm install
npm run build
./check.sh # no browser
npm run preview # then open /home/ and click "About"
check.sh asks for the root layout's loader the way the client does after a navigation:
root layout loader, requested at "/" (where the client requests it when you click a link):
GET /q-loader-layout-data.ewze0u.json
-> [5,[0,"r",0,"/home/"]]
src/routes/index.tsx's useLanding ran 1 time(s) for this request
the same loader, requested at "/home/":
GET /home/q-loader-layout-data.ewze0u.json
-> [5,[0,"d",5,[0,"ok",3,2]]]
REPRODUCED: the layout loader was answered with the redirect of the index page's loader.
What I ran (click About on /home/, headless Chromium):
| build | page ends on | useLanding runs |
|---|---|---|
2.0.0-rc.4, production |
/home/ |
2 |
main at bbcccb5 (pkg.pr.new preview build), production |
/home/ |
2 |
2.0.0-rc.4, production, qwikRouter({ strictLoaders: false }) |
/about/ |
0 |
2.0.0-rc.4, vite --mode ssr |
/about/ |
0 |
2.0.0-rc.0, production |
/about/ |
0 |
2.0.0-rc.4, production, the redirect in onGet instead of a loader |
/about/ |
handler not run |
2.0.0-rc.4, production, useLanding without the redirect |
/about/ |
1 |
2.0.0-rc.0, production, useLanding without the redirect |
/about/ |
0 |
A nested layout behaves the same on rc.4: with a loader in src/routes/app/layout.tsx and a redirecting loader in src/routes/app/index.tsx, the layout's loader is requested at /app/ and answered with the redirect (a variant I ran locally, not in the repository).
System Info
@qwik.dev/router 2.0.0-rc.4
@qwik.dev/core 2.0.0-rc.4
vite 8.2.4
Node 26.13.0, npm 11.20.0
Linux arm64, Chromium 157 (headless, Playwright)
Additional Information
Workarounds I know of: qwikRouter({ strictLoaders: false }) (the page path is sent; see #8859 for its trade-off with search params), the redirect in onGet, or an early return in each affected loader when the request names another loader, which needs the internal key above.
- Dominant language
- TypeScript
- Stars
- 22.1k
- Forks
- 1.4k
- Avg merge
- 2d 17h
- Merged PRs (30d)
- 99
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- Ships a Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from QwikDev/qwik
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Maintainers usually reply within 1 day
-
[🐞] SSG: "Qwik Router SSG was skipped" warning on every SSG build, while pages are prerenderedPossibly taken @itsnjstyle27 claimed this 2 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
docs router V2
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
-
DX enhancement V2
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
QwikDev/qwik#8480 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 52/100
Maintainers usually reply within 1 day
Similar issues
-
area:frontend kind:bug priority:p2 ready
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
handsontable/hyperformula#1879 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
RunestoneInteractive/rs#1661 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
capricorn86/happy-dom#2594 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day