Skip to content

Conversation

@tannerlinsley
Copy link
Collaborator

@tannerlinsley tannerlinsley commented Oct 17, 2025

Summary by CodeRabbit

  • New Features

    • Added a new page and navigation link for "Server Functions Middleware Unhandled Exception" E2E tests.
  • Bug Fixes

    • Improved middleware execution and client/server error/redirect propagation for more consistent handling.
    • Enhanced server response handling to support streaming and structured server-function payloads.
  • Chores

    • Removed a legacy middleware export from the public API.
    • Added a context header constant and updated client/server fetch plumbing to support the new server-function flow.

@nx-cloud
Copy link

nx-cloud bot commented Oct 17, 2025

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit ed54879

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

☁️ Nx Cloud last updated this comment at 2025-10-19 21:07:11 UTC

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 17, 2025

Walkthrough

Adds a new E2E route /middleware/unhandled-exception with an auth middleware that throws; refactors server/client middleware and server-function request/response handling across start-client-core and start-server-core, and updates exports/constants and client fetcher behavior.

Changes

Cohort / File(s) Summary
Route tree & registration
e2e/react-start/server-functions/src/routeTree.gen.ts
Import and register MiddlewareUnhandledExceptionRoute, add to rootRouteChildren, and include in FileRoutesByFullPath/FileRoutesByTo/FileRoutesById and TanStack Router augmentations.
New route + navigation
e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx, e2e/react-start/server-functions/src/routes/index.tsx
New file route /middleware/unhandled-exception with authMiddleware (throws Unauthorized), personServerFn using middleware chain, loader and component; index adds nav link labeled "Server Functions Middleware Unhandled Exception E2E tests".
Client middleware orchestration
packages/start-client-core/src/createServerFn.ts, packages/start-client-core/src/index.tsx, packages/start-client-core/src/constants.ts
Major refactor of middleware execution to callNextMiddleware/userNext wrapper flow, await middleware results, centralize error/redirect handling; removed public applyMiddleware export; added X_TSS_CONTEXT constant export.
Client RPC / fetcher
packages/start-client-core/src/client-rpc/serverFnFetcher.ts
New handler parameter and proxied fetch path; adds serverFn headers, supports serialized/streamed server-function responses (NDJSON / cross-JSON), and reworks JSON/redirect/not-found handling.
Server request/response handling
packages/start-server-core/src/createStartHandler.ts, packages/start-server-core/src/server-functions-handler.ts
Adjusted header checks (use x-tsr-createServerFn), removed middleware context propagation from execute call, added debug logging, added form-data detection, reworked payload parsing and response serialization (streaming NDJSON / cross-JSON), set X_TSS_RAW_RESPONSE for raw Responses.
Small UI text change
e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx
Minor copy update in displayed guidance text.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Caller (client/serverFn)
    participant Runner as createServerFn.execute
    participant Chain as callNextMiddleware
    participant MW as Middleware (authMiddleware / others)
    participant Handler as Final Handler / ServerFn
    participant Fetcher as serverFnFetcher
    participant Server as start-server-core handler

    Client->>Fetcher: invoke(serverFn args, handler)
    Fetcher->>Server: proxied fetch with serverFn headers
    Server->>Runner: handleServerAction -> execute middlewares
    Runner->>Chain: start(index=0, ctx)
    Chain->>MW: execute(ctx, userNext)
    alt MW calls userNext
        MW->>Chain: userNext(mergedCtx) -> next index
    else MW throws
        MW-->>Runner: error (propagated)
    end
    opt reached handler
        Chain->>Handler: invoke with final ctx
        Handler-->>Server: result or error
    end
    Server-->>Fetcher: serialized or raw response (NDJSON/JSON/Response)
    Fetcher-->>Client: parsed/streamed result or redirect/error
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Areas needing extra attention:

  • packages/start-client-core/src/createServerFn.ts — new middleware control-flow, userNext semantics, and removal of applyMiddleware.
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts — streaming/NDJSON and proxied fetch handling.
  • packages/start-server-core/src/server-functions-handler.ts — serializeResult and ctx-result unwrapping, Content-Type/X_TSS headers and Response construction.
  • Integration between client fetcher and server serialized responses (headers: x-tsr-createServerFn, x-tsr-serverFn, X_TSS_RAW_RESPONSE, X_TSS_SERIALIZED).

Possibly related PRs

Suggested reviewers

  • schiller-manuel

