Skip to content

HEAD requests to server routes return text/html instead of handler's content-type #7270

@pyyupsk

Description

@pyyupsk

Which project does this relate to?

Start

Describe the bug

Server route handlers (createFileRoute with server.handlers.GET) return the correct content-type on GET, but HEAD to the same path returns content-type: text/html; charset=utf-8 (the SSR default from getStartResponseHeaders) and an empty body. Handler appears not to run for HEAD; the SSR shell takes over.

Real consumers (browsers, RSS readers, sitemap parsers) use GET so impact is small, but it breaks tools that do HEAD probes (curl -sI, link checkers, uptime monitors) and conflicts with the HTTP spec — RFC 9110 §9.3.2 requires HEAD to send the same header fields as GET.

Confirmed still reproducing on @tanstack/react-start 1.167.49 + @tanstack/react-router 1.168.24 (latest as of filing).

Your Example Website or App

https://fasu.dev/sitemap.xml (TanStack Start on Cloudflare Workers)

Route source:

// src/routes/sitemap[.]xml.ts
import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/sitemap.xml")({
  server: {
    handlers: {
      GET: () =>
        new Response(buildSitemap(), {
          headers: {
            "content-type": "application/xml; charset=utf-8",
            "cache-control": "public, max-age=3600",
          },
        }),
    },
  },
});

Steps to Reproduce the Bug or Issue

  1. Create a server-only file route with a GET handler returning new Response(body, { headers: { "content-type": "application/xml; charset=utf-8" } }).
  2. curl -i https://fasu.dev/sitemap.xmlcontent-type: application/xml; charset=utf-8
  3. curl -I https://fasu.dev/sitemap.xmlcontent-type: text/html; charset=utf-8

Reproduces in both wrangler dev and vite preview. Same behavior across /sitemap.xml, /llms.txt, /rss.xml, and a /windsurf shell-script proxy route.

Expected behavior

HEAD returns the same headers as GET. Per RFC 9110 §9.3.2: "the server SHOULD send the same header fields in response to a HEAD request as it would have sent if the request method had been GET."

Screenshots or Videos

$ curl -sI https://fasu.dev/sitemap.xml
HTTP/2 200
content-type: text/html; charset=utf-8       # ❌
...

$ curl -s -o /dev/null -D - https://fasu.dev/sitemap.xml
HTTP/2 200
content-type: application/xml; charset=utf-8  # ✅
cache-control: public, max-age=3600
...

Platform

  • Router / Start Version: @tanstack/react-start 1.167.49, @tanstack/react-router 1.168.24, @tanstack/router-plugin 1.167.27
  • OS: Linux (Cloudflare Workers runtime + local repro)
  • Browser: n/a (curl)
  • Bundler: vite 8.0.10 + @cloudflare/vite-plugin 1.33.2
  • Wrangler: 4.85.0

Additional context

Tracing through the bundle: handleServerRoutes builds routeMiddlewares = [handlerMiddleware, executeRouter]. On GET, handler middleware returns a Response and executeMiddleware short-circuits. On HEAD, the handler appears to be skipped (or its return discarded) and executeRouter runs, invoking defaultStreamHandler whose responseHeaders default to text/html; charset=utf-8.

Likely culprit in handleServerRoutes:

const handler = handlers[request.method.toUpperCase()] ?? handlers["ANY"];

HEAD has no entry, no ANY fallback, so the handler is never registered and the chain falls through to SSR.

Possible fixes:

  • For server-only routes (no component), auto-fall-back HEAD → GET handler with body stripped (matches RFC 9110 and common framework behavior).
  • Or document that users must explicitly add a HEAD handler when defining GET.

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