Skip to content

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Oct 16, 2025

fixes #5416

Summary by CodeRabbit

  • New Features

    • Added primitives route for testing server function capabilities.
  • Improvements

    • Enhanced server function request and response handling with improved payload serialization.
    • Added SSR query integration support for better server-side rendering performance.
    • Improved error logging and request validation for server functions.
  • Tests

    • Added comprehensive end-to-end tests for server function primitives.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 16, 2025

Walkthrough

This PR fixes a regression where null values sent to server functions were converted to undefined, and adds SSR query integration. It refactors client-side payload serialization and server-side parsing logic, introduces a new primitives test route to validate type handling, and extends routing to support the test endpoint.

Changes

Cohort / File(s) Summary
Dependencies
e2e/react-start/server-functions/package.json
Added @tanstack/react-query and @tanstack/react-router-ssr-query dependencies for SSR query support.
Route Setup
e2e/react-start/server-functions/src/routeTree.gen.ts, e2e/react-start/server-functions/src/router.tsx
Generated /primitives route wired into route tree and type maps; configured SSR query integration with QueryClient setup.
Primitives Test Route
e2e/react-start/server-functions/src/routes/primitives/index.tsx
New route exposing test harness for server functions with primitive types (null, undefined, string) via paired POST/GET handlers and React Query-driven UI.
E2E Test
e2e/react-start/server-functions/tests/server-functions.spec.ts
Added test block validating primitives route by comparing expected vs. actual results for each test case.
Null/Undefined Handling
packages/start-client-core/src/client-rpc/serverFnFetcher.ts
Refactored payload serialization and request body construction to explicitly preserve null values and properly handle undefined payloads across GET and POST flows; introduced getFetchBody utility.
Server-Side Payload Parsing
packages/start-server-core/src/server-functions-handler.ts
Updated GET and POST payload parsing to use safe defaults ({}) for missing payloads and conditionally parse JSON only when content-type exactly matches application/json.

Sequence Diagram

sequenceDiagram
    participant Client
    participant ServerFn as Server<br/>(Fetcher)
    participant Handler as Server<br/>(Handler)
    participant DB as Test Route
    
    rect rgb(220, 240, 255)
    Note over Client,Handler: Old Flow (Bug: null → undefined)
    Client->>ServerFn: POST { body: null }
    ServerFn->>ServerFn: serializePayload()<br/>(treats null as falsy)
    ServerFn->>Handler: payload: undefined
    Handler->>DB: receives undefined
    end
    
    rect rgb(220, 255, 220)
    Note over Client,Handler: New Flow (Fixed: null preserved)
    Client->>ServerFn: POST { body: null }
    ServerFn->>ServerFn: serializePayload()<br/>(explicitly checks for null)
    ServerFn->>Handler: payload: "null"<br/>(serialized JSON)
    Handler->>Handler: parsePayload()<br/>(JSON.parse preserves null)
    Handler->>DB: receives null
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

The PR spans multiple layers (client fetcher, server handler, routing, test harness) with heterogeneous changes. The core logic modifications in payload serialization and null-preservation require careful reasoning to ensure type safety and backward compatibility, while the new test route introduces a complex component hierarchy with strongly-typed generics and React Query integration.

Possibly related PRs

  • TanStack/router#5276: Modifies the same client/server request-response files (serverFnFetcher.ts, server-functions-handler.ts) to refine request/response handling, indicating shared payload serialization concerns.
  • TanStack/router#5390: Updates the same route tree generation file (routeTree.gen.ts) to add file-based routes and wire them into type maps, following the same route scaffolding pattern.

Suggested labels

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

Suggested reviewers

  • chorobin

Poem

