[🐞] [V2] [beta-38] [router] In strictLoaders mode, production builds send no route-path header — a root/parent-layout routeLoader$ is resolved at its mount path after a SPA action, hitting that route's onGet (e.g. a redirect)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 22.1k
- Forks
- 1.4k
- Avg merge
- 1d 10h
- Merged PRs (30d)
- 52
Description
Which component is affected?
Qwik Router (@qwik.dev/router)
Version
@qwik.dev/router@2.0.0-beta.38 (latest at time of writing).
Summary
In strict-loaders mode (the default) + a production build, the client
never sends a route-path header when it re-fetches a route loader. As a result, a
routeLoader$ mounted on the root layout is resolved server-side at its mount
path / instead of the currently active route. The /q-loader-*.json sub-request
then goes through the full middleware chain of / — so if the / route has an
onGet that calls redirect(), the loader fetch itself is redirected, and/or the
loader executes with the wrong url.pathname.
This works in dev and breaks only in a production build, with no error at build time.
Root cause (traced in the installed code)
In chunks/head.qwik.mjs, fetchRouteLoaderData decides whether to tell the server
the active route when it differs from the loader's mount path:
// head.qwik.mjs (beta.38), ~line 632
if (pageUrl && pageUrl.pathname !== pathBase) {
if (!globalThis.__STRICT_LOADERS__) {
headers[FULLPATH_HEADER] = pageUrl.pathname; // "X-Qwik-fullpath"
} else if (isDev) {
headers[ROUTE_PATH_HEADER] = pageUrl.pathname; // "X-Qwik-route-path"
}
}
- Non-strict → always sends
X-Qwik-fullpath. - Strict and
isDev→ sendsX-Qwik-route-path. - Strict and production (
isDev === false) → sends neither header.
With no header, the server cannot recover the active route
(resolveValidInternalFullPathname("/", null) → undefined) and falls back to the
mount base /. The loader sub-request is then processed as if it belonged to /.
This matches the beta.38 release note: "q-loader requests run through the full page
middleware chain and handles unfollowed HTTP redirects."
Confirmed on a real production deployment
On a production Cloudflare Pages deployment (strict mode temporarily restored to
verify), submitting an ordinary SPA form action (routeAction$, no reloadDocument)
on /ja/ produced this network trace:
[302] /q-loader-gOdvj5T9iC0.vl5gpo.json → /fr/
gOdvj5T9iC0 is the hash of a root-layout routeLoader$ that reads url.pathname.
The root route (/) has an onGet that redirects to a locale-specific path
(/fr/, /en/, or /ja/ depending on cf-ipcountry/Accept-Language) — exactly
the "sub-request intercepted by a navigation handler" scenario the release notes
warn about.
Minimal reproduction
https://github.com/tidiview/qwik-repro-strictloaders
Mirrors a real app's route-tree shape: a root layout loader reading url.pathname,
several sibling route trees with their own loaders including a catch-all, a /
route with an onGet redirect, and a plain routeAction$ form with no
reloadDocument. See the repo's README for exact repro steps.
Submitting the SPA action refetches the root loader at /q-loader-*.json (no route
prefix), and the loader computes pathname: "/" instead of the active route's path
— verifiable in the raw JSON response body.
(Note for anyone reproducing on Windows: building this from a deeply nested path
can trigger an unrelated, build-time-only bug where the router's route→loader
manifest (_R) never gets populated at all — due to a path-normalization mismatch
between addRouteLoaderHash (normalizes) and replaceLoaderPlaceholders (does not)
in lib/vite/index.mjs. That masks this issue entirely rather than reproducing it.
Building from a short path avoids it.)
Expected
Either the route-path header is sent in strict + production too (so a root/parent
loader resolves against the active route), or loader sub-requests are not subjected
to a page route's navigation handlers (onGet redirects).
Workaround
qwikRouter({ strictLoaders: false }) — this makes the client send X-Qwik-fullpath
in production, so the loader resolves against the active route. Trade-off: in
non-strict mode search params are forwarded to all loaders (refetch on change);
restorable per-loader with { search: [] }.
Contributor 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.
Research direction
Start in chunks/head.qwik.mjs at fetchRouteLoaderData and inspect how strict-loaders production requests set route headers. Reproduce the issue with the linked minimal repository using a production build and an SPA routeAction$. Done means the root loader receives the active route path and its q-loader request is not redirected through the root route's onGet.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- full-stack, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100