Skip to content

server.ts fetch handler: 404 responses silently replaced with SPA HTML in dev mode #4162

@productdevbook

Description

@productdevbook

Problem

In Vite dev mode, when server.ts fetch handler returns a Response with status 404, Nitro's dispatchFetch pipeline silently replaces it with the SPA index.html (status 200, content-type text/html).

This breaks any API that legitimately returns 404 (e.g., "entity not found").

Reproduction

// server.ts
export default {
  fetch: (request: Request) => {
    const url = new URL(request.url)
    if (url.pathname === '/api/users/999') {
      return new Response(
        JSON.stringify({ code: 'NOT_FOUND', status: 404, message: 'User not found' }),
        { status: 404, headers: { 'content-type': 'application/json' } }
      )
    }
    return new Response('ok')
  }
}
curl http://localhost:3000/api/users/999
# Expected: {"code":"NOT_FOUND","status":404,"message":"User not found"}
# Actual: <!doctype html><html>... (SPA index.html)

Debug findings

Using logs in node_modules/nitro/dist/vite.mjs:

[nitro:dev] devApp.fetch → status=404   (correct — no file-based route)
[nitro:dev] envRes (server.ts) → status=404 type=application/json  (correct — our handler)
[nitro:dev] but client receives → status=200 type=text/html  (WRONG — overridden!)

The server.ts fetch handler correctly returns a JSON 404 Response, but somewhere between dispatchFetch return and sendNodeResponse, the response is replaced with SPA HTML.

Expected behavior

server.ts fetch handler should have full control over the Response. If it returns a 404 with application/json content-type, that should be sent to the client as-is.

Only "unmatched" requests (where server.ts returns undefined or doesn't handle the path) should trigger SPA fallback.

Environment

  • nitro: 3.0.260311-beta
  • Vite 8 dev mode

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