Skip to content

staticFunctionMiddleware Unexpected token '<', "<!DOCTYPE "... is not valid JSON  #7630

Description

@aymericzip

Which project does this relate to?

Router

Describe the bug

When I add staticFunctionMiddleware to my server function, it throws an error after the build:

Unexpected token '<', "<!DOCTYPE "... is not valid JSON

The returned object is serializable as JSON.

I also tried JSON.parse(JSON.stringify(result)), but I get the same error.

Complete minimal reproducer

https://github.com/aymericzip/staticFunctionMiddleware_repro

Steps to Reproduce the Bug

bun install 
bun run build
cd ./apps/website-tanstack
uncomment middleware in `apps/website-tanstack/src/serverFunctions/docs.ts`
bun run build
bun run start 

navigate to /doc

I also included a demo for #7516
comment code in packages/@intlayer/design-system/src/providers/ReactQueryProvider.tsx

cd ../../
bun run build ## rebuild design system
cd ./apps/website-tanstack
bun run build

Expected behavior

Screenshots or Videos

Demo: https://drive.google.com/open?id=1gBbIZ2okGl099IfONT5lGjToc2TeMtBf&usp=drive_fs

Platform

"@tanstack/react-devtools": "0.10.5",
"@tanstack/react-query": "5.101.0",
"@tanstack/react-router": "1.170.15",
"@tanstack/react-router-ssr-query": "1.167.1",
"@tanstack/react-start": "1.168.25",
"@tanstack/react-virtual": "3.14.2",
"@tanstack/router-plugin": "1.168.18",
"bun": "1.3.14",

mac

Additional context

No response

