diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index 51327cb8164..34595c339d0 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -35,6 +35,8 @@ import { Route as RedirectTestSsrTargetRouteImport } from './routes/redirect-tes import { Route as MiddlewareSendServerFnRouteImport } from './routes/middleware/send-serverFn' import { Route as MiddlewareRequestMiddlewareRouteImport } from './routes/middleware/request-middleware' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' +import { Route as MethodNotAllowedPostRouteImport } from './routes/method-not-allowed/post' +import { Route as MethodNotAllowedGetRouteImport } from './routes/method-not-allowed/get' import { Route as CookiesSetRouteImport } from './routes/cookies/set' import { Route as FormdataRedirectTargetNameRouteImport } from './routes/formdata-redirect/target.$name' @@ -170,6 +172,16 @@ const MiddlewareClientMiddlewareRouterRoute = path: '/middleware/client-middleware-router', getParentRoute: () => rootRouteImport, } as any) +const MethodNotAllowedPostRoute = MethodNotAllowedPostRouteImport.update({ + id: '/method-not-allowed/post', + path: '/method-not-allowed/post', + getParentRoute: () => rootRouteImport, +} as any) +const MethodNotAllowedGetRoute = MethodNotAllowedGetRouteImport.update({ + id: '/method-not-allowed/get', + path: '/method-not-allowed/get', + getParentRoute: () => rootRouteImport, +} as any) const CookiesSetRoute = CookiesSetRouteImport.update({ id: '/cookies/set', path: '/cookies/set', @@ -198,6 +210,8 @@ export interface FileRoutesByFullPath { '/status': typeof StatusRoute '/submit-post-formdata': typeof SubmitPostFormdataRoute '/cookies/set': typeof CookiesSetRoute + '/method-not-allowed/get': typeof MethodNotAllowedGetRoute + '/method-not-allowed/post': typeof MethodNotAllowedPostRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute @@ -228,6 +242,8 @@ export interface FileRoutesByTo { '/status': typeof StatusRoute '/submit-post-formdata': typeof SubmitPostFormdataRoute '/cookies/set': typeof CookiesSetRoute + '/method-not-allowed/get': typeof MethodNotAllowedGetRoute + '/method-not-allowed/post': typeof MethodNotAllowedPostRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute @@ -259,6 +275,8 @@ export interface FileRoutesById { '/status': typeof StatusRoute '/submit-post-formdata': typeof SubmitPostFormdataRoute '/cookies/set': typeof CookiesSetRoute + '/method-not-allowed/get': typeof MethodNotAllowedGetRoute + '/method-not-allowed/post': typeof MethodNotAllowedPostRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/request-middleware': typeof MiddlewareRequestMiddlewareRoute '/middleware/send-serverFn': typeof MiddlewareSendServerFnRoute @@ -291,6 +309,8 @@ export interface FileRouteTypes { | '/status' | '/submit-post-formdata' | '/cookies/set' + | '/method-not-allowed/get' + | '/method-not-allowed/post' | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' @@ -321,6 +341,8 @@ export interface FileRouteTypes { | '/status' | '/submit-post-formdata' | '/cookies/set' + | '/method-not-allowed/get' + | '/method-not-allowed/post' | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' @@ -351,6 +373,8 @@ export interface FileRouteTypes { | '/status' | '/submit-post-formdata' | '/cookies/set' + | '/method-not-allowed/get' + | '/method-not-allowed/post' | '/middleware/client-middleware-router' | '/middleware/request-middleware' | '/middleware/send-serverFn' @@ -382,6 +406,8 @@ export interface RootRouteChildren { StatusRoute: typeof StatusRoute SubmitPostFormdataRoute: typeof SubmitPostFormdataRoute CookiesSetRoute: typeof CookiesSetRoute + MethodNotAllowedGetRoute: typeof MethodNotAllowedGetRoute + MethodNotAllowedPostRoute: typeof MethodNotAllowedPostRoute MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareRequestMiddlewareRoute: typeof MiddlewareRequestMiddlewareRoute MiddlewareSendServerFnRoute: typeof MiddlewareSendServerFnRoute @@ -581,6 +607,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MiddlewareClientMiddlewareRouterRouteImport parentRoute: typeof rootRouteImport } + '/method-not-allowed/post': { + id: '/method-not-allowed/post' + path: '/method-not-allowed/post' + fullPath: '/method-not-allowed/post' + preLoaderRoute: typeof MethodNotAllowedPostRouteImport + parentRoute: typeof rootRouteImport + } + '/method-not-allowed/get': { + id: '/method-not-allowed/get' + path: '/method-not-allowed/get' + fullPath: '/method-not-allowed/get' + preLoaderRoute: typeof MethodNotAllowedGetRouteImport + parentRoute: typeof rootRouteImport + } '/cookies/set': { id: '/cookies/set' path: '/cookies/set' @@ -614,6 +654,8 @@ const rootRouteChildren: RootRouteChildren = { StatusRoute: StatusRoute, SubmitPostFormdataRoute: SubmitPostFormdataRoute, CookiesSetRoute: CookiesSetRoute, + MethodNotAllowedGetRoute: MethodNotAllowedGetRoute, + MethodNotAllowedPostRoute: MethodNotAllowedPostRoute, MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareRequestMiddlewareRoute: MiddlewareRequestMiddlewareRoute, MiddlewareSendServerFnRoute: MiddlewareSendServerFnRoute, diff --git a/e2e/react-start/server-functions/src/routes/method-not-allowed/get.tsx b/e2e/react-start/server-functions/src/routes/method-not-allowed/get.tsx new file mode 100644 index 00000000000..c07679ee475 --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/method-not-allowed/get.tsx @@ -0,0 +1,56 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' +import { useState } from 'react' + +export const Route = createFileRoute('/method-not-allowed/get')({ + component: MethodNotAllowedFn, +}) + +export const getableServerFn = createServerFn({ method: 'GET' }).handler(() => { + return new Response('Hello, World!') +}) + +const fetchFn = async (method: string) => { + const response = await fetch('/_serverFn/constant_id_2?createServerFn', { + method, + }) + return [response.status, await response.text()] as const +} + +function MethodNotAllowedFn() { + const [fetchResult, setFetchResult] = useState< + readonly [number, string] | null + >(null) + return ( +
+

Method Not Allowed GET

+ + + + + + +
{JSON.stringify(fetchResult)}
+
+ ) +} diff --git a/e2e/react-start/server-functions/src/routes/method-not-allowed/post.tsx b/e2e/react-start/server-functions/src/routes/method-not-allowed/post.tsx new file mode 100644 index 00000000000..e25a4b588b5 --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/method-not-allowed/post.tsx @@ -0,0 +1,58 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createServerFn } from '@tanstack/react-start' +import { useState } from 'react' + +export const Route = createFileRoute('/method-not-allowed/post')({ + component: MethodNotAllowedFn, +}) + +export const postableServerFn = createServerFn({ method: 'POST' }).handler( + () => { + return new Response('Hello, World!') + }, +) + +const fetchFn = async (method: string) => { + const response = await fetch('/_serverFn/constant_id_3?createServerFn', { + method, + }) + return [response.status, await response.text()] as const +} + +function MethodNotAllowedFn() { + const [fetchResult, setFetchResult] = useState< + readonly [number, string] | null + >(null) + return ( +
+

Method Not Allowed POST

+ + + + + + +
{JSON.stringify(fetchResult)}
+
+ ) +} 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 b384be2ea6f..8b2f2cedc5d 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -541,3 +541,65 @@ test('redirect in server function called in query during SSR', async ({ await expect(page.getByTestId('redirect-target-ssr')).toBeVisible() expect(page.url()).toContain('/redirect-test-ssr/target') }) + +test.describe('server function returns 405 when method is not allowed', () => { + test('serverFn defined with GET method', async ({ page }) => { + await page.goto('/method-not-allowed/get') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('get-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[200,"Hello, World!"]', + ) + + await page.getByTestId('post-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected GET method. Got POST"]', + ) + + await page.getByTestId('put-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected GET method. Got PUT"]', + ) + + await page.getByTestId('options-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected GET method. Got OPTIONS"]', + ) + }) + + test('serverFn defined with POST method', async ({ page }) => { + await page.goto('/method-not-allowed/post') + + await page.waitForLoadState('networkidle') + + await page.getByTestId('get-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected POST method. Got GET"]', + ) + + await page.getByTestId('post-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[200,"Hello, World!"]', + ) + + await page.getByTestId('put-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected POST method. Got PUT"]', + ) + + await page.getByTestId('options-button').click() + await page.waitForLoadState('networkidle') + await expect(page.getByTestId('fetch-result')).toContainText( + '[405,"expected POST method. Got OPTIONS"]', + ) + }) +}) diff --git a/e2e/react-start/server-functions/vite.config.ts b/e2e/react-start/server-functions/vite.config.ts index 97ff657630e..3df377bbd3f 100644 --- a/e2e/react-start/server-functions/vite.config.ts +++ b/e2e/react-start/server-functions/vite.config.ts @@ -6,6 +6,8 @@ import viteReact from '@vitejs/plugin-react' const FUNCTIONS_WITH_CONSTANT_ID = [ 'src/routes/submit-post-formdata.tsx/greetUser_createServerFn_handler', 'src/routes/formdata-redirect/index.tsx/greetUser_createServerFn_handler', + 'src/routes/method-not-allowed/get.tsx/getableServerFn_createServerFn_handler', + 'src/routes/method-not-allowed/post.tsx/postableServerFn_createServerFn_handler', ] export default defineConfig({ diff --git a/packages/start-server-core/src/server-functions-handler.ts b/packages/start-server-core/src/server-functions-handler.ts index 69e0dc19bc4..b9f8b82f34b 100644 --- a/packages/start-server-core/src/server-functions-handler.ts +++ b/packages/start-server-core/src/server-functions-handler.ts @@ -12,6 +12,15 @@ import { getServerFnById } from './getServerFnById' let regex: RegExp | undefined = undefined +const methodNotAllowed = (expectedMethod: string, actualMethod: string) => { + throw new Response(`expected ${expectedMethod} method. Got ${actualMethod}`, { + status: 405, + headers: { + Allow: expectedMethod, + }, + }) +} + export const handleServerAction = async ({ request, context, @@ -119,7 +128,7 @@ export const handleServerAction = async ({ } if (method.toLowerCase() !== 'post') { - throw new Error('expected POST method') + throw methodNotAllowed('POST', method) } let jsonPayload