From 1abc34874a7349750f2f4d8152c37141c4ea0c79 Mon Sep 17 00:00:00 2001 From: Zelys Date: Thu, 7 May 2026 19:40:22 -0500 Subject: [PATCH 1/8] 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 37d4ead122..352dfbe87b 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/8] 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 352dfbe87b..ba9f792092 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/8] 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 ba9f792092..7a5a8e9815 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/8] 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 7a5a8e9815..37c2e6cbdf 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/8] 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 37c2e6cbdf..2d900ffb73 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/8] =?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 2d900ffb73..c5aafd73f2 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/8] 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 206b70505c..20ca5b1b0d 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() From 39a19bcda8fff58c0d14791628fcca657a7cbc87 Mon Sep 17 00:00:00 2001 From: Zelys Date: Sun, 10 May 2026 11:13:22 -0500 Subject: [PATCH 8/8] fix(start-client-core): restore original #tanstack-* import order in hydrateStart.ts Virtual module imports (#tanstack-*) resolve as internal (rank 2), not unknown. They must appear before sibling imports, not after. Reverts mistaken reorder. --- 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 20ca5b1b0d..206b70505c 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()