Skip to content

fix(start): initialize serialization adapters for early client server…#7708

Open
schiller-manuel wants to merge 1 commit into
mainfrom
fix-7706
Open

fix(start): initialize serialization adapters for early client server…#7708
schiller-manuel wants to merge 1 commit into
mainfrom
fix-7706

Conversation

@schiller-manuel

@schiller-manuel schiller-manuel commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

… functions

fixes #7706

Summary by CodeRabbit

  • Bug Fixes

    • Fixed server functions called from client entry modules before hydration, improving support for custom serialization adapters and server-function fetch settings.
    • Improved early initialization so client-side server function calls work more reliably during app startup.
  • Tests

    • Added end-to-end coverage for client-entry server-function serialization with nested return values.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces initStartOptions() to lazily initialize and cache start options (including serialization adapters) at the module level, replacing the window.__TSS_START_OPTIONS__ global. This allows server functions called from the client entry before hydration to access custom serialization adapters. The options are threaded through client RPC creation, middleware execution, seroval plugin selection, and server-side action handling.

Changes

Pre-hydration serialization adapter fix

Layer / File(s) Summary
initStartOptions, setStartOptions, and getStartOptions rework
packages/start-client-core/src/getStartOptions.ts, packages/start-client-core/src/global.ts, packages/start-client-core/src/getDefaultSerovalPlugins.ts, packages/router-core/src/index.ts
Module-level cached startOptions replaces window.__TSS_START_OPTIONS__. setStartOptions wires serialization adapters. New initStartOptions initializes from startInstance (handling promise results) or creates empty options. getDefaultSerovalPlugins now accepts startOptions as a parameter. isPromise is re-exported from router-core.
Client RPC and serverFnFetcher updated
packages/start-client-core/src/client-rpc/createClientRpc.ts, packages/start-client-core/src/client-rpc/serverFnFetcher.ts
createClientRpc replaces synchronous getStartOptions() with initStartOptions() and adds Promise-aware branching. serverFnFetcher signature changes from a handler fetch function to startOptions, using it for seroval plugin selection and fetch defaulting.
Middleware execution and hydrateStart
packages/start-client-core/src/createServerFn.ts, packages/start-client-core/src/client/hydrateStart.ts
executeMiddleware gains an explicit globalMiddlewares parameter. Client-side server function calls initialize startOptions via initStartOptions() and pass functionMiddleware explicitly. hydrateStart awaits initStartOptions() instead of reading from startInstance and writing to the window global.
Server-side handler
packages/start-server-core/src/createStartHandler.ts, packages/start-server-core/src/server-functions-handler.ts
handleServerAction accepts a new startOptions: AnyStartInstanceOptions parameter, used to derive seroval plugins per-request via getDefaultSerovalPlugins(startOptions).
E2E fixture and test
e2e/react-start/serialization-adapters/src/client-entry-server-functions.ts, e2e/react-start/serialization-adapters/src/client.tsx, e2e/react-start/serialization-adapters/tests/app.spec.ts, .changeset/calm-adapters-serialize.md
Client-entry server functions and a custom client entry are added to call them before hydration and store results in a window global. A Playwright test asserts the expected serialized values.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • TanStack/router#5276: Modifies serverFnFetcher.ts to change client server-function request/response processing, directly overlapping with this PR's changes to the same file's fetch selection and plugin initialization logic.

Suggested labels

package: start-client-core, package: start-server-core

🐇 Before the page wakes, the functions arise,
Serialization adapters reach for the skies!
No window to lean on, the cache holds the key,
initStartOptions sets the adapters free.
Custom types flow through before hydration's call —
The rabbit hops early and catches them all! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title is specific and matches the core fix around early serialization adapter initialization.
Linked Issues check ✅ Passed The implementation and e2e test directly address #7706 by initializing start options and serialization adapters before client-entry server function calls.
Out of Scope Changes check ✅ Passed All code changes support early client-side server function serialization; no unrelated scope is evident.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-7706

Comment @coderabbitai help to get the list of available commands.

@nx-cloud

nx-cloud Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 85cabb9

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 11m 58s View ↗
nx run-many --target=build --exclude=examples/*... ❌ Failed 2m 18s View ↗

☁️ Nx Cloud last updated this comment at 2026-06-27 17:29:43 UTC

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

2 package(s) bumped directly, 12 bumped as dependents.

🟩 Patch bumps

Package Version Reason
@tanstack/start-client-core 1.170.12 → 1.170.13 Changeset
@tanstack/start-server-core 1.169.15 → 1.169.16 Changeset
@tanstack/react-start 1.168.26 → 1.168.27 Dependent
@tanstack/react-start-client 1.168.14 → 1.168.15 Dependent
@tanstack/react-start-rsc 0.1.25 → 0.1.26 Dependent
@tanstack/react-start-server 1.167.20 → 1.167.21 Dependent
@tanstack/solid-start 1.168.26 → 1.168.27 Dependent
@tanstack/solid-start-client 1.168.14 → 1.168.15 Dependent
@tanstack/solid-start-server 1.167.20 → 1.167.21 Dependent
@tanstack/start-plugin-core 1.171.18 → 1.171.19 Dependent
@tanstack/start-static-server-functions 1.167.17 → 1.167.18 Dependent
@tanstack/vue-start 1.168.25 → 1.168.26 Dependent
@tanstack/vue-start-client 1.167.17 → 1.167.18 Dependent
@tanstack/vue-start-server 1.167.20 → 1.167.21 Dependent

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: 8d37346a6638
  • Measured at: 2026-06-27T17:18:50.223Z
  • Baseline source: history:ba52d2b8f9e9
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.33 KiB 0 B (0.00%) 87.19 KiB 273.80 KiB 76.00 KiB ▁▁▁▁▁▁▁████
react-router.full 91.06 KiB 0 B (0.00%) 90.92 KiB 285.70 KiB 79.10 KiB ▁▁▁▁▁▁▁████
solid-router.minimal 35.53 KiB 0 B (0.00%) 35.40 KiB 106.00 KiB 31.98 KiB ▁▁▁▁▁▁▁████
solid-router.full 40.58 KiB 0 B (0.00%) 40.46 KiB 121.22 KiB 36.50 KiB ▁▁▁▁▁▁▁████
vue-router.minimal 53.01 KiB 0 B (0.00%) 52.88 KiB 150.04 KiB 47.68 KiB ▁▁▁▁▁▁▁████
vue-router.full 58.99 KiB 0 B (0.00%) 58.86 KiB 168.80 KiB 52.88 KiB ▁▁▁▁▁▁▁████
react-start.minimal 102.00 KiB +12 B (+0.01%) 101.86 KiB 322.30 KiB 88.27 KiB ▁▁▁▁▁▁▁▄▄▄▇█
react-start.deferred-hydration 102.74 KiB +14 B (+0.01%) 101.88 KiB 323.68 KiB 89.03 KiB ▁▁▁▁▁▁▁▄▄▄▇█
react-start.full 105.42 KiB +20 B (+0.02%) 105.28 KiB 332.29 KiB 91.22 KiB ▁▁▁▁▁▁▁▄▄▄▇█
react-start.rsbuild.minimal 99.71 KiB +17 B (+0.02%) 99.54 KiB 316.73 KiB 85.84 KiB ▁▁▁▁▁▁▁▄▃▃▇█
react-start.rsbuild.minimal-iife 100.11 KiB +14 B (+0.01%) 99.94 KiB 317.66 KiB 86.23 KiB ▁▁▁▁▁▁▁▄▃▃▇█
react-start.rsbuild.full 102.94 KiB +24 B (+0.02%) 102.77 KiB 326.87 KiB 88.59 KiB ▁▁▁▁▁▁▁▄▃▃▆█
solid-start.minimal 49.70 KiB +26 B (+0.05%) 49.57 KiB 152.25 KiB 43.91 KiB ▁▁▁▁▁▁▁▄▄▄▆█
solid-start.deferred-hydration 52.96 KiB +24 B (+0.04%) 49.62 KiB 160.28 KiB 46.84 KiB ▁▁▁▁▁▁▁▄▄▄▆█
solid-start.full 55.53 KiB +43 B (+0.08%) 55.40 KiB 169.35 KiB 48.93 KiB ▁▁▁▁▁▁▁▃▃▃▆█
vue-start.minimal 71.12 KiB +7 B (+0.01%) 70.99 KiB 207.36 KiB 62.91 KiB ▁▁▁▁▁▁▁▅▅▅██
vue-start.full 75.13 KiB +37 B (+0.05%) 75.00 KiB 220.06 KiB 66.45 KiB ▁▁▁▁▁▁▁▄▄▄▆█

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/start-client-core/src/client/hydrateStart.ts (1)

21-30: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Clone the adapter list before merging router adapters.

serializationAdapters references cached startOptions.serializationAdapters, so push() mutates module-level Start options used later by client RPC serialization. Build a combined array instead, ideally deduping router adapters already present after retries/HMR.

Proposed fix
-  const startOptions = (await initStartOptions()) as AnyStartInstanceOptions
-  const serializationAdapters = startOptions.serializationAdapters
-
-  if (router.options.serializationAdapters) {
-    serializationAdapters.push(...router.options.serializationAdapters)
-  }
+  const startOptions =
+    ((await initStartOptions()) ?? {}) as AnyStartInstanceOptions
+  const startSerializationAdapters = startOptions.serializationAdapters ?? []
+  const routerSerializationAdapters =
+    router.options.serializationAdapters?.filter(
+      (adapter) => !startSerializationAdapters.includes(adapter),
+    ) ?? []
+  const serializationAdapters = [
+    ...startSerializationAdapters,
+    ...routerSerializationAdapters,
+  ]
 
   router.update({
     basepath: process.env.TSS_ROUTER_BASEPATH,
-    ...{ serializationAdapters },
+    serializationAdapters,
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-client-core/src/client/hydrateStart.ts` around lines 21 - 30,
Clone the adapter list in hydrateStart before merging router adapters:
startOptions.serializationAdapters is being reused directly, so the push in the
router.options.serializationAdapters branch mutates cached Start options. In
hydrateStart, build a new combined array from startOptions.serializationAdapters
and router.options.serializationAdapters, and pass that copy into router.update.
Also ensure duplicate adapters are not re-added when hydrateStart runs again
(for retries/HMR) by deduping based on adapter identity before updating the
router.
🧹 Nitpick comments (2)
e2e/react-start/serialization-adapters/src/client.tsx (1)

39-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the rejection as unknown before reading error fields.

Promise.catch gives you an untyped rejection here, so error?.name / error?.message sidestep the strict-safety rule for this .tsx file. Narrow it first and build the error payload from a checked shape. As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Proposed fix
-    .catch((error) => {
+    .catch((error: unknown) => {
+      const name = error instanceof Error ? error.name : 'Error'
+      const message = error instanceof Error ? error.message : String(error)
+
       window.__serializationAdapterClientEntryResult = {
         status: 'error',
-        name: error?.name ?? 'Error',
-        message: error?.message ?? String(error),
+        name,
+        message,
       }
     })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-start/serialization-adapters/src/client.tsx` around lines 39 - 44,
The Promise.catch handler in client.tsx is reading `error?.name` and
`error?.message` from an untyped rejection, which violates strict type safety;
update the catch in the client entry flow to treat the rejection as `unknown`
first, then narrow it with a checked shape before building
`window.__serializationAdapterClientEntryResult`. Use the existing catch block
in the client entry logic to extract `name` and `message` only after validation,
and fall back safely for non-error values.

Source: Coding guidelines

e2e/react-start/serialization-adapters/tests/app.spec.ts (1)

86-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Avoid window as any in the test predicate.

This drops type safety in the one place the spec reads the browser-side contract it is asserting. A small local type for the result shape keeps the wait predicate and the final assertion aligned. As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety.

Proposed fix
+type ClientEntryServerFnResult =
+  | {
+      status: 'success'
+      ping: string
+      shout: string
+      whisper: string
+    }
+  | {
+      status: 'error'
+      name: string
+      message: string
+    }
+
     const result = await page
       .waitForFunction(
-        () => (window as any).__serializationAdapterClientEntryResult,
+        () =>
+          (
+            window as Window & {
+              __serializationAdapterClientEntryResult?: ClientEntryServerFnResult
+            }
+          ).__serializationAdapterClientEntryResult,
       )
-      .then((handle) => handle.jsonValue())
+      .then((handle) => handle.jsonValue() as Promise<ClientEntryServerFnResult>)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/react-start/serialization-adapters/tests/app.spec.ts` around lines 86 -
90, The test predicate in app.spec.ts is bypassing type safety by using window
as any when reading __serializationAdapterClientEntryResult. Replace that cast
with a small local TypeScript type that describes the browser-side result shape,
and use it both in the waitForFunction predicate and the final
jsonValue/assertion so they stay aligned. Keep the change scoped to the existing
page.waitForFunction flow and the __serializationAdapterClientEntryResult
contract.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/start-client-core/src/client-rpc/serverFnFetcher.ts`:
- Around line 114-116: The seroval plugin cache in serverFnFetcher should not be
initialized with a one-time fallback because it can lock in stale adapters from
the first call. Update the logic around getDefaultSerovalPlugins and
serovalPlugins so it recomputes or refreshes based on the current startOptions
instead of using ||=, and avoid module-level shared state that can leak across
concurrent calls. Make sure the behavior still works with hydrateStart mutating
serializationAdapters before later fetches.

In `@packages/start-client-core/src/getDefaultSerovalPlugins.ts`:
- Around line 9-11: `getDefaultSerovalPlugins` currently declares `start` as
`AnyStartInstanceOptions | undefined`, which still forces callers to pass an
argument and breaks the zero-arg uses in `staticFunctionMiddleware`. Update the
function signature in `getDefaultSerovalPlugins` to make `start` optional with
`start?: AnyStartInstanceOptions`, and keep the existing handling inside the
function compatible with an omitted value.

In `@packages/start-server-core/src/server-functions-handler.ts`:
- Around line 70-72: The plugin cache in handleServerAction is incorrectly
shared across requests, so it only reflects the first
startOptions/serializationAdapters it sees. Remove the module-level
serovalPlugins reuse and make the plugin list request-scoped by computing it
from the current startOptions inside handleServerAction for each call, or
otherwise key the cache by the incoming options. Keep the fix centered around
handleServerAction and getDefaultSerovalPlugins so each request gets the correct
server plugin set.

---

Outside diff comments:
In `@packages/start-client-core/src/client/hydrateStart.ts`:
- Around line 21-30: Clone the adapter list in hydrateStart before merging
router adapters: startOptions.serializationAdapters is being reused directly, so
the push in the router.options.serializationAdapters branch mutates cached Start
options. In hydrateStart, build a new combined array from
startOptions.serializationAdapters and router.options.serializationAdapters, and
pass that copy into router.update. Also ensure duplicate adapters are not
re-added when hydrateStart runs again (for retries/HMR) by deduping based on
adapter identity before updating the router.

---

Nitpick comments:
In `@e2e/react-start/serialization-adapters/src/client.tsx`:
- Around line 39-44: The Promise.catch handler in client.tsx is reading
`error?.name` and `error?.message` from an untyped rejection, which violates
strict type safety; update the catch in the client entry flow to treat the
rejection as `unknown` first, then narrow it with a checked shape before
building `window.__serializationAdapterClientEntryResult`. Use the existing
catch block in the client entry logic to extract `name` and `message` only after
validation, and fall back safely for non-error values.

In `@e2e/react-start/serialization-adapters/tests/app.spec.ts`:
- Around line 86-90: The test predicate in app.spec.ts is bypassing type safety
by using window as any when reading __serializationAdapterClientEntryResult.
Replace that cast with a small local TypeScript type that describes the
browser-side result shape, and use it both in the waitForFunction predicate and
the final jsonValue/assertion so they stay aligned. Keep the change scoped to
the existing page.waitForFunction flow and the
__serializationAdapterClientEntryResult contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4a3f9a71-fce9-4c62-9871-929251eaefb8

📥 Commits

Reviewing files that changed from the base of the PR and between bb2daa6 and 85cabb9.

📒 Files selected for processing (14)
  • .changeset/calm-adapters-serialize.md
  • e2e/react-start/serialization-adapters/src/client-entry-server-functions.ts
  • e2e/react-start/serialization-adapters/src/client.tsx
  • e2e/react-start/serialization-adapters/tests/app.spec.ts
  • packages/router-core/src/index.ts
  • packages/start-client-core/src/client-rpc/createClientRpc.ts
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts
  • packages/start-client-core/src/client/hydrateStart.ts
  • packages/start-client-core/src/createServerFn.ts
  • packages/start-client-core/src/getDefaultSerovalPlugins.ts
  • packages/start-client-core/src/getStartOptions.ts
  • packages/start-client-core/src/global.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-server-core/src/server-functions-handler.ts
💤 Files with no reviewable changes (1)
  • packages/start-client-core/src/global.ts

Comment on lines +114 to +116
startOptions: AnyStartInstanceOptions | undefined,
) {
if (!serovalPlugins) {
serovalPlugins = getDefaultSerovalPlugins()
}
serovalPlugins ||= getDefaultSerovalPlugins(startOptions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't freeze client seroval plugins on the first call.

serovalPlugins ||= ... snapshots the adapters from the first invocation. packages/start-client-core/src/client/hydrateStart.ts:19-32 later mutates startOptions.serializationAdapters, so any pre-hydration call can leave this fetch path permanently missing adapters added during hydration. Since this function is async, the module-level cache also creates cross-call leakage if different startOptions are observed concurrently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-client-core/src/client-rpc/serverFnFetcher.ts` around lines
114 - 116, The seroval plugin cache in serverFnFetcher should not be initialized
with a one-time fallback because it can lock in stale adapters from the first
call. Update the logic around getDefaultSerovalPlugins and serovalPlugins so it
recomputes or refreshes based on the current startOptions instead of using ||=,
and avoid module-level shared state that can leak across concurrent calls. Make
sure the behavior still works with hydrateStart mutating serializationAdapters
before later fetches.

Comment on lines +9 to +11
export function getDefaultSerovalPlugins(
start: AnyStartInstanceOptions | undefined,
): Array<Plugin<any, any>> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=ts '\bgetDefaultSerovalPlugins\s*\(' packages

Repository: TanStack/router

Length of output: 773


🏁 Script executed:

#!/bin/bash
sed -n '1,80p' packages/start-client-core/src/getDefaultSerovalPlugins.ts
printf '\n---\n'
sed -n '88,132p' packages/start-static-server-functions/src/staticFunctionMiddleware.ts
printf '\n---\n'
sed -n '100,130p' packages/start-client-core/src/client-rpc/serverFnFetcher.ts
printf '\n---\n'
sed -n '60,90p' packages/start-server-core/src/server-functions-handler.ts

Repository: TanStack/router

Length of output: 3520


Make start optional. AnyStartInstanceOptions | undefined still requires an argument, so the zero-arg calls in packages/start-static-server-functions/src/staticFunctionMiddleware.ts:100 and :123 don’t match this signature. start?: AnyStartInstanceOptions aligns with the existing call sites.

Proposed fix
 export function getDefaultSerovalPlugins(
-  start: AnyStartInstanceOptions | undefined,
+  start?: AnyStartInstanceOptions,
 ): Array<Plugin<any, any>> {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function getDefaultSerovalPlugins(
start: AnyStartInstanceOptions | undefined,
): Array<Plugin<any, any>> {
export function getDefaultSerovalPlugins(
start?: AnyStartInstanceOptions,
): Array<Plugin<any, any>> {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-client-core/src/getDefaultSerovalPlugins.ts` around lines 9 -
11, `getDefaultSerovalPlugins` currently declares `start` as
`AnyStartInstanceOptions | undefined`, which still forces callers to pass an
argument and breaks the zero-arg uses in `staticFunctionMiddleware`. Update the
function signature in `getDefaultSerovalPlugins` to make `start` optional with
`start?: AnyStartInstanceOptions`, and keep the existing handling inside the
function compatible with an omitted value.

Comment on lines 70 to +72
// Initialize serovalPlugins lazily (cached at module level)
if (!serovalPlugins) {
serovalPlugins = getDefaultSerovalPlugins()
serovalPlugins = getDefaultSerovalPlugins(startOptions)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the server plugin list request-scoped.

handleServerAction now receives startOptions per call, but this cache only honors the first one. Later requests or handler instances with different serializationAdapters will still use the first plugin set.

Proposed fix
-  if (!serovalPlugins) {
-    serovalPlugins = getDefaultSerovalPlugins(startOptions)
-  }
+  const serovalPlugins = getDefaultSerovalPlugins(startOptions)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Initialize serovalPlugins lazily (cached at module level)
if (!serovalPlugins) {
serovalPlugins = getDefaultSerovalPlugins()
serovalPlugins = getDefaultSerovalPlugins(startOptions)
// Initialize serovalPlugins lazily (cached at module level)
const serovalPlugins = getDefaultSerovalPlugins(startOptions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/src/server-functions-handler.ts` around lines 70 -
72, The plugin cache in handleServerAction is incorrectly shared across
requests, so it only reflects the first startOptions/serializationAdapters it
sees. Remove the module-level serovalPlugins reuse and make the plugin list
request-scoped by computing it from the current startOptions inside
handleServerAction for each call, or otherwise key the cache by the incoming
options. Keep the fix centered around handleServerAction and
getDefaultSerovalPlugins so each request gets the correct server plugin set.

@codspeed-hq

codspeed-hq Bot commented Jun 27, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 3 improved benchmarks
❌ 2 regressed benchmarks
✅ 139 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Memory mem serialization-payload (vue) 6.8 MB 9.5 MB -28.77%
Memory mem serialization-payload (solid) 6.8 MB 7.1 MB -3.3%
Memory mem aborted-requests (solid) 2.4 MB 1.9 MB +24.03%
Memory mem peak-large-page (solid) 3.9 MB 3.4 MB +14.19%
Memory mem aborted-requests (vue) 1,021 KB 935.4 KB +9.15%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing fix-7706 (85cabb9) with main (ba52d2b)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (bb2daa6) during the generation of this report, so ba52d2b was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nx Cloud is proposing a fix for your failed CI:

We updated staticFunctionMiddleware.ts to pass getStartOptions() to both calls of getDefaultSerovalPlugins, fixing the TypeScript error introduced when the PR changed the function signature to require a start argument. This call site in @tanstack/start-static-server-functions was the only one not updated alongside the refactor in @tanstack/start-client-core.

Tip

We verified this fix by re-running @tanstack/start-static-server-functions:build, @tanstack/start-static-server-functions:test:types.

Suggested Fix changes
diff --git a/packages/start-client-core/src/index.tsx b/packages/start-client-core/src/index.tsx
index 5bf09c44..b322d19d 100644
--- a/packages/start-client-core/src/index.tsx
+++ b/packages/start-client-core/src/index.tsx
@@ -130,6 +130,7 @@ export type { Register } from '@tanstack/router-core'
 
 export { getRouterInstance } from './getRouterInstance'
 export { getDefaultSerovalPlugins } from './getDefaultSerovalPlugins'
+export { getStartOptions } from './getStartOptions'
 export { getGlobalStartContext } from './getGlobalStartContext'
 export { safeObjectMerge, createNullProtoObject } from './safeObjectMerge'
 export { trackPostProcessPromise } from './client-rpc/serverFnFetcher'
diff --git a/packages/start-static-server-functions/src/staticFunctionMiddleware.ts b/packages/start-static-server-functions/src/staticFunctionMiddleware.ts
index e6681fce..f8cc6f06 100644
--- a/packages/start-static-server-functions/src/staticFunctionMiddleware.ts
+++ b/packages/start-static-server-functions/src/staticFunctionMiddleware.ts
@@ -3,6 +3,7 @@ import path from 'node:path'
 import {
   createMiddleware,
   getDefaultSerovalPlugins,
+  getStartOptions,
 } from '@tanstack/start-client-core'
 import { fromJSON, toJSONAsync } from 'seroval'
 
@@ -97,7 +98,7 @@ async function addItemToCache({
           result: response.result,
           context: response.context.sendContext,
         },
-        { plugins: getDefaultSerovalPlugins() },
+        { plugins: getDefaultSerovalPlugins(getStartOptions()) },
       ),
     )
     await fs.writeFile(filePath, stringifiedResult, 'utf-8')
@@ -120,7 +121,9 @@ const fetchItem = async ({
     method: 'GET',
   })
     .then((r) => r.json())
-    .then((d) => fromJSON(d, { plugins: getDefaultSerovalPlugins() }))
+    .then((d) =>
+      fromJSON(d, { plugins: getDefaultSerovalPlugins(getStartOptions()) }),
+    )
 
   return result
 }

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally XqTW-a3TK

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Calling any server function from the client entry point breaks custom serialization adapters

1 participant