[🐞] 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

Open
#9,387 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@bercedev is already working on this.

Since Nov 7, 2026.

  • #9389 by @bercedev — open

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
46/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active

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 redirect thrown 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:

  1. 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 an onGet redirect does not bounce here. The page's loaders get no such treatment.
  2. 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 @loaderId sharedMap key).

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

Open in Codespaces

Starts the project's dev container in your browser, under your own GitHub account.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from QwikDev/qwik

All issues in QwikDev/qwik

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.