diff --git a/e2e/start/basic/app/routeTree.gen.ts b/e2e/start/basic/app/routeTree.gen.ts index 059009361c0..5668aecde41 100644 --- a/e2e/start/basic/app/routeTree.gen.ts +++ b/e2e/start/basic/app/routeTree.gen.ts @@ -13,6 +13,7 @@ import { Route as rootRoute } from './routes/__root' import { Route as UsersImport } from './routes/users' import { Route as StatusImport } from './routes/status' +import { Route as ServerFnsImport } from './routes/server-fns' import { Route as SearchParamsImport } from './routes/search-params' import { Route as RedirectImport } from './routes/redirect' import { Route as PostsImport } from './routes/posts' @@ -42,6 +43,12 @@ const StatusRoute = StatusImport.update({ getParentRoute: () => rootRoute, } as any) +const ServerFnsRoute = ServerFnsImport.update({ + id: '/server-fns', + path: '/server-fns', + getParentRoute: () => rootRoute, +} as any) + const SearchParamsRoute = SearchParamsImport.update({ id: '/search-params', path: '/search-params', @@ -170,6 +177,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SearchParamsImport parentRoute: typeof rootRoute } + '/server-fns': { + id: '/server-fns' + path: '/server-fns' + fullPath: '/server-fns' + preLoaderRoute: typeof ServerFnsImport + parentRoute: typeof rootRoute + } '/status': { id: '/status' path: '/status' @@ -301,6 +315,7 @@ export interface FileRoutesByFullPath { '/posts': typeof PostsRouteWithChildren '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute + '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute '/users': typeof UsersRouteWithChildren '/posts/$postId': typeof PostsPostIdRoute @@ -318,6 +333,7 @@ export interface FileRoutesByTo { '/deferred': typeof DeferredRoute '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute + '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute '/posts/$postId': typeof PostsPostIdRoute '/users/$userId': typeof UsersUserIdRoute @@ -336,6 +352,7 @@ export interface FileRoutesById { '/posts': typeof PostsRouteWithChildren '/redirect': typeof RedirectRoute '/search-params': typeof SearchParamsRoute + '/server-fns': typeof ServerFnsRoute '/status': typeof StatusRoute '/users': typeof UsersRouteWithChildren '/_layout/_layout-2': typeof LayoutLayout2RouteWithChildren @@ -357,6 +374,7 @@ export interface FileRouteTypes { | '/posts' | '/redirect' | '/search-params' + | '/server-fns' | '/status' | '/users' | '/posts/$postId' @@ -373,6 +391,7 @@ export interface FileRouteTypes { | '/deferred' | '/redirect' | '/search-params' + | '/server-fns' | '/status' | '/posts/$postId' | '/users/$userId' @@ -389,6 +408,7 @@ export interface FileRouteTypes { | '/posts' | '/redirect' | '/search-params' + | '/server-fns' | '/status' | '/users' | '/_layout/_layout-2' @@ -409,6 +429,7 @@ export interface RootRouteChildren { PostsRoute: typeof PostsRouteWithChildren RedirectRoute: typeof RedirectRoute SearchParamsRoute: typeof SearchParamsRoute + ServerFnsRoute: typeof ServerFnsRoute StatusRoute: typeof StatusRoute UsersRoute: typeof UsersRouteWithChildren PostsPostIdDeepRoute: typeof PostsPostIdDeepRoute @@ -421,6 +442,7 @@ const rootRouteChildren: RootRouteChildren = { PostsRoute: PostsRouteWithChildren, RedirectRoute: RedirectRoute, SearchParamsRoute: SearchParamsRoute, + ServerFnsRoute: ServerFnsRoute, StatusRoute: StatusRoute, UsersRoute: UsersRouteWithChildren, PostsPostIdDeepRoute: PostsPostIdDeepRoute, @@ -442,6 +464,7 @@ export const routeTree = rootRoute "/posts", "/redirect", "/search-params", + "/server-fns", "/status", "/users", "/posts_/$postId/deep" @@ -472,6 +495,9 @@ export const routeTree = rootRoute "/search-params": { "filePath": "search-params.tsx" }, + "/server-fns": { + "filePath": "server-fns.tsx" + }, "/status": { "filePath": "status.tsx" }, diff --git a/e2e/start/basic/app/routes/server-fns.tsx b/e2e/start/basic/app/routes/server-fns.tsx new file mode 100644 index 00000000000..31626b82b68 --- /dev/null +++ b/e2e/start/basic/app/routes/server-fns.tsx @@ -0,0 +1,124 @@ +import * as React from 'react' +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/start' + +export const Route = createFileRoute('/server-fns')({ + component: RouteComponent, +}) + +function RouteComponent() { + return ( + <> + + + ) +} + +// START CONSISTENT_SERVER_FN_CALLS +const cons_getFn1 = createServerFn() + .validator((d: { username: string }) => d) + .handler(async ({ data }) => { + return { payload: data } + }) + +const cons_serverGetFn1 = createServerFn() + .validator((d: { username: string }) => d) + .handler(({ data }) => { + return cons_getFn1({ data }) + }) + +const cons_postFn1 = createServerFn({ method: 'POST' }) + .validator((d: { username: string }) => d) + .handler(async ({ data }) => { + return { payload: data } + }) + +const cons_serverPostFn1 = createServerFn({ method: 'POST' }) + .validator((d: { username: string }) => d) + .handler(({ data }) => { + return cons_postFn1({ data }) + }) + +/** + * This component checks whether the returned payloads from server function + * are the same, regardless of whether the server function is called directly + * from the client or from within the server function. + * @link https://github.com/TanStack/router/issues/1866 + * @link https://github.com/TanStack/router/issues/2481 + */ +function ConsistentServerFnCalls() { + const [getServerResult, setGetServerResult] = React.useState({}) + const [getDirectResult, setGetDirectResult] = React.useState({}) + + const [postServerResult, setPostServerResult] = React.useState({}) + const [postDirectResult, setPostDirectResult] = React.useState({}) + + return ( +
+

Consistent Server Fn GET Calls

+

+ This component checks whether the returned payloads from server function + are the same, regardless of whether the server function is called + directly from the client or from within the server function. +

+
+ It should return{' '} + +
+            {JSON.stringify({ payload: { username: 'TEST' } })}
+          
+
+
+

+ {`GET: cons_getFn1 called from server cons_serverGetFn1 returns`} +
+ + {JSON.stringify(getServerResult)} + +

+

+ {`GET: cons_getFn1 called directly returns`} +
+ + {JSON.stringify(getDirectResult)} + +

+

+ {`POST: cons_postFn1 called from cons_serverPostFn1 returns`} +
+ + {JSON.stringify(postServerResult)} + +

+

+ {`POST: cons_postFn1 called directly returns`} +
+ + {JSON.stringify(postDirectResult)} + +

+ +
+ ) +} + +// END CONSISTENT_SERVER_FN_CALLS diff --git a/e2e/start/basic/tests/app.spec.ts b/e2e/start/basic/tests/app.spec.ts index addd6913cec..58af4a5b198 100644 --- a/e2e/start/basic/tests/app.spec.ts +++ b/e2e/start/basic/tests/app.spec.ts @@ -101,3 +101,33 @@ test('invoking a server function with custom response status code', async ({ }) await requestPromise }) + +test('Consistent server function returns both on client and server for GET and POST calls', async ({ + page, +}) => { + await page.goto('/server-fns') + + await page.waitForLoadState('networkidle') + const expected = + (await page + .getByTestId('expected-consistent-server-fns-result') + .textContent()) || '' + expect(expected).not.toBe('') + + await page.getByTestId('test-consistent-server-fn-calls-btn').click() + await page.waitForLoadState('networkidle') + + // GET calls + await expect(page.getByTestId('cons_serverGetFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_getFn1-response')).toContainText(expected) + + // POST calls + await expect(page.getByTestId('cons_serverPostFn1-response')).toContainText( + expected, + ) + await expect(page.getByTestId('cons_postFn1-response')).toContainText( + expected, + ) +}) diff --git a/packages/start/src/client-runtime/fetcher.tsx b/packages/start/src/client-runtime/fetcher.tsx index e7c0773564d..ab46f9ee45c 100644 --- a/packages/start/src/client-runtime/fetcher.tsx +++ b/packages/start/src/client-runtime/fetcher.tsx @@ -55,7 +55,10 @@ export async function fetcher( body: type === 'formData' ? first.data - : (defaultTransformer.stringify(first.data ?? null) as any), + : (defaultTransformer.stringify({ + data: first.data ?? null, + context: first.context, + }) as any), } : {}), }) diff --git a/packages/start/src/client/createServerFn.ts b/packages/start/src/client/createServerFn.ts index 4cb9a99bdc4..565cc1960ae 100644 --- a/packages/start/src/client/createServerFn.ts +++ b/packages/start/src/client/createServerFn.ts @@ -153,6 +153,10 @@ export function createServerFn< TValidator > + if (typeof resolvedOptions.method === 'undefined') { + resolvedOptions.method = 'GET' as TMethod + } + return { options: resolvedOptions as any, middleware: (middleware) => { diff --git a/packages/start/src/server-handler/index.tsx b/packages/start/src/server-handler/index.tsx index 732b3ad7894..a854a30cde2 100644 --- a/packages/start/src/server-handler/index.tsx +++ b/packages/start/src/server-handler/index.tsx @@ -79,7 +79,8 @@ export async function handleServerRequest(request: Request, _event?: H3Event) { } // For non-form, non-get - return await request.json() + const jsonPayloadAsString = await request.text() + return defaultTransformer.parse(jsonPayloadAsString) })() const result = await action(arg)