Poem

🐰 I hopped through middleware, one by one,
A token tossed, an Unauthorized run,
I built a route for tests at dawn,
Streams and headers hum along—
I nibble logs and hum this song.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "fix: refactor middleware" is related to the primary changes in the changeset, particularly the significant middleware execution refactoring in packages/start-client-core/src/createServerFn.ts. This file underwent substantial changes including reworked client/server execution flow, expanded server-side middleware handling, middleware execution refactoring with new internal wrappers, and enhanced error handling. Supporting changes across multiple files (serverFnFetcher.ts, server-functions-handler.ts, createStartHandler.ts) appear designed to integrate and support this middleware refactoring. The title accurately captures the core intent of the PR, though it does not encompass the full scope of related server function handling changes.
✨ 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/refactor-middleware

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

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 17, 2025

More templates

@tanstack/arktype-adapter

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

@tanstack/directive-functions-plugin

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

@tanstack/eslint-plugin-router

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

@tanstack/history

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

@tanstack/nitro-v2-vite-plugin

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

@tanstack/react-router

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

@tanstack/react-router-devtools

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

@tanstack/react-router-ssr-query

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

@tanstack/react-start

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

@tanstack/react-start-client

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

@tanstack/react-start-server

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

@tanstack/router-cli

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

@tanstack/router-core

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

@tanstack/router-devtools

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

@tanstack/router-devtools-core

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

@tanstack/router-generator

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

@tanstack/router-plugin

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

@tanstack/router-ssr-query-core

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

@tanstack/router-utils

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

@tanstack/router-vite-plugin

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

@tanstack/server-functions-plugin

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

@tanstack/solid-router

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

@tanstack/solid-router-devtools

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

@tanstack/solid-start

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

@tanstack/solid-start-client

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

@tanstack/solid-start-server

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

@tanstack/start-client-core

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

@tanstack/start-plugin-core

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

@tanstack/start-server-core

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

@tanstack/start-static-server-functions

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

@tanstack/start-storage-context

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

@tanstack/valibot-adapter

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

@tanstack/virtual-file-routes

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

@tanstack/zod-adapter

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

commit: ed54879

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: 2

🧹 Nitpick comments (3)
packages/start-client-core/src/createServerFn.ts (3)

259-263: Check for undefined explicitly and improve the error message.

if (!(result as any)) treats falsy values as undefined and the message doesn’t include context. Prefer a strict undefined check and include env/functionId for easier debugging.

-        if (!(result as any)) {
-          throw new Error(
-            'User middleware returned undefined. You must call next() or return a result in your middlewares.',
-          )
-        }
+        if (typeof result === 'undefined') {
+          throw new Error(
+            `User middleware returned undefined (env=${env}, functionId=${opts.functionId}). You must call next() or return a result.`
+          )
+        }

182-190: Optional: avoid mutating the flattened array with shift().

Using shift() mutates shared state in the closure. An index-based callNextMiddleware(ctx, i) is clearer and safer if you ever need re-entrancy or debug indices.

I can provide a small refactor to pass an index parameter instead of mutating the array if you want to pursue this.


146-154: Style consistency: prefer await over .then here.

Elsewhere you use await; using it here keeps style consistent and simplifies stack traces.

-            return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
-              (d) => ({
-                // Only send the result and sendContext back to the client
-                result: d.result,
-                error: d.error,
-                context: d.sendContext,
-              }),
-            )
+            const d = await executeMiddleware(resolvedMiddleware, 'server', ctx)
+            return {
+              // Only send the result and sendContext back to the client
+              result: d.result,
+              error: d.error,
+              context: d.sendContext,
+            }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7978449 and 55b636c.

📒 Files selected for processing (5)
  • e2e/react-start/server-functions/src/routeTree.gen.ts (11 hunks)
  • e2e/react-start/server-functions/src/routes/index.tsx (1 hunks)
  • e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx (1 hunks)
  • packages/start-client-core/src/createServerFn.ts (3 hunks)
  • packages/start-client-core/src/index.tsx (0 hunks)
💤 Files with no reviewable changes (1)
  • packages/start-client-core/src/index.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{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/src/routes/index.tsx
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx
  • packages/start-client-core/src/createServerFn.ts
**/src/routes/**

📄 CodeRabbit inference engine (AGENTS.md)

Place file-based routes under src/routes/ directories

Files:

  • e2e/react-start/server-functions/src/routes/index.tsx
  • e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx
e2e/**

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • e2e/react-start/server-functions/src/routes/index.tsx
  • e2e/react-start/server-functions/src/routeTree.gen.ts
  • e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.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/createServerFn.ts
🧬 Code graph analysis (2)
e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx (1)
packages/start-client-core/src/createServerFn.ts (1)
  • createServerFn (48-169)
packages/start-client-core/src/createServerFn.ts (2)
packages/start-client-core/src/index.tsx (4)
  • executeMiddleware (79-79)
  • NextFn (71-71)
  • ServerFnMiddlewareResult (68-68)
  • mergeHeaders (6-6)
packages/router-core/src/ssr/client.ts (1)
  • mergeHeaders (1-1)
⏰ 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: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
e2e/react-start/server-functions/src/routes/index.tsx (1)

91-95: LGTM: adds nav to new middleware error route.

e2e/react-start/server-functions/src/routes/middleware/unhandled-exception.tsx (1)

1-33: Intent check: loader will reject due to middleware throw.

This looks correct for exercising the unhandled-exception path. Ensure the E2E asserts the error boundary/response instead of relying on the component render.

Can you confirm the E2E expects a rejected loader (e.g., checks for router error state) rather than querying data-testid="regular-person"?

e2e/react-start/server-functions/src/routeTree.gen.ts (1)

30-33: Generated: additions look consistent with the new route.

No action; keep as-is.

Also applies to: 127-132, 180-181, 206-207, 233-234, 261-262, 287-288, 313-314, 340-341, 477-483

Comment on lines 214 to 251
// Execute the middleware
try {
if (middlewareFn) {
const userNext = async (
userCtx: ServerFnMiddlewareResult | undefined = {} as any,
) => {
// Return the next middleware
const nextCtx = {
...ctx,
...userCtx,
context: {
...ctx.context,
...userCtx.context,
},
sendContext: {
...ctx.sendContext,
...(userCtx.sendContext ?? {}),
},
headers: mergeHeaders(ctx.headers, userCtx.headers),
result:
userCtx.result !== undefined
? userCtx.result
: userCtx instanceof Response
? userCtx
: (ctx as any).result,
error: userCtx.error ?? (ctx as any).error,
}

try {
return await callNextMiddleware(nextCtx)
} catch (error: any) {
return {
...newCtx,
...nextCtx,
error,
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Prevent multiple next() calls within a single middleware.

As written, a middleware can call next() multiple times, advancing the chain repeatedly and producing hard-to-debug behavior. Add a simple guard.

-      if (middlewareFn) {
-        const userNext = async (
+      if (middlewareFn) {
+        let nextCalled = false
+        const userNext = async (
           userCtx: ServerFnMiddlewareResult | undefined = {} as any,
         ) => {
+          if (nextCalled) {
+            throw new Error('next() called multiple times in a middleware')
+          }
+          nextCalled = true
           // Return the next middleware
           const nextCtx = {
             ...ctx,
             ...userCtx,
             context: {
               ...ctx.context,
               ...userCtx.context,
             },
             sendContext: {
               ...ctx.sendContext,
               ...(userCtx.sendContext ?? {}),
             },
             headers: mergeHeaders(ctx.headers, userCtx.headers),
             result:
               userCtx.result !== undefined
                 ? userCtx.result
                 : userCtx instanceof Response
                   ? userCtx
                   : (ctx as any).result,
             error: userCtx.error ?? (ctx as any).error,
           }
 
           try {
             return await callNextMiddleware(nextCtx)
           } catch (error: any) {
             return {
               ...nextCtx,
               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
// Execute the middleware
try {
if (middlewareFn) {
const userNext = async (
userCtx: ServerFnMiddlewareResult | undefined = {} as any,
) => {
// Return the next middleware
const nextCtx = {
...ctx,
...userCtx,
context: {
...ctx.context,
...userCtx.context,
},
sendContext: {
...ctx.sendContext,
...(userCtx.sendContext ?? {}),
},
headers: mergeHeaders(ctx.headers, userCtx.headers),
result:
userCtx.result !== undefined
? userCtx.result
: userCtx instanceof Response
? userCtx
: (ctx as any).result,
error: userCtx.error ?? (ctx as any).error,
}
try {
return await callNextMiddleware(nextCtx)
} catch (error: any) {
return {
...newCtx,
...nextCtx,
error,
}
}
}
// Execute the middleware
try {
if (middlewareFn) {
let nextCalled = false
const userNext = async (
userCtx: ServerFnMiddlewareResult | undefined = {} as any,
) => {
if (nextCalled) {
throw new Error('next() called multiple times in a middleware')
}
nextCalled = true
// Return the next middleware
const nextCtx = {
...ctx,
...userCtx,
context: {
...ctx.context,
...userCtx.context,
},
sendContext: {
...ctx.sendContext,
...(userCtx.sendContext ?? {}),
},
headers: mergeHeaders(ctx.headers, userCtx.headers),
result:
userCtx.result !== undefined
? userCtx.result
: userCtx instanceof Response
? userCtx
: (ctx as any).result,
error: userCtx.error ?? (ctx as any).error,
}
try {
return await callNextMiddleware(nextCtx)
} catch (error: any) {
return {
...nextCtx,
error,
}
}
}
🤖 Prompt for AI Agents
In packages/start-client-core/src/createServerFn.ts around lines 214 to 251, the
userNext function allows a middleware to call next() multiple times; add a
simple reentrancy guard inside the userNext closure (e.g., a boolean "called"
flag set on first invocation) that throws or returns an error if next() is
invoked again, ensuring subsequent calls are rejected; keep the rest of next's
behavior (building nextCtx, calling callNextMiddleware, and catching errors)
unchanged but bail early when the guard detects a repeated call.

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: 1

♻️ Duplicate comments (2)
packages/start-client-core/src/createServerFn.ts (2)

114-125: Still dropping caller-provided context in client fetcher.

Line 120 sets context: {}, ignoring any opts?.context passed by the caller. This prevents client-side middlewares from receiving user-supplied context.

Apply this fix:

 const result = await executeMiddleware(resolvedMiddleware, 'client', {
   ...extractedFn,
   ...newOptions,
   data: opts?.data as any,
   headers: opts?.headers,
   signal: opts?.signal,
-  context: {},
+  context: opts?.context ?? {},
 })

218-252: Still missing guard against multiple next() calls.

The userNext function allows a middleware to call next() multiple times, which can advance the chain repeatedly and cause unpredictable behavior.

Apply this diff to add a reentrancy guard:

 if (middlewareFn) {
+  let nextCalled = false
   const userNext = async (
     userCtx: ServerFnMiddlewareResult | undefined = {} as any,
   ) => {
+    if (nextCalled) {
+      throw new Error('next() called multiple times in a middleware')
+    }
+    nextCalled = true
     // Return the next middleware
     const nextCtx = {
       ...ctx,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55b636c and 621a2aa.

📒 Files selected for processing (1)
  • packages/start-client-core/src/createServerFn.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-client-core/src/createServerFn.ts
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/createServerFn.ts
🧬 Code graph analysis (1)
packages/start-client-core/src/createServerFn.ts (1)
packages/start-client-core/src/index.tsx (6)
  • executeMiddleware (79-79)
  • NextFn (71-71)
  • execValidator (77-77)
  • MiddlewareFn (66-66)
  • ServerFnMiddlewareResult (68-68)
  • mergeHeaders (6-6)
⏰ 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: Test
  • GitHub Check: Preview
🔇 Additional comments (2)
packages/start-client-core/src/createServerFn.ts (2)

182-217: LGTM: Middleware selection and validation logic.

The refactored callNextMiddleware correctly shifts through the middleware chain, validates input on the server side, and selects the appropriate middleware function based on the environment.


269-285: LGTM: Fallback handling and chain initialization.

The fallback correctly continues the middleware chain when no middleware function is present, and the chain initialization properly sets up the initial context with appropriate defaults.

@schiller-manuel schiller-manuel force-pushed the fix/refactor-middleware branch from 621a2aa to ed54879 Compare October 19, 2025 20:41
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: 6

Caution

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

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

92-116: Per-request global fetch override is unsafe (cross-request race/leak).

Overwriting globalThis.fetch per request can affect concurrent requests (origin/requestOpts bleed), producing incorrect routing and privacy issues. At minimum, restore in a finally; preferably use an ALS-scoped wrapper or pass a handler instead of patching global.

Apply a minimal containment fix:

-    globalThis.fetch = async function (input, init) {
+    const prevFetch = globalThis.fetch
+    globalThis.fetch = async function (input, init) {
       function resolve(url: URL, requestOptions: RequestInit | undefined) {
         const fetchRequest = new Request(url, requestOptions)
         return startRequestResolver(fetchRequest, requestOpts)
       }
       ...
-    } as typeof fetch
+    } as typeof fetch
+    try {
+      // existing logic continues...
+      // ...
+    } finally {
+      globalThis.fetch = prevFetch
+    }

If you want a robust fix, wrap a single global fetch once with AsyncLocalStorage and consult per-request context to resolve same-origin URLs. I can draft that if helpful.

♻️ Duplicate comments (3)
packages/start-client-core/src/createServerFn.ts (3)

228-262: Guard against multiple next() calls in a middleware.

A middleware can call next() multiple times and reenter the chain.

-        const userNext = async (
+        let nextCalled = false
+        const userNext = async (
           userCtx: ServerFnMiddlewareResult | undefined = {} as any,
         ) => {
+          if (nextCalled) {
+            throw new Error('next() called multiple times in a middleware')
+          }
+          nextCalled = true
           // Return the next middleware
           const nextCtx = {
             ...ctx,
             ...userCtx,
             context: {
               ...ctx.context,
               ...userCtx.context,
             },

115-123: Don’t drop caller-provided context.

You’re passing context: {} and ignore opts?.context; this blocks client middlewares from receiving user context.

-            context: {},
+            context: opts?.context ?? {},

286-290: Bug: undefined check rejects valid falsy middleware returns.

!(result as any) treats 0/''/false as undefined; check strictly for undefined.

-        if (!(result as any)) {
+        if (result === undefined) {
           throw new Error(
             'User middleware returned undefined. You must call next() or return a result in your middlewares.',
           )
         }
🧹 Nitpick comments (5)
e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx (1)

36-36: Remove unnecessary empty string template literal.

The {''} at the end of the line renders nothing and appears to be a leftover from editing. Consider removing it or replacing with a colon for better readability:

Apply this diff:

-        It should navigate to a raw response of {''}
+        It should navigate to a raw response of:

Or simply:

-        It should navigate to a raw response of {''}
+        It should navigate to a raw response
packages/start-server-core/src/createStartHandler.ts (1)

202-211: Return 406 for unsupported Accept instead of 500.

Not an internal error; 406 Not Acceptable fits better.

-                  return json(
-                    { error: 'Only HTML requests are supported here' },
-                    { status: 500 },
-                  )
+                  return json(
+                    { error: 'Only HTML requests are supported here' },
+                    { status: 406 },
+                  )
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (2)

66-70: Avoid double serialization work.

serializePayload() is called twice; reuse the computed value.

-      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,
+        })

175-181: Avoid unconditional console.log on errors.

Noise and potential sensitive data leak. Use warning in dev only.

-      console.log(error)
+      if (process.env.NODE_ENV === 'development') {
+        // console.warn(error)
+      }
packages/start-server-core/src/server-functions-handler.ts (1)

288-291: Optional: return 204 No Content for undefined results.

serializeResult(undefined) currently responds with empty body and no content-type; set a 204 to clarify semantics.

-        return new Response(undefined, {
-          status: alsResponse?.status,
-          statusText: alsResponse?.statusText,
-        })
+        return new Response(null, {
+          status: alsResponse?.status ?? 204,
+          statusText: alsResponse?.statusText,
+        })
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed54879 and c7eddee.

📒 Files selected for processing (7)
  • e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx (1 hunks)
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts (7 hunks)
  • packages/start-client-core/src/constants.ts (1 hunks)
  • packages/start-client-core/src/createServerFn.ts (6 hunks)
  • packages/start-client-core/src/index.tsx (1 hunks)
  • packages/start-server-core/src/createStartHandler.ts (2 hunks)
  • packages/start-server-core/src/server-functions-handler.ts (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/start-client-core/src/index.tsx
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts
  • packages/start-client-core/src/constants.ts
  • packages/start-client-core/src/createServerFn.ts
  • packages/start-server-core/src/server-functions-handler.ts
  • e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx
packages/{*-start,start-*}/**

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-client-core/src/client-rpc/serverFnFetcher.ts
  • packages/start-client-core/src/constants.ts
  • packages/start-client-core/src/createServerFn.ts
  • packages/start-server-core/src/server-functions-handler.ts
**/src/routes/**

📄 CodeRabbit inference engine (AGENTS.md)

Place file-based routes under src/routes/ directories

Files:

  • e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx
e2e/**

📄 CodeRabbit inference engine (AGENTS.md)

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

Files:

  • e2e/react-start/server-functions/src/routes/submit-post-formdata.tsx
🧬 Code graph analysis (4)
packages/start-client-core/src/client-rpc/serverFnFetcher.ts (1)
packages/start-client-core/src/constants.ts (1)
  • X_TSS_SERIALIZED (7-7)
packages/start-client-core/src/constants.ts (1)
packages/start-client-core/src/index.tsx (1)
  • X_TSS_CONTEXT (87-87)
packages/start-client-core/src/createServerFn.ts (1)
packages/router-core/src/index.ts (3)
  • redirect (370-370)
  • parseRedirect (373-373)
  • isRedirect (371-371)
packages/start-server-core/src/server-functions-handler.ts (3)
packages/start-client-core/src/index.tsx (3)
  • json (6-6)
  • X_TSS_RAW_RESPONSE (86-86)
  • X_TSS_SERIALIZED (85-85)
packages/start-client-core/src/constants.ts (2)
  • X_TSS_RAW_RESPONSE (8-8)
  • X_TSS_SERIALIZED (7-7)
packages/start-server-core/src/request-response.ts (1)
  • getResponse (328-331)
🔇 Additional comments (1)
packages/start-client-core/src/constants.ts (1)

9-9: X_TSS_CONTEXT is properly integrated into the public API.

The constant is correctly exported as part of the public API surface in index.tsx alongside other similar constants (TSS_FORMDATA_CONTEXT, X_TSS_RAW_RESPONSE, etc.). While not used internally within the package, this is the expected pattern for public API constants defined in a barrel export. No action needed.

Comment on lines 189 to 191
invariant(contentType, 'expected content-type header to be set')
const serializedByStart = !!response.headers.get(X_TSS_SERIALIZED)
// If the response is not ok, throw an error
if (!response.ok) {
if (serializedByStart && contentType.includes('application/json')) {
const jsonPayload = await response.json()
const result = fromCrossJSON(jsonPayload, { plugins: serovalPlugins! })
throw result
}

throw new Error(await response.text())
}

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

Handle no-content responses; don’t assert content-type.

Server may return undefined/204 with no content-type (e.g., serializeResult(undefined)). Current invariant throws. Treat “no content” as undefined when ok.

-  invariant(contentType, 'expected content-type header to be set')
+  const contentType = response.headers.get('content-type')
+  if (!contentType) {
+    if (response.ok && (response.status === 204 || !response.body)) {
+      return undefined
+    }
+    if (!response.ok) {
+      throw new Error(await response.text())
+    }
+    return response
+  }

Also applies to: 288-294

🤖 Prompt for AI Agents
In packages/start-client-core/src/client-rpc/serverFnFetcher.ts around lines
189-191 (and similarly at 288-294), the code currently invariants that a
content-type header exists which breaks for no-content responses (e.g., 204 or
serializeResult(undefined)); change the logic to allow missing content-type for
empty responses: first detect no-content (response.status === 204 or
content-length === '0' or response.headers.get('content-type') === null and
response.bodyUsed is false/response has no body), treat those cases as undefined
result instead of asserting, and only assert/parse content-type when a body is
actually present; preserve the existing check for the X_TSS_SERIALIZED header
when present and ensure parsing flows handle both serialized and
plain/no-content responses.

Comment on lines 215 to 221
invariant(result, 'expected result to be resolved')
if (result instanceof Error) {
throw result
}

return result
}
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

Bug: falsy results (0, '', false) are incorrectly rejected.

invariant(result, ...) rejects valid falsy values; check only undefined.

-    invariant(result, 'expected result to be resolved')
+    invariant(typeof result !== 'undefined', 'expected result to be resolved')
📝 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
invariant(result, 'expected result to be resolved')
if (result instanceof Error) {
throw result
}
return result
}
invariant(typeof result !== 'undefined', 'expected result to be resolved')
if (result instanceof Error) {
throw result
}
return result
}
🤖 Prompt for AI Agents
In packages/start-client-core/src/client-rpc/serverFnFetcher.ts around lines 215
to 221, the code uses invariant(result, 'expected result to be resolved') which
treats valid falsy values (0, '', false) as errors; change the check to only
reject undefined (e.g., replace the invariant call with a check like if (result
=== undefined) { throw new Error('expected result to be resolved') } or use
invariant(result !== undefined, ...)) so that falsy but valid results are
preserved, keep the existing Error instance handling (if (result instanceof
Error) throw result) and then return result.

ValidateSerializableInput,
Validator,
} from '@tanstack/router-core'
import type { TSS_SERVER_FUNCTION } from './constants'
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

Invalid type-only import for value symbol.

TSS_SERVER_FUNCTION is a value, not a type. Using it as a computed key requires a value import; type-only import breaks under strict TS/isolatedModules.

-import type { TSS_SERVER_FUNCTION } from './constants'
+import { TSS_SERVER_FUNCTION } from './constants'

If you want a type-only reference, declare/export it as declare const TSS_SERVER_FUNCTION: unique symbol in constants and keep a value export to use as a key. As per coding guidelines.

🤖 Prompt for AI Agents
In packages/start-client-core/src/createServerFn.ts around line 18, the import
uses a type-only import for TSS_SERVER_FUNCTION but that identifier is a runtime
value used as a computed key; change the import to a value import (remove the
leading "type") so the symbol exists at runtime. If constants currently only
export a type, update packages/start-client-core/src/constants to export a
runtime symbol (e.g., export const TSS_SERVER_FUNCTION = Symbol(...) or export
declare const TSS_SERVER_FUNCTION: unique symbol plus a value export) so callers
can import the value; ensure callers use the value import for computed property
access.

Comment on lines +277 to +278
console.log('response', response)

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

Remove noisy response logging (potential PII leakage).

console.log(response) can log bodies/headers in prod. Guard by NODE_ENV or delete.

-    console.log('response', response)
+    if (process.env.NODE_ENV === 'development') {
+      // console.debug('response', response)
+    }
📝 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
console.log('response', response)
if (process.env.NODE_ENV === 'development') {
// console.debug('response', response)
}
🤖 Prompt for AI Agents
In packages/start-server-core/src/createStartHandler.ts around lines 277-278,
the console.log('response', response) is noisy and may leak PII; remove this raw
response logging or guard it so it only runs in non-production (e.g. check
NODE_ENV !== 'production') and/or replace with a structured logger at a debug
level that only emits safe fields (status/code, timing) rather than full
headers/bodies.

Comment on lines +145 to +153
console.log(
{
isServerFn,
isCreateServerFn,
isFormData,
isCtxResult,
},
res,
)
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 | 🟡 Minor

Remove stray debug logging.

Avoid logging payloads/results in prod.

-      console.log(
-        {
-          isServerFn,
-          isCreateServerFn,
-          isFormData,
-          isCtxResult,
-        },
-        res,
-      )
+      // Debug block removed
📝 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
console.log(
{
isServerFn,
isCreateServerFn,
isFormData,
isCtxResult,
},
res,
)
// Debug block removed
🤖 Prompt for AI Agents
In packages/start-server-core/src/server-functions-handler.ts around lines 145
to 153, remove the stray console.log that prints full payloads/results; replace
it either by deleting the log entirely or by using a non-production gated logger
(e.g. wrap with NODE_ENV !== 'production' or use a debug/logger library) so
sensitive data is not printed in production. Ensure no raw payload or result is
logged in the production path.

Comment on lines +156 to +164
if (
isPlainObject(result) &&
('result' in result || 'error' in result)
) {
console.log('tanner')
return result.result || result.error
}
return result
}
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

Fix unwrap logic; truthiness breaks falsy results.

result.result || result.error misroutes 0/''/false to error. Prefer explicit presence checks.

-      function unwrapResultOrError(result: any) {
-        if (
-          isPlainObject(result) &&
-          ('result' in result || 'error' in result)
-        ) {
-          console.log('tanner')
-          return result.result || result.error
-        }
-        return result
-      }
+      function unwrapResultOrError(result: any) {
+        if (isPlainObject(result) && ('result' in result || 'error' in result)) {
+          return 'error' in result ? result.error : result.result
+        }
+        return result
+      }
🤖 Prompt for AI Agents
In packages/start-server-core/src/server-functions-handler.ts around lines 156
to 164, the unwrap logic uses truthiness (result.result || result.error) which
incorrectly treats valid falsy values (0, '', false) as absent; replace this
with explicit presence checks: if the plain object hasOwnProperty('result')
return result.result; else if it hasOwnProperty('error') return result.error;
also remove the stray console.log and ensure you check property presence rather
than truthiness to preserve falsy payloads.

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.

2 participants