Skip to content

Vite dev middleware treats Sec-Fetch-Dest: manifest requests as static assets and skips server routes #4284

@missish

Description

@missish

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions