Skip to content

[Start] Preserve response-context headers in non-2xx serialized Server Function responses #7914

Description

@6mn12j

Which project does this relate to?

Start

Describe the bug

Headers set through the Server Function response context are lost when the final serialized Server Function Response has a non-2xx status.

This is status-dependent rather than specific to whether the Server Function returns a value or throws:

  • For 2xx serialized responses, h3 merges the response-context headers during final response processing.
  • For non-2xx serialized responses, h3 skips that merge because Response.ok is false.
  • TanStack Start currently preserves Set-Cookie separately for non-2xx responses, but other response-context headers are lost.

Minimal example:

import { createServerFn } from "@tanstack/react-start";
import {
  getResponseHeaders,
  setResponseStatus,
} from "@tanstack/react-start/server";

export const failingFn = createServerFn({ method: "GET" }).handler(
  async () => {
    getResponseHeaders().set("x-custom-header", "true");
    setResponseStatus(401);

    throw new Error("Expected error");
  },
);

After calling failingFn from the client, the resulting /_serverFn/... response has status 401 and the serialization headers, but does not contain x-custom-header: true.

An ordinary serialized return value is affected as well if its response status is set to a non-2xx value.

Complete minimal reproducer

To run it locally:

git clone https://github.com/6mn12j/tanstack-router-7914-repro.git
cd tanstack-router-7914-repro
npm ci
npm run dev

Steps to Reproduce the Bug

  1. Open the Railway reproduction, or clone and run the repository locally.
  2. Click Run reproduction once.
  3. Inspect both the rendered evidence and the resulting /_serverFn/... request in the browser Network panel.
  4. Observe that the response status is 401 and x-tss-serialized is true, while x-custom-header is missing.
  5. The same status-dependent loss applies to non-streaming, streaming/framed, and serialized error responses when the final HTTP status is non-2xx.

Expected behavior

Headers previously added to the Server Function response context should be preserved in the final serialized response for non-2xx statuses as well as 2xx statuses.

This matters beyond the application-specific reproduction. Headers such as WWW-Authenticate, Retry-After, CORS headers, rate-limit metadata, request IDs, and trace metadata are often required specifically on non-2xx responses.

The final merge should have one consistent conflict policy:

  • Framework-owned serialization headers such as Content-Type and x-tss-serialized should take precedence.
  • Headers invalidated by replacing or streaming the response body, such as a stale Content-Length, should be excluded or recalculated.
  • Multiple Set-Cookie values should be appended exactly once.

Screenshots or Videos

No response

Platform

  • Router / Start Version: @tanstack/react-start 1.167.50
  • Resolved @tanstack/start-server-core: 1.167.22
  • OS: macOS
  • Browser: Chrome
  • Bundler: Vite

Additional context

In our application, a backend error logger sets x-logged before rethrowing a 401 ApiError. An outer framework logger checks this marker to prevent duplicate logging.

Because the header is lost from the non-2xx response, both logging layers emit an entry for the same request. In Datadog, the paired events appear approximately 7–20 ms apart:

  • Backend 401 ApiError
  • TanStack Start Framework-level error

The full response path appears to be:

  1. server-functions-handler.ts creates serialized JSON and framed Response objects with status and framework serialization headers.
  2. request-response.ts handles final response attachment. For non-2xx responses, it currently carries over Set-Cookie only.
  3. h3 response finalization merges prepared response headers only when the returned Response is ok; it returns non-OK responses without that merge.

Could response-context headers be merged once at the final response boundary for both 2xx and non-2xx responses, with framework-owned and body-dependent headers handled according to the rules above?

A response-finalization fix may avoid duplicate merging of 2xx headers or Set-Cookie values that could occur if each serialization branch copied the response-context headers independently.

Useful regression coverage would be a status matrix such as 200 and 401 across:

  • Non-streaming serialized values
  • Streaming/framed values
  • Serialized errors
  • Raw Response returns

Related issues that appear adjacent but concern request-header propagation or cookie handling:

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