Start: server function URL computed at module init from unreplaced `process.env.TSS_SERVER_FN_BASE`, yielding a relative RPC URL in dev
Nobody has claimed this yet.
- 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
createClientRpc() computes a server function's request URL at module evaluation time (packages/start-client-core/src/client-rpc/createClientRpc.ts):
export function createClientRpc(functionId: string) {
const url = process.env.TSS_SERVER_FN_BASE + functionId
// …
}
TSS_SERVER_FN_BASE is supplied as a Vite define (defineReplaceEnv() in start-plugin-core). During vite dev that define is not applied to files inside node_modules — the dev-served module still contains the literal process.env.TSS_SERVER_FN_BASE, and the value exists only because Vite's /@vite/env shim assigns globalThis.process.env at runtime. The URL is therefore only correct if /@vite/env has executed before the generated server-function module evaluates.
When it hasn't, url becomes the string "undefined" + functionId, which is a relative URL. The fetch then resolves against the current document path — e.g. /posts/1/undefinedsrc_utils_posts_tsx--fetchPost_createServerFn_handler — never matches TSS_SERVER_FN_BASE in createStartHandler, falls through to the router, and since serverFnFetcher sends accept: application/x-ndjson, application/json the router answers:
500 {"error":"Only HTML requests are supported here"}
serverFnFetcher rethrows that as new Error(await response.text()), so app authors see a bare Error whose message is raw JSON, thrown from whichever component happens to call a server function first — with nothing pointing at the real cause.
Two things make it look random rather than reproducible:
- it depends on module evaluation order, which shifts with dep-optimization state, HMR and import graph changes;
- a relative URL still resolves correctly on
/(undefined…→/undefined…is wrong but at the root there is no prefix to inherit), while on/foo/barit inherits the path. So the same app "works" on one page and fails on another.
Complete minimal reproducer
https://stackblitz.com/github/tanstack/router/tree/main/examples/react/start-basic
No custom code is needed — the example's own server functions (src/utils/posts.tsx) are enough. Any Start app with a server function shows it.
Steps to Reproduce the Bug
-
Start the dev server for the example above.
-
Confirm the define is not statically applied in dev — request the dev-served dependency module:
curl -s 'http://localhost:3000/@fs/<abs-path>/node_modules/@tanstack/start-client-core/dist/esm/client-rpc/createClientRpc.js' | grep TSS_SERVER_FN_BASEconst url = process.env.TSS_SERVER_FN_BASE + functionId; // unreplacedIn the browser,
process.env.TSS_SERVER_FN_BASEis a runtime property (Object.keys(globalThis.process.env)→TSS_CLIENT_OUTPUT_DIR, TSS_DEV_SERVER, TSS_ROUTER_BASEPATH, TSS_SERVER_FN_BASE, TSS_SHELL), i.e. it comes from the shim, not from a build-time replacement. -
Force the ordering that the shim normally hides. On any non-root route, in the browser console:
delete process.env.TSS_SERVER_FN_BASE const m = await import('/src/utils/posts.tsx?forcereeval=1') // fresh module instance await m.fetchPost({ data: '1' })Error: {"error":"Only HTML requests are supported here"} Network: GET /posts/undefinedsrc_utils_posts_tsx--fetchPost_createServerFn_handler?payload=…
Step 3 forces deterministically what module ordering otherwise decides by chance; the timing dependence is exactly why this surfaces intermittently in real apps rather than always.
Expected behavior
A server function's request URL shouldn't depend on module evaluation order in dev — it should be absolute in every case.
A few directions (not prescriptive):
- compute
urllazily insideclientFn, so the read happens at call time, long after the dev shims are installed (urlis also exposed on the returned object, so a getter would preserve that); - or fall back to the configured default when the define is missing, instead of string-concatenating
undefined; - or read
import.meta.env.TSS_SERVER_FN_BASE—defineReplaceEnv()already defines both spellings, and Vite providesimport.meta.envper module in dev.
Separately, and regardless of which fix is chosen: a request that reaches the router with a non-HTML Accept currently reports as 500, which sent me looking for a server fault. See #7913 for that side of it.
Screenshots or Videos
No response
Platform
- Router / Start Version:
@tanstack/react-start1.133.3 — the relevant line is unchanged in@tanstack/start-client-core1.170.27, anddefineReplaceEnv/ the dev client-entry injection are unchanged too, so this is not fixed by upgrading - OS: macOS (darwin 25.6.0)
- Browser: Chrome / Chromium
- Bundler:
rolldown-vite(aliased asvite), Vite 7 dev server - Also present with
@cloudflare/vite-pluginas the SSR environment, though that is not needed to reproduce
Additional context
Workaround for app authors until this is fixed — seed the value from an inline classic script in <head>, which runs while the head is parsed and therefore before any module script exists:
<script>globalThis.process ??= {}; globalThis.process.env ??= {}; globalThis.process.env.TSS_SERVER_FN_BASE ??= "/_serverFn/";</script>
??= yields to the real value whenever the shim wins the race anyway, and the snippet can be gated to dev so production keeps using the statically replaced define.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/start-client-core/src/client-rpc/createClientRpc.ts and trace how defineReplaceEnv in start-plugin-core supplies TSS_SERVER_FN_BASE during Vite dev. Reproduce with examples/react/start-basic, especially a nested route and src/utils/posts.tsx, then verify the server-function request URL is absolute and no longer depends on module evaluation order.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, vite
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 62/100