remix-run/react-router · critical · Error

Missing body in server response

Error message

Missing body in server response

What it means

In the RSC SSR server pipeline, once a request is identified as a normal document (HTML) request — not a data, manifest, or RSC-action request, and not one where the handler already returned an RSC resource response — the returned server `Response` must have a body so the server can decode it, detect redirects, and build the HTML shell. A bodyless response at this point is treated as a programming error in the request handler and throws.

Source

Thrown at packages/react-router/lib/rsc/server.ssr.tsx:127

  hydrate?: boolean;
  nonce?: string;
}): Promise<Response> {
  const url = new URL(request.url);
  const isDataRequest = isReactServerRequest(url);
  const respondWithRSCPayload =
    isDataRequest ||
    isManifestRequest(url) ||
    request.headers.has("rsc-action-id");

  if (
    respondWithRSCPayload ||
    serverResponse.headers.get("React-Router-Resource") === "true"
  ) {
    return serverResponse;
  }

  if (!serverResponse.body) {
    throw new Error("Missing body in server response");
  }

  const detectRedirectResponse = serverResponse.clone();

  let serverResponseB: Response | null = null;
  if (hydrate) {
    serverResponseB = serverResponse.clone();
  }

  const body = serverResponse.body;

  let buffer: Uint8Array[] | undefined;
  let streamControllers: ReadableStreamDefaultController<Uint8Array>[] = [];

  const createStream = () => {
    if (!buffer) {
      buffer = [];
      return body.pipeThrough(

View on GitHub (pinned to 7aea711dd1)

Solutions

  1. Make every document-request code path return a Response with a real body (HTML string or stream), or use the framework's redirect utilities instead of hand-built bodyless responses.
  2. If you must return an empty status (e.g., 204), scope it to non-document requests or set `React-Router-Resource: true` handling appropriately so it bypasses the HTML pipeline.
  3. Ensure middleware returns a fresh Response when it has consumed the original body.
  4. Log `response.status`/`response.body` in your handler before returning to find the offending branch.

Example fix

// before (custom handleRequest)
if (isBlocked) {
  return new Response(null, { status: 204 });
}

// after
if (isBlocked) {
  return new Response("<html><body>Blocked</body></html>", {
    status: 403,
    headers: { "content-type": "text/html" },
  });
}
Defensive patterns

Strategy: validation

Validate before calling

// in a custom handleRequest, before returning
if (!response.body && !request.headers.has("rsc-action-id")) {
  return new Response("<html><body>Empty response</body></html>", {
    status: 200,
    headers: { "content-type": "text/html" },
  });
}
return response;

Type guard

const hasBody = (res: Response): boolean => res.body != null;

Prevention

When it happens

Trigger: A custom `handleRequest`/middleware returning `new Response(null, { status: 204 })` or any `Response` with a null body for document requests; returning the same `Response` object after its stream was already consumed elsewhere; redirect helpers misused so a bodyless 3xx reaches the document pipeline.

Common situations: Writing custom framework-mode server entries or middleware that short-circuit with empty responses; adapting express/cloudflare adapters that construct bare `Response` objects; middleware that returns `next()` results after reading them.

Related errors


AI-assisted analysis of remix-run/react-router@7aea711dd1 (2026-08-18). Data as JSON: /api/errors/d9a809ffa0e86c82. Report an issue: GitHub.