🐰 A null walks into a server, a undefined walks out—
No more! Our fuzzy friend says with a shout!
Primitives tested, types now flow true,
Where null stays null, and undefined's due. 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "fix: undefined / null serialization" directly and concisely describes the main objective of the changeset. The core modifications to serverFnFetcher.ts and server-functions-handler.ts address the serialization and deserialization of null and undefined values, which is exactly what the title indicates. The title is specific enough to clearly communicate the primary change without being verbose or including extraneous information.
Linked Issues Check ✅ Passed The pull request directly addresses issue #5416 by fixing the bug where null values sent to serverFn handlers are incorrectly converted to undefined. The code changes modify payload serialization in serverFnFetcher.ts to properly handle null and undefined values during transmission, and update server-side payload parsing in server-functions-handler.ts to correctly deserialize these values. Additionally, comprehensive test coverage is added via a new primitives test route that explicitly validates that null, undefined, and string values are correctly preserved through the serverFn call cycle, directly confirming the fix works as expected [#5416].
Out of Scope Changes Check ✅ Passed All changes in the pull request are directly related to fixing and validating the null/undefined serialization bug reported in #5416. The core fix is in serverFnFetcher.ts and server-functions-handler.ts, while supporting changes include adding a new test route (primitives/index.tsx), related dependencies, and generated route configuration files (routeTree.gen.ts, router.tsx, package.json) necessary to scaffold and run the test infrastructure. The e2e test additions validate the fix works correctly. No extraneous or unrelated changes are present.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-5416

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

@nx-cloud
Copy link

nx-cloud bot commented Oct 16, 2025

View your CI Pipeline Execution ↗ for commit 7695b8a

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 3m 56s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 27s View ↗

☁️ Nx Cloud last updated this comment at 2025-10-16 21:34:31 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 16, 2025

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@5501

@tanstack/directive-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/directive-functions-plugin@5501

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@5501

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@5501

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@5501

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@5501

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@5501

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@5501

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@5501

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@5501

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@5501

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@5501

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@5501

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@5501

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@5501

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@5501

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@5501

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@5501

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@5501

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@5501

@tanstack/server-functions-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/server-functions-plugin@5501

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@5501

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@5501

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@5501

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@5501

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@5501

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@5501

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@5501

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@5501

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@5501

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@5501

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@5501

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@5501

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@5501

commit: 7695b8a

@schiller-manuel schiller-manuel merged commit ad66e89 into main Oct 16, 2025
5 of 6 checks passed
@schiller-manuel schiller-manuel deleted the fix-5416 branch October 16, 2025 21:35
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 (2)
e2e/react-start/server-functions/src/router.tsx (1)

1-6: Fix import order to satisfy lint

The new package imports must precede relative imports; otherwise the import/order rule keeps failing. Reorder these top-level imports so CI passes.

-import { createRouter } from '@tanstack/react-router'
-import { routeTree } from './routeTree.gen'
-import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
-import { NotFound } from './components/NotFound'
-import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
-import { QueryClient } from '@tanstack/react-query'
+import { QueryClient } from '@tanstack/react-query'
+import { setupRouterSsrQueryIntegration } from '@tanstack/react-router-ssr-query'
+import { createRouter } from '@tanstack/react-router'
+import { routeTree } from './routeTree.gen'
+import { DefaultCatchBoundary } from './components/DefaultCatchBoundary'
+import { NotFound } from './components/NotFound'

As per coding guidelines

packages/start-server-core/src/server-functions-handler.ts (1)

135-135: Add validation or fallback for undefined jsonPayload when handling non-createServerFn POST requests.

Line 135 spreads jsonPayload without checking if it's defined. For non-createServerFn POST requests with a content-type other than application/json, jsonPayload remains undefined, causing a TypeError. Unlike the isCreateServerFn branch (line 129) which safely handles this with jsonPayload ? parsePayload(jsonPayload) : {}, the non-createServerFn path has no such fallback.

Either add validation to reject non-JSON POST requests, or provide a safe default like:

return await action(...(jsonPayload ?? []))
🧹 Nitpick comments (1)
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (1)

55-59: Avoid double serializing the GET payload

serializePayload already ran on Line 55, but we invoke it again when building encodedPayload. Seroval serialization is comparatively heavy, so this double work adds needless latency. Reuse the cached string instead.

-      const serializedPayload = await serializePayload(first)
-      if (serializedPayload !== undefined) {
-        const encodedPayload = encode({
-          payload: await serializePayload(first),
-        })
+      const serializedPayload = await serializePayload(first)
+      if (serializedPayload !== undefined) {
+        const encodedPayload = encode({
+          payload: serializedPayload,
+        })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 60ff0e4 and 7695b8a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (7)
  • e2e/react-start/server-functions/package.json (1 hunks)
  • e2e/react-start/server-functions/src/routeTree.gen.ts (11 hunks)
  • e2e/react-start/server-functions/src/router.tsx (2 hunks)
  • e2e/react-start/server-functions/src/routes/primitives/index.tsx (1 hunks)
  • e2e/react-start/server-functions/tests/server-functions.spec.ts (1 hunks)
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts (5 hunks)
  • packages/start-server-core/src/server-functions-handler.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript in strict mode with extensive type safety across the codebase

Files:

  • e2e/react-start/server-functions/tests/server-functions.spec.ts
  • e2e/react-start/server-functions/src/router.tsx
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • e2e/react-start/server-functions/src/routes/primitives/index.tsx
  • packages/start-server-core/src/server-functions-handler.ts
e2e/**

📄 CodeRabbit inference engine (AGENTS.md)

Store end-to-end tests under the e2e/ directory

Files:

  • e2e/react-start/server-functions/tests/server-functions.spec.ts
  • e2e/react-start/server-functions/src/router.tsx
  • e2e/react-start/server-functions/package.json
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • e2e/react-start/server-functions/src/routes/primitives/index.tsx
packages/{*-start,start-*}/**

📄 CodeRabbit inference engine (AGENTS.md)

Name and place Start framework packages under packages/-start/ or packages/start-/

Files:

  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts
  • packages/start-server-core/src/server-functions-handler.ts
**/package.json

📄 CodeRabbit inference engine (AGENTS.md)

Use workspace:* protocol for internal dependencies in package.json files

Files:

  • e2e/react-start/server-functions/package.json
**/src/routes/**

📄 CodeRabbit inference engine (AGENTS.md)

Place file-based routes under src/routes/ directories

Files:

  • e2e/react-start/server-functions/src/routes/primitives/index.tsx
🧬 Code graph analysis (2)
e2e/react-start/server-functions/src/router.tsx (1)
packages/react-router-ssr-query/src/index.tsx (1)
  • setupRouterSsrQueryIntegration (12-29)
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (1)
packages/start-client-core/src/constants.ts (1)
  • TSS_FORMDATA_CONTEXT (1-1)
🪛 ESLint
e2e/react-start/server-functions/src/router.tsx

[error] 5-5: @tanstack/react-router-ssr-query import should occur before import of ./routeTree.gen

(import/order)


[error] 6-6: @tanstack/react-query import should occur before import of ./routeTree.gen

(import/order)

e2e/react-start/server-functions/src/routes/primitives/index.tsx

[error] 5-5: Expected 1 empty line after import statement not followed by another import.

(import/newline-after-import)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test

Comment on lines +109 to 110
payload = payload ? parsePayload(JSON.parse(payload)) : {}
payload.context = { ...context, ...payload.context }
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Risk: Potential null-pointer exception if payload is null.

While the string check on line 109 correctly handles the string "null" by passing the parsed null value to parsePayload(), if parsePayload() returns null (which it should to preserve the null value), line 110 will throw a TypeError when attempting to access .context on null.

Consider wrapping the payload in a safe structure or checking if it's an object before accessing properties:

 let payload: any = search.payload
 payload = payload ? parsePayload(JSON.parse(payload)) : {}
-payload.context = { ...context, ...payload.context }
+if (typeof payload === 'object' && payload !== null) {
+  payload.context = { ...context, ...payload.context }
+} else {
+  payload = { data: payload, context }
+}

Alternatively, if primitives should not be supported for GET requests, add validation and return an error.

📝 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
payload = payload ? parsePayload(JSON.parse(payload)) : {}
payload.context = { ...context, ...payload.context }
payload = payload ? parsePayload(JSON.parse(payload)) : {}
if (typeof payload === 'object' && payload !== null) {
payload.context = { ...context, ...payload.context }
} else {
payload = { data: payload, context }
}
🤖 Prompt for AI Agents
In packages/start-server-core/src/server-functions-handler.ts around lines
109-110, payload can be null after parsePayload(JSON.parse(payload)) which
causes a TypeError when accessing payload.context; fix by checking that payload
is a non-null object before merging context (e.g., if typeof payload ===
'object' && payload !== null then merge context into payload.context, otherwise
set payload = { context: { ...context } } or return a validation error for
unsupported primitives), ensuring no property access occurs on null.

if (!contentType || !contentType.includes('application/json')) {
throw new Error('expected application/json content type')
let jsonPayload
if (contentType === 'application/json') {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Content-type check should handle charset and other parameters.

The exact equality check contentType === 'application/json' will fail if the header includes additional parameters like application/json; charset=utf-8, which is common with fetch requests. The existing FormData check on lines 71-73 uses .includes() for this reason.

Apply this diff to make the check more robust:

-if (contentType === 'application/json') {
+if (contentType?.includes('application/json')) {
   jsonPayload = await request.json()
 }
📝 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
if (contentType === 'application/json') {
if (contentType?.includes('application/json')) {
🤖 Prompt for AI Agents
In packages/start-server-core/src/server-functions-handler.ts around line 120,
the content-type equality check uses `contentType === 'application/json'` which
fails when the header contains parameters like charset; update the check to
safely handle null/undefined and match types with parameters by using a
normalized check such as
`contentType?.toLowerCase().startsWith('application/json')` (or
`.includes('application/json')`) so requests like `application/json;
charset=utf-8` are accepted.

Comment on lines +127 to 129
const payload = jsonPayload ? parsePayload(jsonPayload) : {}
payload.context = { ...payload.context, ...context }
return await action(payload, signal)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Falsy check converts null to empty object, breaking null serialization.

This is the root cause of issue #5416. When jsonPayload is null, the falsy check jsonPayload ? evaluates to false and returns {} instead of preserving the null value. The same issue affects other falsy primitives like 0, false, and "".

Additionally, line 128 will throw a TypeError if payload is null or any non-object primitive when attempting to access .context.

Apply this diff to preserve null and other falsy values while still defaulting to {} for missing payloads:

-const payload = jsonPayload ? parsePayload(jsonPayload) : {}
-payload.context = { ...payload.context, ...context }
+let payload = jsonPayload !== undefined ? parsePayload(jsonPayload) : {}
+if (typeof payload === 'object' && payload !== null) {
+  payload.context = { ...payload.context, ...context }
+} else {
+  payload = { data: payload, context }
+}

This change:

  • Preserves null by checking !== undefined instead of truthiness
  • Prevents TypeError by checking if payload is an object before accessing .context
  • Wraps primitives in an object structure if needed
📝 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
const payload = jsonPayload ? parsePayload(jsonPayload) : {}
payload.context = { ...payload.context, ...context }
return await action(payload, signal)
let payload = jsonPayload !== undefined ? parsePayload(jsonPayload) : {}
if (typeof payload === 'object' && payload !== null) {
payload.context = { ...payload.context, ...context }
} else {
payload = { data: payload, context }
}
return await action(payload, signal)
🤖 Prompt for AI Agents
In packages/start-server-core/src/server-functions-handler.ts around lines 127
to 129, the current truthy check converts null and other falsy primitives to {}
and then accessing payload.context can throw; change the logic to use
jsonPayload !== undefined when deciding to call parsePayload so null (and other
falsy values) are preserved by parsePayload, then ensure payload is an object
before mutating .context: if payload is null or a non-object primitive, wrap it
into an object (e.g., { value: payload }) so you can safely set payload.context;
finally, default to {} only when jsonPayload is actually undefined.

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.

ServerFn started converting null to undefined in RC version

2 participants