diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index b2346f1b1a8..51327cb8164 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as RawResponseRouteImport } from './routes/raw-response' import { Route as MultipartRouteImport } from './routes/multipart' import { Route as IsomorphicFnsRouteImport } from './routes/isomorphic-fns' import { Route as HeadersRouteImport } from './routes/headers' +import { Route as FormdataContextRouteImport } from './routes/formdata-context' import { Route as EnvOnlyRouteImport } from './routes/env-only' import { Route as DeadCodePreserveRouteImport } from './routes/dead-code-preserve' import { Route as ConsistentRouteImport } from './routes/consistent' @@ -77,6 +78,11 @@ const HeadersRoute = HeadersRouteImport.update({ path: '/headers', getParentRoute: () => rootRouteImport, } as any) +const FormdataContextRoute = FormdataContextRouteImport.update({ + id: '/formdata-context', + path: '/formdata-context', + getParentRoute: () => rootRouteImport, +} as any) const EnvOnlyRoute = EnvOnlyRouteImport.update({ id: '/env-only', path: '/env-only', @@ -182,6 +188,7 @@ export interface FileRoutesByFullPath { '/consistent': typeof ConsistentRoute '/dead-code-preserve': typeof DeadCodePreserveRoute '/env-only': typeof EnvOnlyRoute + '/formdata-context': typeof FormdataContextRoute '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute @@ -211,6 +218,7 @@ export interface FileRoutesByTo { '/consistent': typeof ConsistentRoute '/dead-code-preserve': typeof DeadCodePreserveRoute '/env-only': typeof EnvOnlyRoute + '/formdata-context': typeof FormdataContextRoute '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute @@ -241,6 +249,7 @@ export interface FileRoutesById { '/consistent': typeof ConsistentRoute '/dead-code-preserve': typeof DeadCodePreserveRoute '/env-only': typeof EnvOnlyRoute + '/formdata-context': typeof FormdataContextRoute '/headers': typeof HeadersRoute '/isomorphic-fns': typeof IsomorphicFnsRoute '/multipart': typeof MultipartRoute @@ -272,6 +281,7 @@ export interface FileRouteTypes { | '/consistent' | '/dead-code-preserve' | '/env-only' + | '/formdata-context' | '/headers' | '/isomorphic-fns' | '/multipart' @@ -301,6 +311,7 @@ export interface FileRouteTypes { | '/consistent' | '/dead-code-preserve' | '/env-only' + | '/formdata-context' | '/headers' | '/isomorphic-fns' | '/multipart' @@ -330,6 +341,7 @@ export interface FileRouteTypes { | '/consistent' | '/dead-code-preserve' | '/env-only' + | '/formdata-context' | '/headers' | '/isomorphic-fns' | '/multipart' @@ -360,6 +372,7 @@ export interface RootRouteChildren { ConsistentRoute: typeof ConsistentRoute DeadCodePreserveRoute: typeof DeadCodePreserveRoute EnvOnlyRoute: typeof EnvOnlyRoute + FormdataContextRoute: typeof FormdataContextRoute HeadersRoute: typeof HeadersRoute IsomorphicFnsRoute: typeof IsomorphicFnsRoute MultipartRoute: typeof MultipartRoute @@ -442,6 +455,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HeadersRouteImport parentRoute: typeof rootRouteImport } + '/formdata-context': { + id: '/formdata-context' + path: '/formdata-context' + fullPath: '/formdata-context' + preLoaderRoute: typeof FormdataContextRouteImport + parentRoute: typeof rootRouteImport + } '/env-only': { id: '/env-only' path: '/env-only' @@ -584,6 +604,7 @@ const rootRouteChildren: RootRouteChildren = { ConsistentRoute: ConsistentRoute, DeadCodePreserveRoute: DeadCodePreserveRoute, EnvOnlyRoute: EnvOnlyRoute, + FormdataContextRoute: FormdataContextRoute, HeadersRoute: HeadersRoute, IsomorphicFnsRoute: IsomorphicFnsRoute, MultipartRoute: MultipartRoute, diff --git a/e2e/react-start/server-functions/src/routes/formdata-context.tsx b/e2e/react-start/server-functions/src/routes/formdata-context.tsx new file mode 100644 index 00000000000..08aed62c2ed --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/formdata-context.tsx @@ -0,0 +1,147 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import React from 'react' +import { z } from 'zod' + +// Middleware that creates context on client and sends it to server +const testMiddleware = createMiddleware({ type: 'function' }) + .client(async ({ next }) => { + const testString = 'context-from-middleware-' + Date.now() + return next({ + sendContext: { testString }, + }) + }) + .server(async ({ next, context }) => { + if (!context.testString) { + throw new Error( + 'BUG: testString is missing from server middleware context!', + ) + } + return await next({ context: { ...context } }) + }) + +// Server function with FormData +export const formDataWithContextFn = createServerFn({ method: 'POST' }) + .middleware([testMiddleware]) + .inputValidator((data: unknown) => { + const formData = z.instanceof(FormData).parse(data) + return { + name: z.string().parse(formData.get('name')), + } + }) + .handler(({ data, context }) => { + if (!context.testString) { + throw new Error('BUG: testString is missing in handler context!') + } + return { + success: true, + name: data.name, + testString: context.testString, + hasContext: true, + } + }) + +// Server function without parameters +export const simpleTestFn = createServerFn({ method: 'POST' }) + .middleware([testMiddleware]) + .handler(({ context }) => { + if (!context.testString) { + throw new Error('BUG: testString is missing in handler context!') + } + return { + success: true, + testString: context.testString, + hasContext: true, + } + }) + +export const Route = createFileRoute('/formdata-context')({ + component: FormDataContextComponent, +}) + +function FormDataContextComponent() { + const [result, setResult] = React.useState(null) + const [error, setError] = React.useState(null) + const [loading, setLoading] = React.useState(null) + + const testValues = { + name: 'TestUser', + expectedContextValue: 'context-from-middleware', + } + + const handleClick = async (fn: () => Promise, type: string) => { + setLoading(type) + setError(null) + setResult(null) + try { + const response = await fn() + setResult(response) + } catch (err) { + setError(err instanceof Error ? err.message : 'Unknown error') + } finally { + setLoading(null) + } + } + + return ( +
+

FormData with Context Test

+
+ Expected context value:{' '} + +
+            {testValues.expectedContextValue}
+          
+
+
+
+ + +
+ {error && ( +
+ Error: {error} +
+ )} + {result && ( +
+
+            {JSON.stringify(result, null, 2)}
+          
+
+ )} +
+ ) +} diff --git a/e2e/react-start/server-functions/tests/server-functions.spec.ts b/e2e/react-start/server-functions/tests/server-functions.spec.ts index ecf59969939..b384be2ea6f 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -184,6 +184,58 @@ test('Server function can correctly send and receive FormData', async ({ ).toContainText(expected) }) +test('server function correctly passes context when using FormData', async ({ + page, +}) => { + await page.goto('/formdata-context') + + await page.waitForLoadState('networkidle') + + const expectedContextValue = + (await page.getByTestId('expected-formdata-context-value').textContent()) || + '' + expect(expectedContextValue).toBe('context-from-middleware') + + // Test FormData function + await page.getByTestId('test-formdata-context-btn').click() + await page.waitForLoadState('networkidle') + + // Wait for the result to appear + await page.waitForSelector('[data-testid="formdata-context-result"]') + + const resultText = + (await page.getByTestId('formdata-context-result').textContent()) || '' + expect(resultText).not.toBe('') + + const result = JSON.parse(resultText) + + // Verify context was passed correctly for FormData function + expect(result.success).toBe(true) + expect(result.hasContext).toBe(true) + expect(result.name).toBe('TestUser') + expect(result.testString).toBeDefined() + expect(result.testString).toContain('context-from-middleware') + + // Test simple function (no parameters) + await page.getByTestId('test-simple-context-btn').click() + await page.waitForLoadState('networkidle') + + // Wait for the result to appear + await page.waitForSelector('[data-testid="formdata-context-result"]') + + const simpleResultText = + (await page.getByTestId('formdata-context-result').textContent()) || '' + expect(simpleResultText).not.toBe('') + + const simpleResult = JSON.parse(simpleResultText) + + // Verify context was passed correctly for simple function + expect(simpleResult.success).toBe(true) + expect(simpleResult.hasContext).toBe(true) + expect(simpleResult.testString).toBeDefined() + expect(simpleResult.testString).toContain('context-from-middleware') +}) + test('server function can correctly send and receive headers', async ({ page, }) => { diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts index ec9042ec473..8cd98cf30be 100644 --- a/packages/start-server-core/src/server-functions-handler.ts +++ b/packages/start-server-core/src/server-functions-handler.ts @@ -88,8 +88,14 @@ export const handleServerAction = async ({ if (typeof serializedContext === 'string') { try { const parsedContext = JSON.parse(serializedContext) - if (typeof parsedContext === 'object' && parsedContext) { - params.context = { ...context, ...parsedContext } + const deserializedContext = fromJSON(parsedContext, { + plugins: serovalPlugins, + }) + if ( + typeof deserializedContext === 'object' && + deserializedContext + ) { + params.context = { ...context, ...deserializedContext } } } catch {} }