Environment
nitro: 3.0.260522-beta
vite: 8.0.14
@tanstack/react-start: 1.168.13
@tanstack/react-router: 1.170.8
- Node.js:
24.15.0
Description
In Vite dev mode with Nitro + TanStack Start, a server route such as /manifest.json works for a normal request, but fails when requested by the browser as a web app manifest.
The browser sends:
Sec-Fetch-Dest: manifest
Accept: application/manifest+json,*/*;q=0.8
That request returns:
404 Cannot GET /manifest.json
The same route returns 200 without Sec-Fetch-Dest: manifest.
Reproduction
In a TanStack Start app using Nitro's Vite plugin:
import { createFileRoute } from "@tanstack/react-router"
export const Route = createFileRoute("/manifest.json")({
server: {
handlers: {
GET: () =>
Response.json(
{ name: "Demo", short_name: "Demo", start_url: "/", display: "standalone" },
{ headers: { "Content-Type": "application/manifest+json" } },
),
},
},
})
And in the root document/head:
{ rel: "manifest", href: "/manifest.json" }
Then compare:
curl -i http://localhost:3000/manifest.json
returns 200, but:
curl -i \
-H "Sec-Fetch-Dest: manifest" \
-H "Accept: application/manifest+json,*/*;q=0.8" \
http://localhost:3000/manifest.json
returns 404 Cannot GET /manifest.json.
The same behavior can be reproduced with another dynamic route such as /robots.txt: normal requests return 200, while the same URL with Sec-Fetch-Dest: manifest returns the Connect/Vite finalhandler 404.
Suspected Cause
The Nitro Vite dev middleware appears to classify any non-document Sec-Fetch-Dest request as an asset:
const isAsset =
typeof fetchDest === "string" && fetchDest !== "empty"
? !/^(?:document|iframe|frame)$/.test(fetchDest)
: isAssetByExt
if (isAsset) req._nitroHandled = true
next()
For Sec-Fetch-Dest: manifest, isAsset becomes true. The request is marked _nitroHandled, so the later Nitro middleware skips dispatching it to the SSR/server route environment. Since there is no static file in public, Vite/Connect eventually returns Cannot GET /manifest.json.
Expected Behavior
Server routes should still be allowed to handle requests such as Sec-Fetch-Dest: manifest when no static asset was served, or Nitro should avoid marking these requests as handled before the SSR/server-route dispatch path has a chance to run.
Static public/manifest.json is a valid workaround, but dynamic server routes for manifest-like resources should not be bypassed solely because of Sec-Fetch-Dest: manifest.
Environment
nitro:3.0.260522-betavite:8.0.14@tanstack/react-start:1.168.13@tanstack/react-router:1.170.824.15.0Description
In Vite dev mode with Nitro + TanStack Start, a server route such as
/manifest.jsonworks for a normal request, but fails when requested by the browser as a web app manifest.The browser sends:
That request returns:
The same route returns
200withoutSec-Fetch-Dest: manifest.Reproduction
In a TanStack Start app using Nitro's Vite plugin:
And in the root document/head:
Then compare:
returns
200, but:returns
404 Cannot GET /manifest.json.The same behavior can be reproduced with another dynamic route such as
/robots.txt: normal requests return200, while the same URL withSec-Fetch-Dest: manifestreturns the Connect/Vite finalhandler 404.Suspected Cause
The Nitro Vite dev middleware appears to classify any non-document
Sec-Fetch-Destrequest as an asset:For
Sec-Fetch-Dest: manifest,isAssetbecomestrue. The request is marked_nitroHandled, so the later Nitro middleware skips dispatching it to the SSR/server route environment. Since there is no static file inpublic, Vite/Connect eventually returnsCannot GET /manifest.json.Expected Behavior
Server routes should still be allowed to handle requests such as
Sec-Fetch-Dest: manifestwhen no static asset was served, or Nitro should avoid marking these requests as handled before the SSR/server-route dispatch path has a chance to run.Static
public/manifest.jsonis a valid workaround, but dynamic server routes for manifest-like resources should not be bypassed solely because ofSec-Fetch-Dest: manifest.