diff --git a/e2e/react-start/server-functions/src/routeTree.gen.ts b/e2e/react-start/server-functions/src/routeTree.gen.ts index 249a9c42ce..8077133698 100644 --- a/e2e/react-start/server-functions/src/routeTree.gen.ts +++ b/e2e/react-start/server-functions/src/routeTree.gen.ts @@ -47,6 +47,7 @@ import { Route as MiddlewareMiddlewareFactoryRouteImport } from './routes/middle import { Route as MiddlewareFunctionMetadataRouteImport } from './routes/middleware/function-metadata' import { Route as MiddlewareClientMiddlewareRouterRouteImport } from './routes/middleware/client-middleware-router' import { Route as MiddlewareCatchHandlerErrorRouteImport } from './routes/middleware/catch-handler-error' +import { Route as MiddlewareNonErrorPayloadRouteImport } from './routes/middleware/non-error-payload' import { Route as MethodNotAllowedMethodRouteImport } from './routes/method-not-allowed/$method' import { Route as CookiesSetRouteImport } from './routes/cookies/set' import { Route as AbortSignalMethodRouteImport } from './routes/abort-signal/$method' @@ -251,6 +252,12 @@ const MiddlewareCatchHandlerErrorRoute = path: '/middleware/catch-handler-error', getParentRoute: () => rootRouteImport, } as any) +const MiddlewareNonErrorPayloadRoute = + MiddlewareNonErrorPayloadRouteImport.update({ + id: '/middleware/non-error-payload', + path: '/middleware/non-error-payload', + getParentRoute: () => rootRouteImport, + } as any) const MethodNotAllowedMethodRoute = MethodNotAllowedMethodRouteImport.update({ id: '/method-not-allowed/$method', path: '/method-not-allowed/$method', @@ -307,6 +314,7 @@ export interface FileRoutesByFullPath { '/cookies/set': typeof CookiesSetRoute '/method-not-allowed/$method': typeof MethodNotAllowedMethodRoute '/middleware/catch-handler-error': typeof MiddlewareCatchHandlerErrorRoute + '/middleware/non-error-payload': typeof MiddlewareNonErrorPayloadRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute '/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute @@ -353,6 +361,7 @@ export interface FileRoutesByTo { '/cookies/set': typeof CookiesSetRoute '/method-not-allowed/$method': typeof MethodNotAllowedMethodRoute '/middleware/catch-handler-error': typeof MiddlewareCatchHandlerErrorRoute + '/middleware/non-error-payload': typeof MiddlewareNonErrorPayloadRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute '/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute @@ -400,6 +409,7 @@ export interface FileRoutesById { '/cookies/set': typeof CookiesSetRoute '/method-not-allowed/$method': typeof MethodNotAllowedMethodRoute '/middleware/catch-handler-error': typeof MiddlewareCatchHandlerErrorRoute + '/middleware/non-error-payload': typeof MiddlewareNonErrorPayloadRoute '/middleware/client-middleware-router': typeof MiddlewareClientMiddlewareRouterRoute '/middleware/function-metadata': typeof MiddlewareFunctionMetadataRoute '/middleware/middleware-factory': typeof MiddlewareMiddlewareFactoryRoute @@ -448,6 +458,7 @@ export interface FileRouteTypes { | '/cookies/set' | '/method-not-allowed/$method' | '/middleware/catch-handler-error' + | '/middleware/non-error-payload' | '/middleware/client-middleware-router' | '/middleware/function-metadata' | '/middleware/middleware-factory' @@ -494,6 +505,7 @@ export interface FileRouteTypes { | '/cookies/set' | '/method-not-allowed/$method' | '/middleware/catch-handler-error' + | '/middleware/non-error-payload' | '/middleware/client-middleware-router' | '/middleware/function-metadata' | '/middleware/middleware-factory' @@ -540,6 +552,7 @@ export interface FileRouteTypes { | '/cookies/set' | '/method-not-allowed/$method' | '/middleware/catch-handler-error' + | '/middleware/non-error-payload' | '/middleware/client-middleware-router' | '/middleware/function-metadata' | '/middleware/middleware-factory' @@ -587,6 +600,7 @@ export interface RootRouteChildren { CookiesSetRoute: typeof CookiesSetRoute MethodNotAllowedMethodRoute: typeof MethodNotAllowedMethodRoute MiddlewareCatchHandlerErrorRoute: typeof MiddlewareCatchHandlerErrorRoute + MiddlewareNonErrorPayloadRoute: typeof MiddlewareNonErrorPayloadRoute MiddlewareClientMiddlewareRouterRoute: typeof MiddlewareClientMiddlewareRouterRoute MiddlewareFunctionMetadataRoute: typeof MiddlewareFunctionMetadataRoute MiddlewareMiddlewareFactoryRoute: typeof MiddlewareMiddlewareFactoryRoute @@ -880,6 +894,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof MiddlewareCatchHandlerErrorRouteImport parentRoute: typeof rootRouteImport } + '/middleware/non-error-payload': { + id: '/middleware/non-error-payload' + path: '/middleware/non-error-payload' + fullPath: '/middleware/non-error-payload' + preLoaderRoute: typeof MiddlewareNonErrorPayloadRouteImport + parentRoute: typeof rootRouteImport + } '/method-not-allowed/$method': { id: '/method-not-allowed/$method' path: '/method-not-allowed/$method' @@ -947,6 +968,7 @@ const rootRouteChildren: RootRouteChildren = { CookiesSetRoute: CookiesSetRoute, MethodNotAllowedMethodRoute: MethodNotAllowedMethodRoute, MiddlewareCatchHandlerErrorRoute: MiddlewareCatchHandlerErrorRoute, + MiddlewareNonErrorPayloadRoute: MiddlewareNonErrorPayloadRoute, MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute, MiddlewareFunctionMetadataRoute: MiddlewareFunctionMetadataRoute, MiddlewareMiddlewareFactoryRoute: MiddlewareMiddlewareFactoryRoute, diff --git a/e2e/react-start/server-functions/src/routes/middleware/non-error-payload.tsx b/e2e/react-start/server-functions/src/routes/middleware/non-error-payload.tsx new file mode 100644 index 0000000000..31251d092d --- /dev/null +++ b/e2e/react-start/server-functions/src/routes/middleware/non-error-payload.tsx @@ -0,0 +1,55 @@ +import { createFileRoute } from '@tanstack/react-router' +import { createMiddleware, createServerFn } from '@tanstack/react-start' +import { useState } from 'react' + +const structuredErrorMiddleware = createMiddleware({ type: 'function' }).server( + async ({ next }) => { + try { + return await next() + } catch { + // Throw a plain object — not an Error instance — as a structured payload. + // The client should receive this object as the resolved value, not undefined. + throw { code: 'HANDLED', message: 'Middleware returned structured payload' } + } + }, +) + +const $serverFnThatThrows = createServerFn({ method: 'GET' }) + .middleware([structuredErrorMiddleware]) + .handler(() => { + throw new Error('Trigger middleware catch') + }) + +export const Route = createFileRoute('/middleware/non-error-payload')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [result, setResult] = useState(undefined) + const [loading, setLoading] = useState(false) + + const handleClick = async () => { + setLoading(true) + const value = await $serverFnThatThrows() + setResult(value) + setLoading(false) + } + + return ( +
+

+ Non-Error Middleware Payload Test +

+ + {result !== undefined && ( +
{JSON.stringify(result)}
+ )} +
+ ) +} 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 4173362e4c..d1ccd52c50 100644 --- a/e2e/react-start/server-functions/tests/server-functions.spec.ts +++ b/e2e/react-start/server-functions/tests/server-functions.spec.ts @@ -1050,6 +1050,27 @@ test('middleware can catch errors thrown by server function handlers', async ({ ) }) +test('middleware can surface non-Error structured payloads to the caller', async ({ + page, +}) => { + await page.goto('/middleware/non-error-payload') + + await page.waitForLoadState('networkidle') + + await expect(page.getByTestId('non-error-payload-title')).toBeVisible() + + await page.getByTestId('trigger-non-error-btn').click() + + await expect(page.getByTestId('payload-result')).toBeVisible() + + const text = await page.getByTestId('payload-result').textContent() + const payload = JSON.parse(text!) + expect(payload).toEqual({ + code: 'HANDLED', + message: 'Middleware returned structured payload', + }) +}) + test('server function with custom fetch implementation passed directly', async ({ page, }) => { diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 37d4ead122..c5aafd73f2 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -1,6 +1,6 @@ import { mergeHeaders } from '@tanstack/router-core/ssr/client' -import { isRedirect, parseRedirect } from '@tanstack/router-core' +import { isNotFound, isRedirect, parseRedirect } from '@tanstack/router-core' import { TSS_SERVER_FUNCTION_FACTORY } from './constants' import { getStartOptions } from './getStartOptions' import { getStartContextServerOnly } from './getStartContextServerOnly' @@ -10,6 +10,14 @@ import type { ServerFnMeta, TSS_SERVER_FUNCTION, } from './constants' +import type { + AnyFunctionMiddleware, + AnyRequestMiddleware, + AssignAllServerFnContext, + FunctionMiddlewareServerFnResult, + IntersectAllValidatorInputs, + IntersectAllValidatorOutputs, +} from './createMiddleware' import type { AnyValidator, Constrain, @@ -21,14 +29,6 @@ import type { ValidateSerializableInput, Validator, } from '@tanstack/router-core' -import type { - AnyFunctionMiddleware, - AnyRequestMiddleware, - AssignAllServerFnContext, - FunctionMiddlewareServerFnResult, - IntersectAllValidatorInputs, - IntersectAllValidatorOutputs, -} from './createMiddleware' type TODO = any @@ -162,8 +162,15 @@ export const createServerFn: CreateServerFn = (options, __opts) => { throw redirect } - if (result.error) throw result.error - return result.result + if ( + result.error instanceof Error || + isRedirect(result.error) || + isNotFound(result.error) + ) + throw result.error + // Non-Error values in result.error are application-level error payloads; + // return them as the resolved value when no explicit result is present. + return result.result !== undefined ? result.result : result.error }, { // This copies over the URL, function ID @@ -302,10 +309,16 @@ export async function executeMiddleware( const result = await callNextMiddleware(nextCtx) - if (result.error) { + if ( + result.error instanceof Error || + isRedirect(result.error) || + isNotFound(result.error) + ) { throw result.error } + // Non-Error values in result.error are application-level error payloads; + // preserve them for downstream handlers. return result }