QwikDev / QwikDev/qwik

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

Open
#8,859 4 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

V2
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 → sends X-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

Open the contributing guide

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.

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.