Skip to content

SSR HMR: createStartHandler is not a function on every re-evaluation (re-occurrence of #5673, 1.167.50) #7285

@rshelnutt

Description

@rshelnutt

Re-occurrence of #5673 — same error string, same source position (default-entry/server.ts:8:15). Filing as a new issue per @schiller-manuel's request in #5673 ("please create a new issue including a complete reproducer project"). The original issue was closed, but the failure is still present on the latest @tanstack/react-start (1.167.50, 22 minor versions past @zotodev's regression report at 1.145.10).

Summary

Every SSR HMR pass throws (0, __vite_ssr_import_0__.createStartHandler) is not a function on @tanstack/react-start@1.167.50. Cold start always succeeds; the second-and-subsequent SSR pass after Vite invalidates the module graph fails. The dev server becomes unusable as soon as anything is edited until it's killed and restarted.

Identical to @Flusinerd's report in #5673 against Nitro — same error string at the same source position — so the bug isn't specific to any one SSR runner; this report adds Cloudflare workerd (@cloudflare/vite-plugin) to the list of affected runners.

Stack

[vite] (ssr) hmr update /@id/virtual:cloudflare/worker-entry, /src/lib/locale-redirect.ts?tss-serverfn-split
[vite] TypeError: (0 , __vite_ssr_import_0__.createStartHandler) is not a function
    at /<repo>/node_modules/.pnpm/@tanstack+react-start@1.167.50_.../node_modules/@tanstack/react-start/src/default-entry/server.ts:8:15
    at Object.runInlinedModule (workers/runner-worker/index.js:214:4)
    at ModuleRunner.directRequest (workers/runner-worker/vite/module-runner:1204:59)
    at ModuleRunner.cachedRequest (workers/runner-worker/vite/module-runner:1111:73)
    at / virtual:cloudflare/worker-entry:5:1
    at Object.runInlinedModule (workers/runner-worker/index.js:214:4)
    at ModuleRunner.directRequest (workers/runner-worker/vite/module-runner:1204:59)
    at ModuleRunner.cachedRequest (workers/runner-worker/vite/module-runner:1111:73)
    at ModuleRunner.import (workers/runner-worker/vite/module-runner:1061:10)
    at HMRClient.fetchUpdate (workers/runner-worker/vite/module-runner:476:21)
[vite] Failed to reload virtual:cloudflare/worker-entry. This could be due to syntax errors or importing non-existent modules.
[vite] Internal server error: (0 , __vite_ssr_import_0__.createStartHandler) is not a function
      at runInRunnerObject (workers/runner-worker/index.js:106:3)
      at getWorkerEntryExport (workers/runner-worker/index.js:234:17)
      at null.<anonymous> (workers/runner-worker/index.js:353:24)
      at maybeCaptureError (workers/runner-worker/index.js:50:10)

The error pinpoints default-entry/server.ts:8:15, which is createStartHandler(defaultStreamHandler). So __vite_ssr_import_0__ (the namespace from @tanstack/react-start/server) returns without createStartHandler defined on it.

Environment

  • @tanstack/react-start: 1.167.50 → resolves transitively to @tanstack/react-start-server@1.166.44@tanstack/start-server-core@1.167.22
  • @tanstack/react-router: 1.168.25
  • @tanstack/router-plugin: 1.167.28
  • @cloudflare/vite-plugin: 1.31.2 (workerd 1.20260424.1)
  • vite: 7.3.2
  • pnpm: 9.15.4
  • Node: 22.20.0
  • macOS Darwin 23.5.0

vite.config.ts:

plugins: [
  cloudflare({ viteEnvironment: { name: 'ssr' } }),
  svgr(),
  devtools(),
  tsconfigPaths({ projects: ['./tsconfig.json'] }),
  tailwindcss(),
  tanstackStart({ router: { routeFileIgnorePattern: '_admin' } }),
  viteReact()
]

wrangler.jsonc:

{
  "compatibility_date": "2025-09-02",
  "compatibility_flags": ["nodejs_compat"],
  "main": "@tanstack/react-start/server-entry"
}

Reproduction

  1. Boot vite dev. First request to / returns 200.
  2. touch src/lib/seo/meta.ts (any source edit, including a one-character styles.css change).
  3. Next request returns 500 with the stack above. 100% reproducible.

Root cause analysis

createStartHandler is exported through a two-level wildcard re-export chain:

  • @tanstack/react-start/server (dist/esm/server.js) → export * from "@tanstack/react-start-server"
  • @tanstack/react-start-server@1.166.44 (dist/esm/index.js) → export * from "@tanstack/start-server-core"; export { StartServer, defaultRenderHandler, defaultStreamHandler };
  • @tanstack/start-server-core@1.167.22 — direct named export of createStartHandler.

Vite's SSR transform compiles export * from '...' into a runtime call that copies enumerable own properties from the imported namespace into the current module's namespace at evaluation time. When the worker entry is invalidated by HMR and the runner re-evaluates the chain, the deeply-nested wildcard re-exports do not consistently rebind createStartHandler onto the outermost namespace — the static top-level binding __vite_ssr_import_0__.createStartHandler resolves to undefined.

This matches @Flusinerd's repro on Nitro (nitro/dist/runtime/internal/vite/dev-worker.mjs, identical error string at the same default-entry/server.ts:8:15 source position), so the bug appears to be in the re-export chain itself, not in any individual SSR runner.

Workaround that worked

Replacing the auto-generated @tanstack/react-start/server-entry with a user-owned entry that defers the import to runtime:

// src/server.ts
let cachedFetch:
  | ((req: Request, env: unknown, ctx: unknown) => Promise<Response>)
  | null = null

async function getFetch() {
  if (cachedFetch) return cachedFetch
  const mod = await import('@tanstack/react-start/server')
  cachedFetch = mod.createStartHandler(mod.defaultStreamHandler) as (
    req: Request,
    env: unknown,
    ctx: unknown
  ) => Promise<Response>
  return cachedFetch
}

if (import.meta.hot) {
  import.meta.hot.accept(() => {
    cachedFetch = null
  })
}

export default {
  async fetch(request: Request, env: unknown, ctx: unknown) {
    const fn = await getFetch()
    return await fn(request, env, ctx)
  }
}
// wrangler.jsonc
"main": "./src/server.ts"  // was "@tanstack/react-start/server-entry"

Verified across 10 HMR cycles + a Playwright-driven page load — zero recurrences. The dynamic await import(...) (resolved through __vite_ssr_dynamic_import__) materializes the namespace at call time, by which point the inner chain has finished re-evaluating, and the wildcard re-exports are complete. Static top-level destructuring is what trips the bug; deferring to a runtime call avoids it.

What didn't work

  • Clearing node_modules/.vite/ — repro on the very first HMR after a fresh boot.
  • Custom src/server.ts with the same static top-level import from @tanstack/react-start/server — same chain, same error, just relocated to user source.
  • Direct deep imports of createStartHandler from @tanstack/start-server-core and defaultStreamHandler from @tanstack/react-start-server (declared as direct deps to bypass the wildcard) — fails because start-server-core references plugin-provided virtual specifiers (#tanstack-start-entry, #tanstack-start-plugin-adapters, tanstack-start-manifest:v, tanstack-start-injected-head-scripts:v) that the tanstackStart Vite plugin only resolves when the import path flows through the official @tanstack/react-start package facade. esbuild errors out with Could not resolve "#tanstack-start-entry".
  • Downgrading@ulrichstark's bisect in ReferenceError: Cannot access '__vite_ssr_import_2__' before initialization #5673 found 1.142.1 fixed the original report, but @zotodev confirmed regression at 1.145.10, and we're 22 minor versions past that.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions