TanStack / TanStack/router

If you pass createServerOnlyFn(...) directly as an argument to createServerFn().handler(), it will generate a broken server function.

Open
#8,446 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
TypeScript
Stars
15.1k
Forks
1.9k
Avg merge
1d 20h
Merged PRs (30d)
143

Description

Which project does this relate to?

Start

Describe the bug

If you pass a function call that the compiler is supposed to transform directly into the argument of createServerFn().handler(), the build will succeed, but it will generate a broken server function.

  1. createServerOnlyFn
TypeError: Cannot create property 'method' on string 'da38dc3f0228fb3e…'
    at fun.handler (dist/server/server.js:237:23)
    at loadEntries (dist/server/server.js:1419:52)

As a result, it will return 500.
Since it is thrown during module evaluation inside loadEntries, it fails before routing, causing not only that route but the entire application to crash.

  1. createIsomorphicFn
// Broken
.handler(createServerOnlyFn(impl))
.handler(createIsomorphicFn().server(impl).client(other))

// Works (same JavaScript)
const wrapped = createServerOnlyFn(impl)
.handler(wrapped)

No exception is thrown, but the server function returns undefined.
If you bind the call to a variable first and pass the identifier, both will be generated correctly.

Generated Code

Caller chunk (dist/server/assets/router-*.js):

// hoisted            → Correct
createServerFn({ method: "GET" }).handler(createSsrRpc("da38dc3f0228fb3e…"))
// inline-server-only → ID string remains
createServerFn({ method: "GET" }).handler("da38dc3f0228fb3e…")
// inline-isomorphic  → Implementation overwrites the stub
createServerFn({ method: "GET" }).handler(readOnServer)

The client bundle has the same broken slot, and in both cases, a local function is included instead of an RPC.

// inline-server-only
.handler(() => { throw Error("createServerOnlyFn() functions can only be called on the server!") })
// inline-isomorphic
.handler(() => ({ ranOn: "client" }))
Cause

Candidates are collected in a single pass, and each holds a Babel path. The handlers then run in a fixed order defined by BuiltInKindHandlerOrder in packages/start-plugin-core/src/start-compiler/compiler.ts:

['ServerFn', 'Middleware', 'IsomorphicFn', 'ServerOnlyFn', 'ClientOnlyFn']

ServerFn runs first, and handleCreateServerFn.ts:445 replaces the first argument of .handler().

handlerFnPath.replaceWith(rpcStub) // rpcStub is createSsrRpc("<id>")

Subsequent handlers operate through the paths captured before this replacement. From there, they diverge into two cases:

  • handleEnvOnly.ts reads path.node.arguments[0]. Since the node pointed to by the path is now createSsrRpc("<id>"), what it retrieves is the string literal of the ID. This leaves .handler("<id>"), and the handler of createServerFn attempts to assign extractedFn.method to that string.
  • handleCreateIsomorphicFn.ts retains envCallInfo.firstArgPath?.node captured from the original tree, so path.replaceWith(innerFn) overwrites the stub with the implementation itself. The RPC wiring disappears, and no exception is thrown.

The root cause is the same in both cases: subsequent handlers are touching the node replaced by ServerFn through stale paths.

Binding it to a variable avoids this because the node for createServerOnlyFn(...) moves to the initializer of the variable declaration, and none of the handlers replace it there.

Complete minimal reproducer

https://github.com/imaimai17468/tanstack-start-handler-call-expression-repro

Steps to Reproduce the Bug
git clone https://github.com/imaimai17468/tanstack-start-handler-call-expression-repro
cd tanstack-start-handler-call-expression-repro
npm install
npm run repro

This builds three apps, displays the generated .handler(...) in dist/server, and starts vite preview so you can make a request to /.
The results are as follows:

app Shape of .handler() GET /
apps/hoisted Passes const wrapped = createServerOnlyFn(impl) 200, {"ranOn":"server"}
apps/inline-server-only Writes createServerOnlyFn(impl) directly 500, TypeError as shown above
apps/inline-isomorphic Writes createIsomorphicFn()... directly 200, loader data is undefined

They are split into three apps because inline-server-only throws during module evaluation and crashes the entire app, making it impossible to showcase 200 and 500 within the same app.

vite dev fails in the same way (the only difference is that the IDs become base64 strings instead of hashes).

Expected behavior

Since .handler(createServerOnlyFn(impl)) and const f = createServerOnlyFn(impl); .handler(f) represent the same JavaScript, they should compile to the same server function.

If the policy is to not support one of these patterns, the build should fail instead. Currently, it generates either a server function that throws on every request or one that silently returns undefined.

Screenshots or Videos

No response

Platform
  • Router / Start Version: @tanstack/react-start 1.168.54, @tanstack/start-plugin-core 1.171.44, @tanstack/react-router 1.170.36
  • OS: macOS 26.6.2 arm64, Node 26.7.0, npm 11.19.0
  • Browser: Not used
  • Browser Version: N/A
  • Bundler: vite
  • Bundler Version: 8.3.0
Additional context

No response

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with BuiltInKindHandlerOrder in packages/start-plugin-core/src/start-compiler/compiler.ts, then trace handleCreateServerFn.ts:445, handleEnvOnly.ts, and handleCreateIsomorphicFn.ts using the provided minimal reproducer and npm run repro. Done means direct and variable-bound handler calls generate equivalent RPC wiring and the three reproducer apps produce their expected results without broken server functions.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript, vite
Domain
backend-api-design, build-system
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.