diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx
index c2bb6d5319..03591c834c 100644
--- a/packages/react-router/lib/dom/ssr/single-fetch.tsx
+++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx
@@ -18,9 +18,7 @@ import {
 import { createRequestInit } from "./data";
 import type { AssetsManifest, EntryContext } from "./entry";
 import { escapeHtml } from "./markup";
-import type { RouteModule, RouteModules } from "./routeModules";
 import invariant from "./invariant";
-import type { EntryRoute } from "./routes";
 
 export const SingleFetchRedirectSymbol = Symbol("SingleFetchRedirect");
 
@@ -32,15 +30,22 @@ export type SingleFetchRedirectResult = {
   replace: boolean;
 };
 
+// Shared/serializable type used by both turbo-stream and RSC implementations
+type DecodedSingleFetchResults =
+  | { routes: { [key: string]: SingleFetchResult } }
+  | { redirect: SingleFetchRedirectResult };
+
+// This and SingleFetchResults are only used over the wire, and are converted to
+// DecodedSingleFetchResults in `fetchAndDecode`.  This way turbo-stream/RSC
+// can use the same `unwrapSingleFetchResult` implementation.
 export type SingleFetchResult =
   | { data: unknown }
   | { error: unknown }
   | SingleFetchRedirectResult;
 
-export type SingleFetchResults = {
-  [key: string]: SingleFetchResult;
-  [SingleFetchRedirectSymbol]?: SingleFetchRedirectResult;
-};
+export type SingleFetchResults =
+  | { [key: string]: SingleFetchResult }
+  | { [SingleFetchRedirectSymbol]: SingleFetchRedirectResult };
 
 interface StreamTransferProps {
   context: EntryContext;
@@ -50,6 +55,16 @@ interface StreamTransferProps {
   nonce?: string;
 }
 
+// Some status codes are not permitted to have bodies, so we want to just
+// treat those as "no data" instead of throwing an exception:
+//   https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx
+//   https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content
+//   https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content
+//
+// Note: 304 is not included here because the browser should fill those responses
+// with the cached body content.
+export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]);
+
 // StreamTransfer recursively renders down chunks of the `serverHandoffStream`
 // into the client-side `streamController`
 export function StreamTransfer({
@@ -245,12 +260,13 @@ async function singleFetchActionStrategy(
     let result = await handler(async () => {
       let url = singleFetchUrl(request.url, basename);
       let init = await createRequestInit(request);
-      let { data, status } = await fetchAndDecode(url, init);
-      actionStatus = status;
-      return unwrapSingleFetchResult(
-        data as SingleFetchResult,
+      let { data, status } = await fetchAndDecode(
+        url,
+        init,
         actionMatch!.route.id
       );
+      actionStatus = status;
+      return unwrapSingleFetchResult(data, actionMatch!.route.id);
     });
     return result;
   });
@@ -308,23 +324,17 @@ async function singleFetchLoaderNavigationStrategy(
   router: DataRouter,
   basename: string | undefined
 ) {
-  // Track which routes need a server load - in case we need to tack on a
-  // `_routes` param
+  // Track which routes need a server load for use in a `_routes` param
   let routesParams = new Set<string>();
 
-  // We only add `_routes` when one or more routes opts out of a load via
-  // `shouldRevalidate` or `clientLoader`
+  // Only add `_routes` when at least 1 route opts out via `shouldRevalidate`/`clientLoader`
   let foundOptOutRoute = false;
 
-  // Deferreds for each route so we can be sure they've all loaded via
-  // `match.resolve()`, and a singular promise that can tell us all routes
-  // have been resolved
+  // Deferreds per-route so we can be sure they've all loaded via `match.resolve()`
   let routeDfds = matches.map(() => createDeferred<void>());
-  let routesLoadedPromise = Promise.all(routeDfds.map((d) => d.promise));
 
-  // Deferred that we'll use for the call to the server that each match can
-  // await and parse out it's specific result
-  let singleFetchDfd = createDeferred<SingleFetchResults>();
+  // Deferred we'll use for the singleular call to the server
+  let singleFetchDfd = createDeferred<DecodedSingleFetchResults>();
 
   // Base URL and RequestInit for calls to the server
   let url = stripIndexParam(singleFetchUrl(request.url, basename));
@@ -339,6 +349,7 @@ async function singleFetchLoaderNavigationStrategy(
         routeDfds[i].resolve();
 
         let manifestRoute = manifest.routes[m.route.id];
+        invariant(manifestRoute, "No manifest route found for dataStrategy");
 
         let defaultShouldRevalidate =
           !m.unstable_shouldRevalidateArgs ||
@@ -347,8 +358,7 @@ async function singleFetchLoaderNavigationStrategy(
         let shouldCall = m.unstable_shouldCallHandler(defaultShouldRevalidate);
 
         if (!shouldCall) {
-          // If this route opted out of revalidation, we don't want to include
-          // it in the single fetch .data request
+          // If this route opted out, don't include in the .data request
           foundOptOutRoute ||=
             m.unstable_shouldRevalidateArgs != null && // This is a revalidation,
             manifestRoute?.hasLoader === true && // for a route with a server loader,
@@ -358,7 +368,7 @@ async function singleFetchLoaderNavigationStrategy(
 
         // When a route has a client loader, it opts out of the singular call and
         // calls it's server loader via `serverLoader()` using a `?_routes` param
-        if (manifestRoute && manifestRoute.hasClientLoader) {
+        if (manifestRoute.hasClientLoader) {
           if (manifestRoute.hasLoader) {
             foundOptOutRoute = true;
           }
@@ -385,7 +395,7 @@ async function singleFetchLoaderNavigationStrategy(
         try {
           let result = await handler(async () => {
             let data = await singleFetchDfd.promise;
-            return unwrapSingleFetchResults(data, m.route.id);
+            return unwrapSingleFetchResult(data, m.route.id);
           });
           results[m.route.id] = {
             type: "data",
@@ -402,7 +412,7 @@ async function singleFetchLoaderNavigationStrategy(
   );
 
   // Wait for all routes to resolve above before we make the HTTP call
-  await routesLoadedPromise;
+  await Promise.all(routeDfds.map((d) => d.promise));
 
   // We can skip the server call:
   // - On initial hydration - only clientLoaders can pass through via `clientLoader.hydrate`
@@ -417,24 +427,18 @@ async function singleFetchLoaderNavigationStrategy(
   ) {
     singleFetchDfd.resolve({});
   } else {
-    try {
-      // When one or more routes have opted out, we add a _routes param to
-      // limit the loaders to those that have a server loader and did not
-      // opt out
-      if (ssr && foundOptOutRoute && routesParams.size > 0) {
-        url.searchParams.set(
-          "_routes",
-          matches
-            .filter((m) => routesParams.has(m.route.id))
-            .map((m) => m.route.id)
-            .join(",")
-        );
-      }
+    // When routes have opted out, add a `_routes` param to filter server loaders
+    // Skipped in `ssr:false` because we expect to be loading static `.data` files
+    if (ssr && foundOptOutRoute && routesParams.size > 0) {
+      let routes = [...routesParams.keys()].join(",");
+      url.searchParams.set("_routes", routes);
+    }
 
+    try {
       let data = await fetchAndDecode(url, init);
-      singleFetchDfd.resolve(data.data as SingleFetchResults);
+      singleFetchDfd.resolve(data.data);
     } catch (e) {
-      singleFetchDfd.reject(e as Error);
+      singleFetchDfd.reject(e);
     }
   }
 
@@ -471,7 +475,7 @@ function fetchSingleLoader(
     let singleLoaderUrl = new URL(url);
     singleLoaderUrl.searchParams.set("_routes", routeId);
     let { data } = await fetchAndDecode(singleLoaderUrl, init);
-    return unwrapSingleFetchResults(data as SingleFetchResults, routeId);
+    return unwrapSingleFetchResult(data, routeId);
   });
 }
 
@@ -520,8 +524,9 @@ export function singleFetchUrl(
 
 async function fetchAndDecode(
   url: URL,
-  init: RequestInit
-): Promise<{ status: number; data: unknown }> {
+  init: RequestInit,
+  routeId?: string
+): Promise<{ status: number; data: DecodedSingleFetchResults }> {
   let res = await fetch(url, init);
 
   // If this 404'd without hitting the running server (most likely in a
@@ -530,27 +535,39 @@ async function fetchAndDecode(
     throw new ErrorResponseImpl(404, "Not Found", true);
   }
 
-  // some status codes are not permitted to have bodies, so we want to just
-  // treat those as "no data" instead of throwing an exception.
-  // 304 is not included here because the browser should fill those responses
-  // with the cached body content.
-  const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205]);
   if (NO_BODY_STATUS_CODES.has(res.status)) {
-    if (!init.method || init.method === "GET") {
-      // SingleFetchResults can just have no routeId keys which will result
-      // in no data for all routes
-      return { status: res.status, data: {} };
-    } else {
-      // SingleFetchResult is for a singular route and can specify no data
-      return { status: res.status, data: { data: undefined } };
+    let routes: { [key: string]: SingleFetchResult } = {};
+    if (routeId) {
+      routes[routeId] = { data: undefined };
     }
+    return {
+      status: res.status,
+      data: { routes },
+    };
   }
 
   invariant(res.body, "No response body to decode");
 
   try {
     let decoded = await decodeViaTurboStream(res.body, window);
-    return { status: res.status, data: decoded.value };
+    let data: DecodedSingleFetchResults;
+    if (!init.method || init.method === "GET") {
+      let typed = decoded.value as SingleFetchResults;
+      if (SingleFetchRedirectSymbol in typed) {
+        data = { redirect: typed[SingleFetchRedirectSymbol] };
+      } else {
+        data = { routes: typed };
+      }
+    } else {
+      let typed = decoded.value as SingleFetchResult;
+      invariant(routeId, "No routeId found for single fetch call decoding");
+      if ("redirect" in typed) {
+        data = { redirect: typed };
+      } else {
+        data = { routes: { [routeId]: typed } };
+      }
+    }
+    return { status: res.status, data };
   } catch (e) {
     // Can't clone after consuming the body via turbo-stream so we can't
     // include the body here.  In an ideal world we'd look for a turbo-stream
@@ -617,37 +634,34 @@ export function decodeViaTurboStream(
   });
 }
 
-function unwrapSingleFetchResults(
-  results: SingleFetchResults,
+function unwrapSingleFetchResult(
+  result: DecodedSingleFetchResults,
   routeId: string
 ) {
-  let redirect = results[SingleFetchRedirectSymbol];
-  if (redirect) {
-    return unwrapSingleFetchResult(redirect, routeId);
+  if ("redirect" in result) {
+    let {
+      redirect: location,
+      revalidate,
+      reload,
+      replace,
+      status,
+    } = result.redirect;
+    throw redirect(location, {
+      status,
+      headers: {
+        // Three R's of redirecting (lol Veep)
+        ...(revalidate ? { "X-Remix-Revalidate": "yes" } : null),
+        ...(reload ? { "X-Remix-Reload-Document": "yes" } : null),
+        ...(replace ? { "X-Remix-Replace": "yes" } : null),
+      },
+    });
   }
 
-  return results[routeId] !== undefined
-    ? unwrapSingleFetchResult(results[routeId], routeId)
-    : null;
-}
-
-function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
-  if ("error" in result) {
-    throw result.error;
-  } else if ("redirect" in result) {
-    let headers: Record<string, string> = {};
-    if (result.revalidate) {
-      headers["X-Remix-Revalidate"] = "yes";
-    }
-    if (result.reload) {
-      headers["X-Remix-Reload-Document"] = "yes";
-    }
-    if (result.replace) {
-      headers["X-Remix-Replace"] = "yes";
-    }
-    throw redirect(result.redirect, { status: result.status, headers });
-  } else if ("data" in result) {
-    return result.data;
+  let routeResult = result.routes[routeId];
+  if ("error" in routeResult) {
+    throw routeResult.error;
+  } else if ("data" in routeResult) {
+    return routeResult.data;
   } else {
     throw new Error(`No response found for routeId "${routeId}"`);
   }
@@ -655,7 +669,7 @@ function unwrapSingleFetchResult(result: SingleFetchResult, routeId: string) {
 
 function createDeferred<T = unknown>() {
   let resolve: (val?: any) => Promise<void>;
-  let reject: (error?: Error) => Promise<void>;
+  let reject: (error?: unknown) => Promise<void>;
   let promise = new Promise<T>((res, rej) => {
     resolve = async (val: T) => {
       res(val);
@@ -663,7 +677,7 @@ function createDeferred<T = unknown>() {
         await promise;
       } catch (e) {}
     };
-    reject = async (error?: Error) => {
+    reject = async (error?: unknown) => {
       rej(error);
       try {
         await promise;
diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts
index e7bcf80978..ab22c9c2e5 100644
--- a/packages/react-router/lib/server-runtime/routes.ts
+++ b/packages/react-router/lib/server-runtime/routes.ts
@@ -2,7 +2,6 @@ import type {
   AgnosticDataRouteObject,
   LoaderFunctionArgs as RRLoaderFunctionArgs,
   ActionFunctionArgs as RRActionFunctionArgs,
-  RedirectFunction,
   RouteManifest,
   unstable_MiddlewareFunction,
 } from "../router/utils";
diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts
index 84c0bac3c9..5cba00bcd4 100644
--- a/packages/react-router/lib/server-runtime/server.ts
+++ b/packages/react-router/lib/server-runtime/server.ts
@@ -24,18 +24,21 @@ import type { ServerRoute } from "./routes";
 import { createStaticHandlerDataRoutes, createRoutes } from "./routes";
 import { createServerHandoffString } from "./serverHandoff";
 import { getDevServerHooks } from "./dev";
-import type { SingleFetchResult, SingleFetchResults } from "./single-fetch";
 import {
   encodeViaTurboStream,
   getSingleFetchRedirect,
   singleFetchAction,
   singleFetchLoaders,
-  SingleFetchRedirectSymbol,
   SINGLE_FETCH_REDIRECT_STATUS,
-  NO_BODY_STATUS_CODES,
+  SERVER_NO_BODY_STATUS_CODES,
 } from "./single-fetch";
 import { getDocumentHeaders } from "./headers";
 import type { EntryRoute } from "../dom/ssr/routes";
+import type {
+  SingleFetchResult,
+  SingleFetchResults,
+} from "../dom/ssr/single-fetch";
+import { SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch";
 import type { MiddlewareEnabled } from "../types/future";
 
 export type RequestHandler = (
@@ -448,7 +451,7 @@ async function handleDocumentRequest(
     let headers = getDocumentHeaders(build, context);
 
     // Skip response body for unsupported status codes
-    if (NO_BODY_STATUS_CODES.has(context.statusCode)) {
+    if (SERVER_NO_BODY_STATUS_CODES.has(context.statusCode)) {
       return new Response(null, { status: context.statusCode, headers });
     }
 
diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts
index 145b09f4bc..21c2d84538 100644
--- a/packages/react-router/lib/server-runtime/single-fetch.ts
+++ b/packages/react-router/lib/server-runtime/single-fetch.ts
@@ -18,27 +18,16 @@ import type {
   SingleFetchResult,
   SingleFetchResults,
 } from "../dom/ssr/single-fetch";
-import { SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch";
+import {
+  NO_BODY_STATUS_CODES,
+  SingleFetchRedirectSymbol,
+} from "../dom/ssr/single-fetch";
 import type { AppLoadContext } from "./data";
 import { sanitizeError, sanitizeErrors } from "./errors";
 import { ServerMode } from "./mode";
 import { getDocumentHeaders } from "./headers";
 import type { ServerBuild } from "./build";
 
-export type { SingleFetchResult, SingleFetchResults };
-export { SingleFetchRedirectSymbol };
-
-// Do not include a response body if the status code is one of these,
-// otherwise `undici` will throw an error when constructing the Response:
-//   https://github.com/nodejs/undici/blob/bd98a6303e45d5e0d44192a93731b1defdb415f3/lib/web/fetch/response.js#L522-L528
-//
-// Specs:
-//   https://datatracker.ietf.org/doc/html/rfc9110#name-informational-1xx
-//   https://datatracker.ietf.org/doc/html/rfc9110#name-204-no-content
-//   https://datatracker.ietf.org/doc/html/rfc9110#name-205-reset-content
-//   https://datatracker.ietf.org/doc/html/rfc9110#name-304-not-modified
-export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205, 304]);
-
 // We can't use a 3xx status or else the `fetch()` would follow the redirect.
 // We need to communicate the redirect back as data so we can act on it in the
 // client side router.  We use a 202 to avoid any automatic caching we might
@@ -46,6 +35,14 @@ export const NO_BODY_STATUS_CODES = new Set([100, 101, 204, 205, 304]);
 // the user control cache behavior via Cache-Control
 export const SINGLE_FETCH_REDIRECT_STATUS = 202;
 
+// Add 304 for server side - that is not included in the client side logic
+// because the browser should fill those responses with the cached data
+// https://datatracker.ietf.org/doc/html/rfc9110#name-304-not-modified
+export const SERVER_NO_BODY_STATUS_CODES = new Set([
+  ...NO_BODY_STATUS_CODES,
+  304,
+]);
+
 export async function singleFetchAction(
   build: ServerBuild,
   serverMode: ServerMode,
@@ -280,7 +277,7 @@ function generateSingleFetchResponse(
   resultHeaders.set("X-Remix-Response", "yes");
 
   // Skip response body for unsupported status codes
-  if (NO_BODY_STATUS_CODES.has(status)) {
+  if (SERVER_NO_BODY_STATUS_CODES.has(status)) {
     return new Response(null, { status, headers: resultHeaders });
   }