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
    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