Skip to content

Commit

Permalink
fix(start): server functions to handle direct submission of FormData (
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanCassiere authored Jan 27, 2025
1 parent 2466ec1 commit 01a568f
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 3 deletions.
55 changes: 55 additions & 0 deletions e2e/start/basic/app/routes/-server-fns/submit-post-formdata-fn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createServerFn } from '@tanstack/start'

const testValues = {
name: 'Sean',
}

export const greetUser = createServerFn({ method: 'POST' })
.validator((data: FormData) => {
if (!(data instanceof FormData)) {
throw new Error('Invalid! FormData is required')
}
const name = data.get('name')

if (!name) {
throw new Error('Name is required')
}

return {
name: name.toString(),
}
})
.handler(({ data: { name } }) => {
return new Response(`Hello, ${name}!`)
})

export function SubmitPostFormDataFn() {
return (
<div className="p-2 border m-2 grid gap-2">
<h3>Submit POST FormData Fn Call</h3>
<div className="overflow-y-auto">
It should return navigate and return{' '}
<code>
<pre data-testid="expected-submit-post-formdata-server-fn-result">
Hello, {testValues.name}!
</pre>
</code>
</div>
<form
className="flex flex-col gap-2"
data-testid="submit-post-formdata-form"
method="POST"
action={greetUser.url}
>
<input type="text" name="name" defaultValue={testValues.name} />
<button
type="submit"
data-testid="test-submit-post-formdata-fn-calls-btn"
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"
>
Submit (native)
</button>
</form>
</div>
)
}
2 changes: 2 additions & 0 deletions e2e/start/basic/app/routes/server-fns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MultipartServerFnCall } from './-server-fns/multipart-formdata-fn-call'
import { AllowServerFnReturnNull } from './-server-fns/allow-fn-return-null'
import { SerializeFormDataFnCall } from './-server-fns/serialize-formdata-fn-call'
import { ResponseHeaders, getTestHeaders } from './-server-fns/response-headers'
import { SubmitPostFormDataFn } from './-server-fns/submit-post-formdata-fn'

export const Route = createFileRoute('/server-fns')({
component: RouteComponent,
Expand All @@ -25,6 +26,7 @@ function RouteComponent() {
<AllowServerFnReturnNull />
<SerializeFormDataFnCall />
<ResponseHeaders initialTestHeaders={testHeaders} />
<SubmitPostFormDataFn />
</>
)
}
20 changes: 20 additions & 0 deletions e2e/start/basic/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,23 @@ test('server function can correctly send and receive headers', async ({
"accept-encoding": "gzip, deflate, br, zstd"
}`)
})

test('Direct POST submitting FormData to a Server function returns the correct message', async ({
page,
}) => {
await page.goto('/server-fns')

await page.waitForLoadState('networkidle')

const expected =
(await page
.getByTestId('expected-submit-post-formdata-server-fn-result')
.textContent()) || ''
expect(expected).not.toBe('')

await page.getByTestId('test-submit-post-formdata-fn-calls-btn').click()
await page.waitForLoadState('networkidle')

const result = await page.innerText('body')
expect(result).toBe(expected)
})
2 changes: 1 addition & 1 deletion packages/router-core/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type SerializerParseBy<T, TSerializable> = T extends TSerializable
? ReadableStream
: { [K in keyof T]: SerializerParseBy<T[K], TSerializable> }

export type Serializable = Date | undefined | Error | FormData
export type Serializable = Date | undefined | Error | FormData | Response

export type SerializerStringify<T> = SerializerStringifyBy<T, Serializable>

Expand Down
2 changes: 1 addition & 1 deletion packages/start-client/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const createSerializer = <TKey extends string, TInput, TSerialized>(
})

// Keep these ordered by predicted frequency
// Make sure to keep DefaultSerializeable in sync with these serializers
// Make sure to keep DefaultSerializable in sync with these serializers
// Also, make sure that they are unit tested in serializer.test.tsx
const serializers = [
createSerializer(
Expand Down
11 changes: 10 additions & 1 deletion packages/start-server-functions-handler/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,21 @@ async function handleServerRequest(request: Request, _event?: H3Event) {
)
}

// Known FormData 'Content-Type' header values
const formDataContentTypes = [
'multipart/form-data',
'application/x-www-form-urlencoded',
]

const response = await (async () => {
try {
const arg = await (async () => {
// FormData
if (
request.headers.get('Content-Type')?.includes('multipart/form-data')
request.headers.get('Content-Type') &&
formDataContentTypes.some((type) =>
request.headers.get('Content-Type')?.includes(type),
)
) {
// We don't support GET requests with FormData payloads... that seems impossible
invariant(
Expand Down

0 comments on commit 01a568f

Please sign in to comment.