Skip to content

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

Closed

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.

Activity

  1. pi0 commented on May 26, 2026

    @pi0
    Member

    Can you try v3.0.260522-beta

  2. missish commented on Jun 3, 2026

    @missish
    Author

    你能试试吗v3.0.260522-beta

    Yes, that's what I'm using. The problem still exists in my DEV environment during testing.

  3. missish commented on Jun 3, 2026

    @missish
    Author

    I did a deeper check with nitro@3.0.260522-beta. The issue still reproduces in this TanStack Start setup, and I think I found why the fix in 5b7e152bb5dce3e263da6b195c0b9e12e8895044 does not cover this case.

    Environment tested:

    • node: v24.15.0
    • nitro: 3.0.260522-beta
    • vite: 8.0.16
    • @tanstack/react-start: 1.168.18
    • @tanstack/react-router: 1.170.10
    • @tanstack/start-plugin-core: 1.171.10

    Reproduction results:

    curl -i http://localhost:3001/manifest.json
    # 200
    
    curl -i \
      -H "Sec-Fetch-Dest: manifest" \
      -H "Accept: application/manifest+json,*/*;q=0.8" \
      http://localhost:3001/manifest.json
    # 404 Cannot GET /manifest.json
    
    curl -i http://localhost:3001/robots.txt
    # 200
    
    curl -i \
      -H "Sec-Fetch-Dest: manifest" \
      -H "Accept: application/manifest+json,*/*;q=0.8" \
      http://localhost:3001/robots.txt
    # 404 Cannot GET /robots.txt

    I also checked these controls:

    curl -i \
      -H "Accept: application/manifest+json,*/*;q=0.8" \
      http://localhost:3001/manifest.json
    # 200
    
    curl -i \
      -H "Sec-Fetch-Dest: empty" \
      -H "Accept: application/manifest+json,*/*;q=0.8" \
      http://localhost:3001/manifest.json
    # 200

    So the failure is still specifically triggered by Sec-Fetch-Dest: manifest.

    I confirmed that 3.0.260522-beta does include the logic from 5b7e152bb5dce3e263da6b195c0b9e12e8895044. The installed package has the explicit route guard before the asset heuristic:

    const match = nitro.routing.routes.match(...)
    const matchedHandlers = match ? Array.isArray(match) ? match : [match] : []
    
    if (matchedHandlers.some((h) => h?.route && h.route !== "/**" && !h.route.startsWith("/**:"))) {
      return nitroDevMiddleware(req, res, next)
    }

    However, in this TanStack Start app, /manifest.json and /robots.txt are not present in nitro.routing.routes as explicit Nitro routes. I verified this by creating a Vite dev server in middleware mode with the same vite.config.ts and inspecting the initialized Nitro route table.

    The route table was:

    [
      {
        "route": "/_nitro/tasks/**"
      },
      {
        "route": "/**",
        "handler": "nitro/dist/runtime/internal/vite/ssr-renderer"
      }
    ]

    And both routes only matched the SSR catch-all:

    nitro.routing.routes.match("GET", "/manifest.json")
    // => { route: "/**", handler: ".../nitro/dist/runtime/internal/vite/ssr-renderer" }
    
    nitro.routing.routes.match("GET", "/robots.txt")
    // => { route: "/**", handler: ".../nitro/dist/runtime/internal/vite/ssr-renderer" }

    The TanStack Start routes are defined as React Router file routes:

    createFileRoute("/manifest.json")({
      server: {
        handlers: {
          GET: () => Response.json(...)
        }
      }
    })

    So, from Nitro's dev middleware point of view, this is not an explicit /manifest.json Nitro route. It is reached through the SSR renderer / catch-all route.

    That means the new explicit route guard does not trigger, because the matched route is /**, which is explicitly excluded by the guard. Then this code path still applies:

    const fetchDest = req.headers["sec-fetch-dest"]
    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, _nitroHandled is set, and the later Nitro middleware skips dispatching the request to the SSR/TanStack route environment. Since there is no static public/manifest.json, Vite/Connect returns Cannot GET /manifest.json.

    So my current understanding is:

    • The commit fixes explicit Nitro routes.
    • This TanStack Start case still fails because /manifest.json is handled behind Nitro's /** SSR renderer, not as an explicit Nitro route.
    • The asset heuristic still runs before the SSR renderer has a chance to handle the request.

    A possible fix might need to avoid marking Sec-Fetch-Dest: manifest requests as _nitroHandled before the SSR renderer/catch-all path can run, or otherwise allow SSR fallback when Vite did not actually serve a static asset.

  4. Lars418 commented on Jun 15, 2026

    @Lars418

    Hi, I'm experiencing the same issue in the current version (3.0.260610-beta). I have a dynamic route in TanStack start for proxying images - without Nitro, the images are displaye correctly - with Nitro, I get a 404.

    Is there currently a workaround?

  5. pi0 commented on Jun 15, 2026

    @pi0
    Member

    I have proposed a (hacky) solution #4274 /cc @schiller-manuel

  6. Lars418 commented on Jun 16, 2026

    @Lars418

    Hey, that sounds great! Is there an estimated timeline when the bugfix will be available on npm?

  7. pi0 commented on Jun 16, 2026

    @pi0
    Member

    @Lars418 no estimate it is a draft pr pending to be validated by tanstack team or we might need to take an alternative approach. in the meantime you can simply use nitro api routes.

  8. pi0x commented on Sep 7, 2026

    @pi0x
    Contributor

    Fixed on main by #4467, which landed after the 3.0.260610-beta build tested here. Vite still gets first shot at asset-tagged requests, but when it has nothing to serve the request is now handed to the SSR/server routes instead of being dropped. I verified with a fresh dev-server repro: both /manifest.json and /robots.txt with Sec-Fetch-Dest: manifest return 200 from the catch-all. Please retry on a recent nightly.

    Posted by an AI triage agent on behalf of a maintainer.

  9. dennisameling commented on Sep 16, 2026

    @dennisameling

    Confirming this is fixed in nightly v3.0.260903-beta now. I have an API call where we do e.g. http://localhost:3556/api/branding/logo-f1bd5480-30cf-438d-be2e-209a29975c82.png which was failing in local dev earlier. It returned a 404 with body Cannot GET /api/branding/logo-f1bd5480-30cf-438d-be2e-209a29975c82.png.

    With the nightly build, I can confirm it correctly returns a 200 with the actual image contents again. Thanks for the fix! 🚀

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions