Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions e2e/react-start/server-functions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -947,6 +968,7 @@ const rootRouteChildren: RootRouteChildren = {
CookiesSetRoute: CookiesSetRoute,
MethodNotAllowedMethodRoute: MethodNotAllowedMethodRoute,
MiddlewareCatchHandlerErrorRoute: MiddlewareCatchHandlerErrorRoute,
MiddlewareNonErrorPayloadRoute: MiddlewareNonErrorPayloadRoute,
MiddlewareClientMiddlewareRouterRoute: MiddlewareClientMiddlewareRouterRoute,
MiddlewareFunctionMetadataRoute: MiddlewareFunctionMetadataRoute,
MiddlewareMiddlewareFactoryRoute: MiddlewareMiddlewareFactoryRoute,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<unknown>(undefined)
const [loading, setLoading] = useState(false)

const handleClick = async () => {
setLoading(true)
const value = await $serverFnThatThrows()
setResult(value)
setLoading(false)
}

return (
<div className="p-4">
<h1 data-testid="non-error-payload-title">
Non-Error Middleware Payload Test
</h1>
<button
data-testid="trigger-non-error-btn"
onClick={handleClick}
disabled={loading}
>
{loading ? 'Loading' : 'Call Server Function'}
</button>
{result !== undefined && (
<div data-testid="payload-result">{JSON.stringify(result)}</div>
)}
</div>
)
}
21 changes: 21 additions & 0 deletions e2e/react-start/server-functions/tests/server-functions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}) => {
Expand Down
37 changes: 25 additions & 12 deletions packages/start-client-core/src/createServerFn.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -162,8 +162,15 @@ export const createServerFn: CreateServerFn<Register> = (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
Expand Down Expand Up @@ -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
}

Expand Down