Which component is this bug report for?
Qwik Router
Describe the bug
On @qwik.dev/router@2.0.0-beta.40, when a routeLoader$ is declared in a layout.tsx that lives at a dynamic [param] segment, and the actual request being served is for a route nested one level deeper than that layout, the layout's own loader receives:
params — an empty object (missing the param the child route resolves correctly)
url.pathname — truncated to the layout's own segment (e.g. /x/ instead of /x/sub/)
request.url — also truncated the same way (not just a derived value — the raw Request itself)
...while the child route's own routeLoader$, running in the same request, sees the correct full params/url/request.url.
This is absent on 2.0.0-beta.35 (confirmed via bisection — identical repro, only the Qwik version pin changed). It is not adapter-specific — I reproduced it both on a custom Bun-based SSR adapter and on Qwik Router's own official node-server adapter running under plain Node.js, byte-for-byte identically. It does not reproduce under vite dev (live/unbundled SSR) — only in the built/production SSR bundle path both adapters share. It is also not limited to a literal first/"cold" request — it reproduces deterministically on repeated requests to an already-warm server process too; the only thing that avoids it is a client-side <Link>/useNavigate() transition, which never goes through this server-side document-request path at all.
Suspected regression source: beta.40's sole substantial change, the Vite 8 + Rolldown migration (#8909) — bisection confirms the bug is new in beta.40 relative to beta.35, and no other change of similar scope shipped in between.
Real-world impact: found while investigating a production app (a per-tenant dashboard) where this silently redirected users away from any deep-linked, bookmarked, or refreshed page — the entire nested-route tree was affected. The app reverted from beta.40 back to beta.35 because of this.
Related but distinct (same general subsystem/symptom family, different trigger — not duplicates): #8859 (a client-side SPA-action re-fetch missing a route-path header, landing a loader at / — different mechanism, pre-dates beta.40) and #8856 (merged in beta.39, touches getLoaderRequestEvent()'s per-loader RequestEvent field overriding — same code area, but scoped to a different, client-triggered path, and predates the Vite8/Rolldown migration so it can't be this regression).
Reproduction
https://github.com/ixcans/qwik-router-layout-params-repro
Scaffolded from the official Qwik CLI (npm create qwik@beta empty app) with zero added business logic — three small route files, 58 lines total:
// src/routes/[slug]/layout.tsx
import { Slot, component$ } from "@qwik.dev/core";
import { routeLoader$ } from "@qwik.dev/router";
export const useSlugParam = routeLoader$((ev) => {
console.error(
"[LAYOUT useSlugParam] params=%s pathname=%s request.url=%s",
JSON.stringify(ev.params), ev.url.pathname, ev.request.url,
);
return ev.params.slug ?? null;
});
export default component$(() => <Slot />);
// src/routes/[slug]/sub/index.tsx
import { component$ } from "@qwik.dev/core";
import { routeLoader$, useLocation } from "@qwik.dev/router";
import { useSlugParam } from "../layout";
export const useSubParam = routeLoader$((ev) => {
console.error(
"[CHILD useSubParam] params=%s pathname=%s request.url=%s",
JSON.stringify(ev.params), ev.url.pathname, ev.request.url,
);
return ev.params.slug ?? null;
});
export default component$(() => {
const layoutSlug = useSlugParam();
const childSlug = useSubParam();
const loc = useLocation();
return (
<div>
<div>layoutSlug: {JSON.stringify(layoutSlug.value)}</div>
<div>childSlug: {JSON.stringify(childSlug.value)}</div>
<div>useLocation().params: {JSON.stringify(loc.params)}</div>
</div>
);
});
npm install
npm run build
npm run build.preview
npm run preview -- --open=false
Then make a fresh request (curl, or a browser hard-navigation — not a client-side link click) to /x/sub/ (any literal value in place of x).
Steps to reproduce
- Clone https://github.com/ixcans/qwik-router-layout-params-repro
npm install && npm run build && npm run build.preview && npm run preview -- --open=false
curl http://localhost:<printed-port>/x/sub/
- Check the server's stderr for the two log lines
Expected behavior
Both loaders should observe the same, correct params/url.pathname/request.url for the request actually being served.
Actual behavior
[LAYOUT useSlugParam] params={} pathname=/x/ request.url=http://localhost:PORT/x/
[CHILD useSubParam] params={"slug":"x"} pathname=/x/sub/ request.url=http://localhost:PORT/x/sub/
Rendered page: layoutSlug: null, childSlug: "x". Requesting the bare /x/ (no nested sub) does not reproduce this — the layout loader sees correct params there. The corruption is specific to an ancestor layout resolving in the same request as a route nested deeper under it.
Confirmed absent on 2.0.0-beta.35 with the identical repro (only the version pin changed).
System Info
@qwik.dev/core: 2.0.0-beta.40
@qwik.dev/router: 2.0.0-beta.40
vite: 8.0.16
Tested on both a custom Bun-based SSR adapter and Qwik Router's official node-server adapter (Node.js), same result on both.
Additional information
No existing issue found covering this exact bug after a thorough search (issues, PRs, discussions, and every issue filed since beta.40 shipped) — happy to help narrow this down further if useful.
Which component is this bug report for?
Qwik Router
Describe the bug
On
@qwik.dev/router@2.0.0-beta.40, when arouteLoader$is declared in alayout.tsxthat lives at a dynamic[param]segment, and the actual request being served is for a route nested one level deeper than that layout, the layout's own loader receives:params— an empty object (missing the param the child route resolves correctly)url.pathname— truncated to the layout's own segment (e.g./x/instead of/x/sub/)request.url— also truncated the same way (not just a derived value — the rawRequestitself)...while the child route's own
routeLoader$, running in the same request, sees the correct fullparams/url/request.url.This is absent on
2.0.0-beta.35(confirmed via bisection — identical repro, only the Qwik version pin changed). It is not adapter-specific — I reproduced it both on a custom Bun-based SSR adapter and on Qwik Router's own officialnode-serveradapter running under plain Node.js, byte-for-byte identically. It does not reproduce undervite dev(live/unbundled SSR) — only in the built/production SSR bundle path both adapters share. It is also not limited to a literal first/"cold" request — it reproduces deterministically on repeated requests to an already-warm server process too; the only thing that avoids it is a client-side<Link>/useNavigate()transition, which never goes through this server-side document-request path at all.Suspected regression source:
beta.40's sole substantial change, the Vite 8 + Rolldown migration (#8909) — bisection confirms the bug is new inbeta.40relative tobeta.35, and no other change of similar scope shipped in between.Real-world impact: found while investigating a production app (a per-tenant dashboard) where this silently redirected users away from any deep-linked, bookmarked, or refreshed page — the entire nested-route tree was affected. The app reverted from
beta.40back tobeta.35because of this.Related but distinct (same general subsystem/symptom family, different trigger — not duplicates): #8859 (a client-side SPA-action re-fetch missing a route-path header, landing a loader at
/— different mechanism, pre-dates beta.40) and #8856 (merged in beta.39, touchesgetLoaderRequestEvent()'s per-loaderRequestEventfield overriding — same code area, but scoped to a different, client-triggered path, and predates the Vite8/Rolldown migration so it can't be this regression).Reproduction
https://github.com/ixcans/qwik-router-layout-params-repro
Scaffolded from the official Qwik CLI (
npm create qwik@beta empty app) with zero added business logic — three small route files, 58 lines total:Then make a fresh request (curl, or a browser hard-navigation — not a client-side link click) to
/x/sub/(any literal value in place ofx).Steps to reproduce
npm install && npm run build && npm run build.preview && npm run preview -- --open=falsecurl http://localhost:<printed-port>/x/sub/Expected behavior
Both loaders should observe the same, correct
params/url.pathname/request.urlfor the request actually being served.Actual behavior
Rendered page:
layoutSlug: null,childSlug: "x". Requesting the bare/x/(no nestedsub) does not reproduce this — the layout loader sees correct params there. The corruption is specific to an ancestor layout resolving in the same request as a route nested deeper under it.Confirmed absent on
2.0.0-beta.35with the identical repro (only the version pin changed).System Info
Additional information
No existing issue found covering this exact bug after a thorough search (issues, PRs, discussions, and every issue filed since beta.40 shipped) — happy to help narrow this down further if useful.