Skip to content

Commit 91d1e38

Browse files
frenzzyschiller-manuelautofix-ci[bot]
authored
fix: follow server fn redirects when submitting formdata (#4519)
Co-authored-by: Manuel Schiller <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 1d5c8a4 commit 91d1e38

File tree

6 files changed

+169
-1
lines changed

6 files changed

+169
-1
lines changed

e2e/react-start/server-functions/src/routeTree.gen.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserv
2222
import { Route as ConsistentRouteImport } from './routes/consistent'
2323
import { Route as AbortSignalRouteImport } from './routes/abort-signal'
2424
import { Route as IndexRouteImport } from './routes/index'
25+
import { Route as FormdataRedirectIndexRouteImport } from './routes/formdata-redirect/index'
2526
import { Route as CookiesIndexRouteImport } from './routes/cookies/index'
2627
import { Route as CookiesSetRouteImport } from './routes/cookies/set'
28+
import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name'
2729

2830
const SubmitPostFormdataRoute = SubmitPostFormdataRouteImport.update({
2931
id: '/submit-post-formdata',
@@ -90,6 +92,11 @@ const IndexRoute = IndexRouteImport.update({
9092
path: '/',
9193
getParentRoute: () => rootRouteImport,
9294
} as any)
95+
const FormdataRedirectIndexRoute = FormdataRedirectIndexRouteImport.update({
96+
id: '/formdata-redirect/',
97+
path: '/formdata-redirect/',
98+
getParentRoute: () => rootRouteImport,
99+
} as any)
93100
const CookiesIndexRoute = CookiesIndexRouteImport.update({
94101
id: '/cookies/',
95102
path: '/cookies/',
@@ -100,6 +107,12 @@ const CookiesSetRoute = CookiesSetRouteImport.update({
100107
path: '/cookies/set',
101108
getParentRoute: () => rootRouteImport,
102109
} as any)
110+
const FormdataRedirectTargetNameRoute =
111+
FormdataRedirectTargetNameRouteImport.update({
112+
id: '/formdata-redirect/target/$name',
113+
path: '/formdata-redirect/target/$name',
114+
getParentRoute: () => rootRouteImport,
115+
} as any)
103116

104117
export interface FileRoutesByFullPath {
105118
'/': typeof IndexRoute
@@ -117,6 +130,8 @@ export interface FileRoutesByFullPath {
117130
'/submit-post-formdata': typeof SubmitPostFormdataRoute
118131
'/cookies/set': typeof CookiesSetRoute
119132
'/cookies': typeof CookiesIndexRoute
133+
'/formdata-redirect': typeof FormdataRedirectIndexRoute
134+
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
120135
}
121136
export interface FileRoutesByTo {
122137
'/': typeof IndexRoute
@@ -134,6 +149,8 @@ export interface FileRoutesByTo {
134149
'/submit-post-formdata': typeof SubmitPostFormdataRoute
135150
'/cookies/set': typeof CookiesSetRoute
136151
'/cookies': typeof CookiesIndexRoute
152+
'/formdata-redirect': typeof FormdataRedirectIndexRoute
153+
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
137154
}
138155
export interface FileRoutesById {
139156
__root__: typeof rootRouteImport
@@ -152,6 +169,8 @@ export interface FileRoutesById {
152169
'/submit-post-formdata': typeof SubmitPostFormdataRoute
153170
'/cookies/set': typeof CookiesSetRoute
154171
'/cookies/': typeof CookiesIndexRoute
172+
'/formdata-redirect/': typeof FormdataRedirectIndexRoute
173+
'/formdata-redirect/target/$name': typeof FormdataRedirectTargetNameRoute
155174
}
156175
export interface FileRouteTypes {
157176
fileRoutesByFullPath: FileRoutesByFullPath
@@ -171,6 +190,8 @@ export interface FileRouteTypes {
171190
| '/submit-post-formdata'
172191
| '/cookies/set'
173192
| '/cookies'
193+
| '/formdata-redirect'
194+
| '/formdata-redirect/target/$name'
174195
fileRoutesByTo: FileRoutesByTo
175196
to:
176197
| '/'
@@ -188,6 +209,8 @@ export interface FileRouteTypes {
188209
| '/submit-post-formdata'
189210
| '/cookies/set'
190211
| '/cookies'
212+
| '/formdata-redirect'
213+
| '/formdata-redirect/target/$name'
191214
id:
192215
| '__root__'
193216
| '/'
@@ -205,6 +228,8 @@ export interface FileRouteTypes {
205228
| '/submit-post-formdata'
206229
| '/cookies/set'
207230
| '/cookies/'
231+
| '/formdata-redirect/'
232+
| '/formdata-redirect/target/$name'
208233
fileRoutesById: FileRoutesById
209234
}
210235
export interface RootRouteChildren {
@@ -223,6 +248,8 @@ export interface RootRouteChildren {
223248
SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute
224249
CookiesSetRoute: typeof CookiesSetRoute
225250
CookiesIndexRoute: typeof CookiesIndexRoute
251+
FormdataRedirectIndexRoute: typeof FormdataRedirectIndexRoute
252+
FormdataRedirectTargetNameRoute: typeof FormdataRedirectTargetNameRoute
226253
}
227254

228255
declare module '@tanstack/react-router' {
@@ -318,6 +345,13 @@ declare module '@tanstack/react-router' {
318345
preLoaderRoute: typeof IndexRouteImport
319346
parentRoute: typeof rootRouteImport
320347
}
348+
'/formdata-redirect/': {
349+
id: '/formdata-redirect/'
350+
path: '/formdata-redirect'
351+
fullPath: '/formdata-redirect'
352+
preLoaderRoute: typeof FormdataRedirectIndexRouteImport
353+
parentRoute: typeof rootRouteImport
354+
}
321355
'/cookies/': {
322356
id: '/cookies/'
323357
path: '/cookies'
@@ -332,6 +366,13 @@ declare module '@tanstack/react-router' {
332366
preLoaderRoute: typeof CookiesSetRouteImport
333367
parentRoute: typeof rootRouteImport
334368
}
369+
'/formdata-redirect/target/$name': {
370+
id: '/formdata-redirect/target/$name'
371+
path: '/formdata-redirect/target/$name'
372+
fullPath: '/formdata-redirect/target/$name'
373+
preLoaderRoute: typeof FormdataRedirectTargetNameRouteImport
374+
parentRoute: typeof rootRouteImport
375+
}
335376
}
336377
}
337378

@@ -351,6 +392,8 @@ const rootRouteChildren: RootRouteChildren = {
351392
SubmitPostFormdataRoute: SubmitPostFormdataRoute,
352393
CookiesSetRoute: CookiesSetRoute,
353394
CookiesIndexRoute: CookiesIndexRoute,
395+
FormdataRedirectIndexRoute: FormdataRedirectIndexRoute,
396+
FormdataRedirectTargetNameRoute: FormdataRedirectTargetNameRoute,
354397
}
355398
export const routeTree = rootRouteImport
356399
._addFileChildren(rootRouteChildren)
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { createFileRoute, redirect } from '@tanstack/react-router'
2+
import { createServerFn, useServerFn } from '@tanstack/react-start'
3+
import { z } from 'zod'
4+
5+
export const Route = createFileRoute('/formdata-redirect/')({
6+
component: SubmitPostFormDataFn,
7+
validateSearch: z.object({
8+
mode: z.union([z.literal('js'), z.literal('no-js')]).default('js'),
9+
}),
10+
})
11+
12+
const testValues = {
13+
name: 'Sean',
14+
}
15+
16+
export const greetUser = createServerFn({ method: 'POST' })
17+
.validator((data: FormData) => {
18+
if (!(data instanceof FormData)) {
19+
throw new Error('Invalid! FormData is required')
20+
}
21+
const name = data.get('name')
22+
23+
if (!name) {
24+
throw new Error('Name is required')
25+
}
26+
27+
return {
28+
name: name.toString(),
29+
}
30+
})
31+
.handler(({ data: { name } }) => {
32+
throw redirect({ to: '/formdata-redirect/target/$name', params: { name } })
33+
})
34+
35+
function SubmitPostFormDataFn() {
36+
const mode = Route.useSearch({ select: (search) => search.mode })
37+
const greetUserFn = useServerFn(greetUser)
38+
return (
39+
<div className="p-2 m-2 grid gap-2">
40+
<h3>Submit POST FormData Fn Call</h3>
41+
<div className="overflow-y-auto">
42+
It should return redirect to /formdata-redirect/target/{testValues.name}{' '}
43+
and greet the user with their name:
44+
<code>
45+
<pre data-testid="expected-submit-post-formdata-server-fn-result">
46+
{testValues.name}
47+
</pre>
48+
</code>
49+
</div>
50+
<form
51+
className="flex flex-col gap-2"
52+
data-testid="submit-post-formdata-form"
53+
method="POST"
54+
action={greetUser.url}
55+
onSubmit={async (evt) => {
56+
if (mode === 'js') {
57+
evt.preventDefault()
58+
const data = new FormData(evt.currentTarget)
59+
await greetUserFn({ data })
60+
}
61+
}}
62+
>
63+
<input type="text" name="name" defaultValue={testValues.name} />
64+
<button
65+
type="submit"
66+
data-testid="test-submit-post-formdata-fn-calls-btn"
67+
className="rounded-md bg-white px-2.5 py-1.5 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
68+
>
69+
Submit
70+
</button>
71+
</form>
72+
</div>
73+
)
74+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { createFileRoute } from '@tanstack/react-router'
2+
3+
export const Route = createFileRoute('/formdata-redirect/target/$name')({
4+
component: RouteComponent,
5+
})
6+
7+
function RouteComponent() {
8+
return (
9+
<div data-testid="formdata-redirect-target">
10+
Hello{' '}
11+
<span data-testid="formdata-redirect-target-name">
12+
{Route.useParams().name}
13+
</span>
14+
!
15+
</div>
16+
)
17+
}

e2e/react-start/server-functions/src/routes/index.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ function Home() {
7272
<li>
7373
<Link to="/raw-response">server function returns raw response</Link>
7474
</li>
75+
<li>
76+
<Link to="/formdata-redirect" search={{ mode: 'js' }}>
77+
server function redirects when FormData is submitted (via JS)
78+
</Link>
79+
</li>
80+
<li>
81+
<Link to="/formdata-redirect" search={{ mode: 'no-js' }}>
82+
server function redirects when FormData is submitted (via no-JS)
83+
</Link>
84+
</li>
7585
</ul>
7686
</div>
7787
)

e2e/react-start/server-functions/tests/server-functions.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,3 +315,27 @@ test('raw response', async ({ page }) => {
315315

316316
await expect(page.getByTestId('response')).toContainText(expectedValue)
317317
})
318+
;[{ mode: 'js' }, { mode: 'no-js' }].forEach(({ mode }) => {
319+
test(`Server function can redirect when sending formdata: mode = ${mode}`, async ({
320+
page,
321+
}) => {
322+
await page.goto('/formdata-redirect?mode=' + mode)
323+
324+
await page.waitForLoadState('networkidle')
325+
const expected =
326+
(await page
327+
.getByTestId('expected-submit-post-formdata-server-fn-result')
328+
.textContent()) || ''
329+
expect(expected).not.toBe('')
330+
331+
await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click()
332+
333+
await page.waitForLoadState('networkidle')
334+
335+
await expect(
336+
page.getByTestId('formdata-redirect-target-name'),
337+
).toContainText(expected)
338+
339+
expect(page.url().endsWith(`/formdata-redirect/target/${expected}`))
340+
})
341+
})

packages/start-server-functions-fetcher/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ export async function serverFnFetcher(
2525

2626
// Arrange the headers
2727
const headers = new Headers({
28+
'x-tsr-redirect': 'manual',
2829
...(type === 'payload'
2930
? {
3031
'content-type': 'application/json',
3132
accept: 'application/json',
32-
'x-tsr-redirect': 'manual',
3333
}
3434
: {}),
3535
...(first.headers instanceof Headers

0 commit comments

Comments
 (0)