Activity

  1. IgnazioDS commented on Jul 12, 2026

    @IgnazioDS

    Ran into the same error and dug into it using the staticFunctionMiddleware_repro. Everything below is on the pinned versions (@tanstack/[email protected], which resolves [email protected] and [email protected]), so this is not the #6940 output-directory bug - that fix is already present.

    The payload file is missing, and the client middleware has no fallback when it is.

    1. Static payloads are only written for server-function calls that actually execute during prerender (staticFunctionMiddleware.server writes only when TSS_CLIENT_OUTPUT_DIR is set, which prerenderWithVite does). In the repro build the prerender set (crawlLinks: false + explicit pages) rendered 3 pages, none of which invoke the doc server functions - so __tsr/staticServerFnCache/ is never created anywhere on disk. Any route that uses the middleware but is not covered by the prerender page set has no payload, by construction.

    2. At runtime, the client half of the middleware fetches the payload URL and calls r.json() directly - no response.ok check, no content-type check, and no fallback to ctx.next() (staticFunctionMiddleware.js, the fetchItem call in 1.167.17). A missing payload is therefore a hard crash of the navigation instead of a degradation to a live server-function call.

    3. The failed fetch does not even return a 404. The app's own catch-all route serves the SPA shell as 200 text/html for the missing payload path:

    $ curl -i http://localhost:4174/__tsr/staticServerFnCache/deadbeef.json
    HTTP/1.1 200 OK
    content-type: text/html; charset=utf-8
    
    <!DOCTYPE html><html dir="ltr" lang="__tsr">...
    

    (the locale catch-all parsed __tsr as a locale). r.json() on that body is exactly Unexpected token '<', "<!DOCTYPE "... is not valid JSON. Because the shell comes back as 200, checking response.ok alone would not be enough - the response needs a content-type check.

    Suggested fix in start-static-server-functions: in the client middleware, validate the response (ok + application/json) and fall back to ctx.next() when the payload is missing, so uncovered routes degrade to a live server-function call instead of crashing. A build-time warning when a decorated function produced zero payloads during prerender would also make the coverage gap visible.

    Workaround meanwhile: ensure prerender.pages includes every route that calls a static-middleware function (including every locale variant, since the payload hash includes the serialized args), or keep the middleware off until the fallback lands.

  2. IgnazioDS commented on Jul 12, 2026

    @IgnazioDS

    Ran into the same error and dug into it using the staticFunctionMiddleware_repro. Everything below is on the pinned versions (@tanstack/[email protected], which resolves [email protected] and [email protected]), so this is not the #6940 output-directory bug - that fix is already present.

    The payload file is missing, and the client middleware has no fallback when it is.

    1. Static payloads are only written for server-function calls that actually execute during prerender (staticFunctionMiddleware.server writes only when TSS_CLIENT_OUTPUT_DIR is set, which prerenderWithVite does). In the repro build the prerender set (crawlLinks: false + explicit pages) rendered 3 pages, none of which invoke the doc server functions - so __tsr/staticServerFnCache/ is never created anywhere on disk. Any route that uses the middleware but is not covered by the prerender page set has no payload, by construction.

    2. At runtime, the client half of the middleware fetches the payload URL and calls r.json() directly - no response.ok check, no content-type check, and no fallback to ctx.next() (staticFunctionMiddleware.js, the fetchItem call in 1.167.17). A missing payload is therefore a hard crash of the navigation instead of a degradation to a live server-function call.

    3. The failed fetch does not even return a 404. The app's own catch-all route serves the SPA shell as 200 text/html for the missing payload path:

    $ curl -i http://localhost:4174/__tsr/staticServerFnCache/deadbeef.json
    HTTP/1.1 200 OK
    content-type: text/html; charset=utf-8
    
    <!DOCTYPE html><html dir="ltr" lang="__tsr">...
    

    (the locale catch-all parsed __tsr as a locale). r.json() on that body is exactly Unexpected token '<', "<!DOCTYPE "... is not valid JSON. Because the shell comes back as 200, checking response.ok alone would not be enough - the response needs a content-type check.

    Suggested fix in start-static-server-functions: in the client middleware, validate the response (ok + application/json) and fall back to ctx.next() when the payload is missing, so uncovered routes degrade to a live server-function call instead of crashing. A build-time warning when a decorated function produced zero payloads during prerender would also make the coverage gap visible.

    Workaround meanwhile: ensure prerender.pages includes every route that calls a static-middleware function (including every locale variant, since the payload hash includes the serialized args), or keep the middleware off until the fallback lands.

  3. aymericzip commented on Aug 6, 2026

    @aymericzip
    Author

    Found a fix using nitro middleware

    Payloads aren't missing here, they're just not served.

    Nitro bakes its public asset manifest before TanStack's prerender runs, so everything prerender writes lands on disk after the manifest is sealed and falls through to the SSR catch-all. Hence the HTML shell with lang="__tsr".
    You can confirm it by dropping any file into .output/public after a build, same result

    Nitro middleware fix: aymericzip/intlayer@b96d2b2

  4. fullheart commented on Aug 19, 2026

    @fullheart

    I ran into the same crash. What happens: when the cache file for a call doesn't exist, the server has nothing to send back except the app's own HTML page — and fetchItem then tries to parse that HTML as JSON. That's the Unexpected token '<'.

    In our case some calls simply never ran during prerender, so their files were never written, and every navigation that needed one of them died. As @IgnazioDS said: the client just has no fallback for a missing file.

    Here's the fix we've been running in production, as a patch-package file — save it as patches/@tanstack+start-static-server-functions+1.167.29.patch and run npx patch-package:

    diff --git a/node_modules/@tanstack/start-static-server-functions/dist/esm/staticFunctionMiddleware.js b/node_modules/@tanstack/start-static-server-functions/dist/esm/staticFunctionMiddleware.js
    index c6fecf9..50fd024 100644
    --- a/node_modules/@tanstack/start-static-server-functions/dist/esm/staticFunctionMiddleware.js
    +++ b/node_modules/@tanstack/start-static-server-functions/dist/esm/staticFunctionMiddleware.js
    @@ -57,7 +57,14 @@ var fetchItem = async ({ data, functionId }) => {
     		hash: jsonToFilenameSafeString(data)
     	});
     	let result = staticClientCache?.get(url);
    -	result = await fetch(url, { method: "GET" }).then((r) => r.json()).then((d) => fromJSON(d, { plugins: getDefaultSerovalPlugins() }));
    +	// PATCHED — https://github.com/TanStack/router/issues/7630
    +	// Upstream calls `r.json()` on whatever comes back. A payload the build never wrote is
    +	// answered by the app's own catch-all as HTML, so this threw `Unexpected token '<'` and
    +	// took the navigation with it. Both checks are needed: the shell can arrive as a 200.
    +	const response = await fetch(url, { method: "GET" });
    +	if (!response.ok) return void 0;
    +	if (!response.headers.get("content-type")?.includes("application/json")) return void 0;
    +	result = fromJSON(await response.json(), { plugins: getDefaultSerovalPlugins() });
     	return result;
     };
     var staticFunctionMiddleware = createMiddleware({ type: "function" }).client(async (ctx) => {

    With this, a missing file no longer kills the navigation — it falls through to ctx.next(), and what happens then stays your setup's decision: live execution as today, or (for fully prerendered sites, see #6787) a clean "no data" from the server. The content-type check matters too, because the HTML page can come back as a 200. Reproducer: https://github.com/fullheart/tanstack-start-issue-6787-v2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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