From c9c673a297c4073921d22b29b54949c0997363c1 Mon Sep 17 00:00:00 2001 From: Eduard Fischer-Szava Date: Thu, 30 Apr 2026 00:25:07 +0200 Subject: [PATCH 1/2] fix(router): apply trailingSlash config before basepath redirect comparison on SSR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a basepath is configured (e.g. asepath: "/preview"), the SSR redirect check in eforeLoad compares latestLocation.publicHref (the raw incoming URL, e.g. /preview) against extLocation.publicHref (rebuilt via uildLocation). The problem: uildLocation runs the rewrite output ( ewriteBasepath) which joins basepath + "/" = /preview/, but never applies the router's railingSlash option to the result. With railingSlash: "never" (the default) the rebuilt publicHref is /preview/ while the incoming one is /preview, so they always differ and a spurious 308 redirect fires. Fix: after xecuteRewriteOutput produces the rewritten URL inside uildLocation, normalize the pathname component of publicHref according to his.options.trailingSlash before returning, the same way the fast (no-rewrite) path already does via esolvePath. - railingSlash: "never" (default) ΓÇô strip trailing slash ΓåÆ /preview matches incoming /preview, no redirect. - railingSlash: "always" ΓÇô ensure trailing slash ΓåÆ /preview/ ΓåÆ will correctly redirect if the user lands on /preview. - railingSlash: "preserve" ΓÇô leave the rewrite output unchanged (existing behaviour). Fixes #7291 --- packages/router-core/src/router.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index bd8a2898ec4..895e8504f2a 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2020,8 +2020,24 @@ export class RouterCore< publicHref = rewrittenUrl.href external = true } else { + // Apply trailingSlash config to the rewritten pathname so that the + // publicHref is canonical before it is compared to the incoming URL + // in the SSR redirect check. Without this, visiting the bare basepath + // (e.g. /preview) always produces publicHref = /preview/ from the + // rewrite output and triggers a spurious 308 redirect even when + // trailingSlash is 'never'. See: https://github.com/TanStack/router/issues/7291 + const trailingSlashOpt = this.options.trailingSlash ?? 'never' + let rewrittenPathname = rewrittenUrl.pathname + if (trailingSlashOpt === 'never') { + rewrittenPathname = trimPathRight(rewrittenPathname) + } else if ( + trailingSlashOpt === 'always' && + !rewrittenPathname.endsWith('/') + ) { + rewrittenPathname += '/' + } publicHref = - rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash + rewrittenPathname + rewrittenUrl.search + rewrittenUrl.hash } } else { // Fast path: no rewrite, skip URL construction entirely From 8854f4fe22afd0c9cc20f912d40256d172a231ec Mon Sep 17 00:00:00 2001 From: Eduard Fischer-Szava Date: Tue, 5 May 2026 23:54:15 +0200 Subject: [PATCH 2/2] chore: add changeset --- .changeset/fix-ssr-bare-basepath-redirect.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fix-ssr-bare-basepath-redirect.md diff --git a/.changeset/fix-ssr-bare-basepath-redirect.md b/.changeset/fix-ssr-bare-basepath-redirect.md new file mode 100644 index 00000000000..848f68cf221 --- /dev/null +++ b/.changeset/fix-ssr-bare-basepath-redirect.md @@ -0,0 +1,7 @@ +--- +'@tanstack/router-core': patch +--- + +fix(router): apply trailingSlash config to the rewritten basepath URL before the SSR redirect comparison. + +Fixes [#7291](https://github.com/TanStack/router/issues/7291): visiting a bare basepath URL (e.g. `/preview`) on the server always triggered a spurious `308` redirect to `/preview/` even when `trailingSlash` was `'never'` (the default), because the rewritten public href was synthesised from `URL` parsing — which always normalises to a trailing slash — and never reconciled with the user's `trailingSlash` config before being compared to the incoming request URL.