From 1abc34874a7349750f2f4d8152c37141c4ea0c79 Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 7 May 2026 19:40:22 -0500 Subject: [PATCH 1/7] fix(start-client-core): allow middleware to return custom error structures from catch blocks Middleware that wanted to return structured error responses from catch blocks would have those errors thrown immediately to the client. The middleware protocol was using `.error` property for two incompatible purposes: framework errors (Error instances) and application error data (plain objects). This fix uses JavaScript's type system to distinguish them: only Error instances are thrown, allowing middleware to return custom error structures while preserving proper error propagation for real framework errors. Fixes #7238 BREAKING CHANGE: Middleware that throw non-Error values will now have those values captured in result.error instead of being thrown to the client. Only affects non-standard code patterns; best practice is to throw Error instances. --- packages/start-client-core/src/createServerFn.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 37d4ead1226..352dfbe87b5 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -162,8 +162,10 @@ export const createServerFn: CreateServerFn = (options, __opts) => { throw redirect } - if (result.error) throw result.error - return result.result + if (result.error instanceof 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 ?? result.error }, { // This copies over the URL, function ID @@ -302,10 +304,12 @@ export async function executeMiddleware( const result = await callNextMiddleware(nextCtx) - if (result.error) { + if (result.error instanceof Error) { throw result.error } + // Non-Error values in result.error are application-level error payloads; + // preserve them for downstream handlers. return result } From 909b938102af4c5c78305c62fba0ad9df6f3d2ff Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 7 May 2026 22:23:21 -0500 Subject: [PATCH 2/7] fix: handle null returns correctly in server functions The ?? operator treats null as a value, not nullish, so null ?? undefined returns null. However, we need to distinguish between: 1. result.result explicitly set to null (should return null) 2. result.result undefined (should fallback to result.error for custom error payloads) Change to: result.result !== undefined ? result.result : result.error This preserves null while still supporting middleware error payloads. Fixes #7364 --- packages/start-client-core/src/createServerFn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 352dfbe87b5..ba9f7920920 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -165,7 +165,7 @@ export const createServerFn: CreateServerFn = (options, __opts) => { if (result.error instanceof 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 ?? result.error + return result.result !== undefined ? result.result : result.error }, { // This copies over the URL, function ID From fe3a3ae0ea637765e97cdd2eee18834e80a5d578 Mon Sep 17 00:00:00 2001 From: Zelys Date: Fri, 8 May 2026 13:01:22 -0500 Subject: [PATCH 3/7] fix: re-throw redirect and notFound signals in middleware error guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When unwinding middleware, redirect() and notFound() framework signals must be re-thrown to reach the router—they are not Error instances. The previous instanceof Error guard would silently return them as application data, breaking SSR and client-navigation flow detection. Extended both error-handling locations (client fetcher and middleware executor) to also check isRedirect() and isNotFound(), ensuring framework signals propagate correctly while custom error payloads continue to be returned as resolved values. --- packages/start-client-core/src/createServerFn.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index ba9f7920920..7a5a8e98151 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' @@ -162,7 +162,12 @@ export const createServerFn: CreateServerFn = (options, __opts) => { throw redirect } - if (result.error instanceof Error) throw 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; // return them as the resolved value when no explicit result is present. return result.result !== undefined ? result.result : result.error @@ -304,7 +309,11 @@ export async function executeMiddleware( const result = await callNextMiddleware(nextCtx) - if (result.error instanceof Error) { + if ( + result.error instanceof Error || + isRedirect(result.error) || + isNotFound(result.error) + ) { throw result.error } From c4a563d2da9584a7fab60d9bfc7288d8be4c7624 Mon Sep 17 00:00:00 2001 From: Zelys Date: Fri, 8 May 2026 14:15:22 -0500 Subject: [PATCH 4/7] fix: reorder imports for ESLint compliance (external before relative) --- .../start-client-core/src/createServerFn.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 7a5a8e98151..37c2e6cbdf6 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -1,15 +1,6 @@ import { mergeHeaders } from '@tanstack/router-core/ssr/client' import { isNotFound, isRedirect, parseRedirect } from '@tanstack/router-core' -import { TSS_SERVER_FUNCTION_FACTORY } from './constants' -import { getStartOptions } from './getStartOptions' -import { getStartContextServerOnly } from './getStartContextServerOnly' -import { createNullProtoObject, safeObjectMerge } from './safeObjectMerge' -import type { - ClientFnMeta, - ServerFnMeta, - TSS_SERVER_FUNCTION, -} from './constants' import type { AnyValidator, Constrain, @@ -21,6 +12,16 @@ import type { ValidateSerializableInput, Validator, } from '@tanstack/router-core' + +import { TSS_SERVER_FUNCTION_FACTORY } from './constants' +import { getStartOptions } from './getStartOptions' +import { getStartContextServerOnly } from './getStartContextServerOnly' +import { createNullProtoObject, safeObjectMerge } from './safeObjectMerge' +import type { + ClientFnMeta, + ServerFnMeta, + TSS_SERVER_FUNCTION, +} from './constants' import type { AnyFunctionMiddleware, AnyRequestMiddleware, @@ -30,6 +31,7 @@ import type { IntersectAllValidatorOutputs, } from './createMiddleware' + type TODO = any export type ServerFnStrict = boolean | { input?: boolean; output?: boolean } From ffdd5805d69d0ff6ce871de739fe6d362b7e7c88 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 19:16:57 +0000 Subject: [PATCH 5/7] ci: apply automated fixes --- packages/start-client-core/src/createServerFn.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 37c2e6cbdf6..2d900ffb737 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -31,7 +31,6 @@ import type { IntersectAllValidatorOutputs, } from './createMiddleware' - type TODO = any export type ServerFnStrict = boolean | { input?: boolean; output?: boolean } From 9a08e6aaf5597bd5e2719a6af80ef9537a419022 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 9 May 2026 18:22:02 -0500 Subject: [PATCH 6/7] =?UTF-8?q?fix(start-client-core):=20restore=20import?= =?UTF-8?q?=20order=20=E2=80=94=20external=20type=20import=20must=20follow?= =?UTF-8?q?=20local=20type=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../start-client-core/src/createServerFn.ts | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/start-client-core/src/createServerFn.ts b/packages/start-client-core/src/createServerFn.ts index 2d900ffb737..c5aafd73f2e 100644 --- a/packages/start-client-core/src/createServerFn.ts +++ b/packages/start-client-core/src/createServerFn.ts @@ -1,18 +1,6 @@ import { mergeHeaders } from '@tanstack/router-core/ssr/client' import { isNotFound, isRedirect, parseRedirect } from '@tanstack/router-core' -import type { - AnyValidator, - Constrain, - Expand, - Register, - RegisteredSerializableInput, - ResolveValidatorInput, - ValidateSerializable, - ValidateSerializableInput, - Validator, -} from '@tanstack/router-core' - import { TSS_SERVER_FUNCTION_FACTORY } from './constants' import { getStartOptions } from './getStartOptions' import { getStartContextServerOnly } from './getStartContextServerOnly' @@ -30,6 +18,17 @@ import type { IntersectAllValidatorInputs, IntersectAllValidatorOutputs, } from './createMiddleware' +import type { + AnyValidator, + Constrain, + Expand, + Register, + RegisteredSerializableInput, + ResolveValidatorInput, + ValidateSerializable, + ValidateSerializableInput, + Validator, +} from '@tanstack/router-core' type TODO = any From 797ac09bb4addd2fa3ec90fcf20e479d25116ba4 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sat, 9 May 2026 18:47:15 -0500 Subject: [PATCH 7/7] fix(start-client-core): move # virtual module imports after sibling and type imports --- packages/start-client-core/src/client/hydrateStart.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/start-client-core/src/client/hydrateStart.ts b/packages/start-client-core/src/client/hydrateStart.ts index 206b70505cc..20ca5b1b0d2 100644 --- a/packages/start-client-core/src/client/hydrateStart.ts +++ b/packages/start-client-core/src/client/hydrateStart.ts @@ -1,14 +1,14 @@ import { hydrate } from '@tanstack/router-core/ssr/client' +import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter' +import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core' +import type { AnyStartInstanceOptions } from '../createStart' import { startInstance } from '#tanstack-start-entry' import { hasPluginAdapters, pluginSerializationAdapters, } from '#tanstack-start-plugin-adapters' import { getRouter } from '#tanstack-router-entry' -import { ServerFunctionSerializationAdapter } from './ServerFunctionSerializationAdapter' -import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core' -import type { AnyStartInstanceOptions } from '../createStart' export async function hydrateStart(): Promise { const router = await getRouter()