Skip to content

Commit a2da3f2

Browse files
fix: correctly handle client-side vs server-side redirects with rewrites (#5330)
fixes #5324
1 parent ac1fadc commit a2da3f2

File tree

6 files changed

+97
-6
lines changed

6 files changed

+97
-6
lines changed

e2e/react-start/custom-basepath/src/routeTree.gen.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import { Route as LogoutRouteImport } from './routes/logout'
1515
import { Route as DeferredRouteImport } from './routes/deferred'
1616
import { Route as IndexRouteImport } from './routes/index'
1717
import { Route as UsersIndexRouteImport } from './routes/users.index'
18+
import { Route as RedirectIndexRouteImport } from './routes/redirect/index'
1819
import { Route as PostsIndexRouteImport } from './routes/posts.index'
1920
import { Route as UsersUserIdRouteImport } from './routes/users.$userId'
21+
import { Route as RedirectThrowItRouteImport } from './routes/redirect/throw-it'
2022
import { Route as PostsPostIdRouteImport } from './routes/posts.$postId'
2123
import { Route as ApiUsersRouteImport } from './routes/api.users'
2224
import { Route as PostsPostIdDeepRouteImport } from './routes/posts_.$postId.deep'
@@ -52,6 +54,11 @@ const UsersIndexRoute = UsersIndexRouteImport.update({
5254
path: '/',
5355
getParentRoute: () => UsersRoute,
5456
} as any)
57+
const RedirectIndexRoute = RedirectIndexRouteImport.update({
58+
id: '/redirect/',
59+
path: '/redirect/',
60+
getParentRoute: () => rootRouteImport,
61+
} as any)
5562
const PostsIndexRoute = PostsIndexRouteImport.update({
5663
id: '/',
5764
path: '/',
@@ -62,6 +69,11 @@ const UsersUserIdRoute = UsersUserIdRouteImport.update({
6269
path: '/$userId',
6370
getParentRoute: () => UsersRoute,
6471
} as any)
72+
const RedirectThrowItRoute = RedirectThrowItRouteImport.update({
73+
id: '/redirect/throw-it',
74+
path: '/redirect/throw-it',
75+
getParentRoute: () => rootRouteImport,
76+
} as any)
6577
const PostsPostIdRoute = PostsPostIdRouteImport.update({
6678
id: '/$postId',
6779
path: '/$postId',
@@ -91,8 +103,10 @@ export interface FileRoutesByFullPath {
91103
'/users': typeof UsersRouteWithChildren
92104
'/api/users': typeof ApiUsersRouteWithChildren
93105
'/posts/$postId': typeof PostsPostIdRoute
106+
'/redirect/throw-it': typeof RedirectThrowItRoute
94107
'/users/$userId': typeof UsersUserIdRoute
95108
'/posts/': typeof PostsIndexRoute
109+
'/redirect': typeof RedirectIndexRoute
96110
'/users/': typeof UsersIndexRoute
97111
'/api/users/$id': typeof ApiUsersIdRoute
98112
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
@@ -103,8 +117,10 @@ export interface FileRoutesByTo {
103117
'/logout': typeof LogoutRoute
104118
'/api/users': typeof ApiUsersRouteWithChildren
105119
'/posts/$postId': typeof PostsPostIdRoute
120+
'/redirect/throw-it': typeof RedirectThrowItRoute
106121
'/users/$userId': typeof UsersUserIdRoute
107122
'/posts': typeof PostsIndexRoute
123+
'/redirect': typeof RedirectIndexRoute
108124
'/users': typeof UsersIndexRoute
109125
'/api/users/$id': typeof ApiUsersIdRoute
110126
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
@@ -118,8 +134,10 @@ export interface FileRoutesById {
118134
'/users': typeof UsersRouteWithChildren
119135
'/api/users': typeof ApiUsersRouteWithChildren
120136
'/posts/$postId': typeof PostsPostIdRoute
137+
'/redirect/throw-it': typeof RedirectThrowItRoute
121138
'/users/$userId': typeof UsersUserIdRoute
122139
'/posts/': typeof PostsIndexRoute
140+
'/redirect/': typeof RedirectIndexRoute
123141
'/users/': typeof UsersIndexRoute
124142
'/api/users/$id': typeof ApiUsersIdRoute
125143
'/posts_/$postId/deep': typeof PostsPostIdDeepRoute
@@ -134,8 +152,10 @@ export interface FileRouteTypes {
134152
| '/users'
135153
| '/api/users'
136154
| '/posts/$postId'
155+
| '/redirect/throw-it'
137156
| '/users/$userId'
138157
| '/posts/'
158+
| '/redirect'
139159
| '/users/'
140160
| '/api/users/$id'
141161
| '/posts/$postId/deep'
@@ -146,8 +166,10 @@ export interface FileRouteTypes {
146166
| '/logout'
147167
| '/api/users'
148168
| '/posts/$postId'
169+
| '/redirect/throw-it'
149170
| '/users/$userId'
150171
| '/posts'
172+
| '/redirect'
151173
| '/users'
152174
| '/api/users/$id'
153175
| '/posts/$postId/deep'
@@ -160,8 +182,10 @@ export interface FileRouteTypes {
160182
| '/users'
161183
| '/api/users'
162184
| '/posts/$postId'
185+
| '/redirect/throw-it'
163186
| '/users/$userId'
164187
| '/posts/'
188+
| '/redirect/'
165189
| '/users/'
166190
| '/api/users/$id'
167191
| '/posts_/$postId/deep'
@@ -174,6 +198,8 @@ export interface RootRouteChildren {
174198
PostsRoute: typeof PostsRouteWithChildren
175199
UsersRoute: typeof UsersRouteWithChildren
176200
ApiUsersRoute: typeof ApiUsersRouteWithChildren
201+
RedirectThrowItRoute: typeof RedirectThrowItRoute
202+
RedirectIndexRoute: typeof RedirectIndexRoute
177203
PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute
178204
}
179205

@@ -221,6 +247,13 @@ declare module '@tanstack/react-router' {
221247
preLoaderRoute: typeof UsersIndexRouteImport
222248
parentRoute: typeof UsersRoute
223249
}
250+
'/redirect/': {
251+
id: '/redirect/'
252+
path: '/redirect'
253+
fullPath: '/redirect'
254+
preLoaderRoute: typeof RedirectIndexRouteImport
255+
parentRoute: typeof rootRouteImport
256+
}
224257
'/posts/': {
225258
id: '/posts/'
226259
path: '/'
@@ -235,6 +268,13 @@ declare module '@tanstack/react-router' {
235268
preLoaderRoute: typeof UsersUserIdRouteImport
236269
parentRoute: typeof UsersRoute
237270
}
271+
'/redirect/throw-it': {
272+
id: '/redirect/throw-it'
273+
path: '/redirect/throw-it'
274+
fullPath: '/redirect/throw-it'
275+
preLoaderRoute: typeof RedirectThrowItRouteImport
276+
parentRoute: typeof rootRouteImport
277+
}
238278
'/posts/$postId': {
239279
id: '/posts/$postId'
240280
path: '/$postId'
@@ -309,6 +349,8 @@ const rootRouteChildren: RootRouteChildren = {
309349
PostsRoute: PostsRouteWithChildren,
310350
UsersRoute: UsersRouteWithChildren,
311351
ApiUsersRoute: ApiUsersRouteWithChildren,
352+
RedirectThrowItRoute: RedirectThrowItRoute,
353+
RedirectIndexRoute: RedirectIndexRoute,
312354
PostsPostIdDeepRoute: PostsPostIdDeepRoute,
313355
}
314356
export const routeTree = rootRouteImport

e2e/react-start/custom-basepath/src/routes/posts.$postId.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function PostComponent() {
2121
const post = Route.useLoaderData()
2222

2323
return (
24-
<div className="space-y-2">
24+
<div className="space-y-2" data-testid="post-view">
2525
<h4 className="text-xl font-bold underline">{post.title}</h4>
2626
<div className="text-sm">{post.body}</div>
2727
<Link
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Link, createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/redirect/')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return (
9+
<div className="p-2 flex gap-2">
10+
<Link to="/redirect/throw-it">
11+
<div data-testid="link-to-throw-it">Throw It</div>
12+
</Link>
13+
</div>
14+
)
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { createFileRoute, redirect } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/redirect/throw-it')({
4+
beforeLoad: () => {
5+
throw redirect({
6+
to: '/posts/$postId',
7+
params: { postId: '1' },
8+
})
9+
},
10+
})

e2e/react-start/custom-basepath/tests/navigation.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,28 @@ test('Server function URLs correctly include app basepath', async ({
4545
'/custom/basepath/_serverFn/src_routes_logout_tsx--logoutFn_createServerFn_handler',
4646
)
4747
})
48+
49+
test('client-side redirect', async ({ page, baseURL }) => {
50+
await page.goto('/redirect')
51+
await page.getByTestId('link-to-throw-it').click()
52+
await page.waitForLoadState('networkidle')
53+
54+
expect(await page.getByTestId('post-view').isVisible()).toBe(true)
55+
expect(page.url()).toBe(`${baseURL}/posts/1`)
56+
})
57+
58+
test('server-side redirect', async ({ page, baseURL }) => {
59+
await page.goto('/redirect/throw-it')
60+
await page.waitForLoadState('networkidle')
61+
62+
expect(await page.getByTestId('post-view').isVisible()).toBe(true)
63+
expect(page.url()).toBe(`${baseURL}/posts/1`)
64+
65+
// do not follow redirects since we want to test the Location header
66+
await page.request
67+
.get('/redirect/throw-it', { maxRedirects: 0 })
68+
.then((res) => {
69+
const headers = new Headers(res.headers())
70+
expect(headers.get('location')).toBe('/custom/basepath/posts/1')
71+
})
72+
})

packages/router-core/src/router.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2230,18 +2230,17 @@ export class RouterCore<
22302230

22312231
resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
22322232
if (!redirect.options.href) {
2233-
let href = this.buildLocation(redirect.options).url
2233+
const location = this.buildLocation(redirect.options)
2234+
let href = location.url
22342235
if (this.origin && href.startsWith(this.origin)) {
22352236
href = href.replace(this.origin, '') || '/'
22362237
}
2237-
redirect.options.href = href
2238-
redirect.headers.set('Location', redirect.options.href)
2238+
redirect.options.href = location.href
2239+
redirect.headers.set('Location', href)
22392240
}
2240-
22412241
if (!redirect.headers.get('Location')) {
22422242
redirect.headers.set('Location', redirect.options.href)
22432243
}
2244-
22452244
return redirect
22462245
}
22472246

0 commit comments

Comments
 (0)