From deb48d8c7491e738c4c614dbc8ea7f495a50e7bb Mon Sep 17 00:00:00 2001 From: Sheraff Date: Sun, 25 Jan 2026 18:58:15 +0100 Subject: [PATCH] refactor(router-core): throw redirect w/ a pre-build location doesn't need to re-parse from string --- packages/router-core/src/redirect.ts | 26 +++++++++++++++++++++++--- packages/router-core/src/router.ts | 12 ++++++++---- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/router-core/src/redirect.ts b/packages/router-core/src/redirect.ts index 31aaa2f32c7..4cb1e0f5e22 100644 --- a/packages/router-core/src/redirect.ts +++ b/packages/router-core/src/redirect.ts @@ -1,6 +1,7 @@ import { SAFE_URL_PROTOCOLS, isDangerousProtocol } from './utils' import type { NavigateOptions } from './link' import type { AnyRouter, RegisteredRouter } from './router' +import type { ParsedLocation } from './location' export type AnyRedirect = Redirect @@ -14,7 +15,13 @@ export type Redirect< TMaskFrom extends string = TFrom, TMaskTo extends string = '.', > = Response & { - options: NavigateOptions + options: NavigateOptions & { + /** + * @internal + * A **trusted** built location that can be used to redirect to. + */ + _builtLocation?: ParsedLocation + } redirectHandled?: boolean } @@ -45,6 +52,11 @@ export type RedirectOptions< * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RedirectType#headers-property) */ headers?: HeadersInit + /** + * @internal + * A **trusted** built location that can be used to redirect to. + */ + _builtLocation?: ParsedLocation } & NavigateOptions export type ResolvedRedirect< @@ -113,13 +125,21 @@ export function redirect< opts.statusCode = opts.statusCode || opts.code || 307 // Block dangerous protocols in redirect href - if (typeof opts.href === 'string' && isDangerousProtocol(opts.href)) { + if ( + !opts._builtLocation && + typeof opts.href === 'string' && + isDangerousProtocol(opts.href) + ) { throw new Error( `Redirect blocked: unsafe protocol in href "${opts.href}". Only ${SAFE_URL_PROTOCOLS.join(', ')} protocols are allowed.`, ) } - if (!opts.reloadDocument && typeof opts.href === 'string') { + if ( + !opts._builtLocation && + !opts.reloadDocument && + typeof opts.href === 'string' + ) { try { new URL(opts.href) opts.reloadDocument = true diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts index 63044b141f2..20041fd05f7 100644 --- a/packages/router-core/src/router.ts +++ b/packages/router-core/src/router.ts @@ -2292,8 +2292,11 @@ export class RouterCore< // always uses this.origin when constructing URLs if (this.latestLocation.publicHref !== nextLocation.publicHref) { const href = this.getParsedLocationHref(nextLocation) - - throw redirect({ href }) + if (nextLocation.external) { + throw redirect({ href }) + } else { + throw redirect({ href, _builtLocation: nextLocation }) + } } } @@ -2622,8 +2625,9 @@ export class RouterCore< resolveRedirect = (redirect: AnyRedirect): AnyRedirect => { const locationHeader = redirect.headers.get('Location') - if (!redirect.options.href) { - const location = this.buildLocation(redirect.options) + if (!redirect.options.href || redirect.options._builtLocation) { + const location = + redirect.options._builtLocation ?? this.buildLocation(redirect.options) const href = this.getParsedLocationHref(location) redirect.options.href = href redirect.headers.set('Location', href)