diff --git a/.changeset/rich-teachers-warn.md b/.changeset/rich-teachers-warn.md
new file mode 100644
index 00000000000..c07df3aa7c8
--- /dev/null
+++ b/.changeset/rich-teachers-warn.md
@@ -0,0 +1,5 @@
+---
+"@remix-run/server-runtime": patch
+---
+
+Single Fetch: Fix redirects when a `basename` is presernt
diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts
index e383ff812ab..aad153c0d41 100644
--- a/integration/single-fetch-test.ts
+++ b/integration/single-fetch-test.ts
@@ -1463,6 +1463,71 @@ test.describe("single-fetch", () => {
expect(await app.getHtml("#target")).toContain("Target");
});
+ test("processes redirects when a basename is present", async ({ page }) => {
+ let fixture = await createFixture({
+ compiler: "vite",
+ files: {
+ ...files,
+ "vite.config.ts": js`
+ import { defineConfig } from "vite";
+ import { vitePlugin as remix } from "@remix-run/dev";
+ export default defineConfig({
+ plugins: [
+ remix({
+ basename: '/base',
+ future: {
+ unstable_singleFetch: true,
+ }
+ }),
+ ],
+ });
+ `,
+ "app/routes/data.tsx": js`
+ import { redirect } from '@remix-run/node';
+ export function loader() {
+ throw redirect('/target');
+ }
+ export default function Component() {
+ return null
+ }
+ `,
+ "app/routes/target.tsx": js`
+ export default function Component() {
+ return
Target
+ }
+ `,
+ },
+ });
+
+ console.error = () => {};
+
+ let res = await fixture.requestDocument("/base/data");
+ expect(res.status).toBe(302);
+ expect(res.headers.get("Location")).toBe("/base/target");
+ expect(await res.text()).toBe("");
+
+ let { status, data } = await fixture.requestSingleFetchData(
+ "/base/data.data"
+ );
+ expect(data).toEqual({
+ [SingleFetchRedirectSymbol]: {
+ status: 302,
+ redirect: "/target",
+ reload: false,
+ replace: false,
+ revalidate: false,
+ },
+ });
+ expect(status).toBe(202);
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/base/");
+ await app.clickLink("/base/data");
+ await page.waitForSelector("#target");
+ expect(await app.getHtml("#target")).toContain("Target");
+ });
+
test("processes thrown loader errors", async ({ page }) => {
let fixture = await createFixture({
config: {
diff --git a/packages/remix-server-runtime/single-fetch.ts b/packages/remix-server-runtime/single-fetch.ts
index e9298122a7b..b1c888f033a 100644
--- a/packages/remix-server-runtime/single-fetch.ts
+++ b/packages/remix-server-runtime/single-fetch.ts
@@ -8,6 +8,7 @@ import {
isRouteErrorResponse,
unstable_data as routerData,
UNSAFE_ErrorResponseImpl as ErrorResponseImpl,
+ stripBasename,
} from "@remix-run/router";
import { encode } from "turbo-stream";
@@ -111,7 +112,11 @@ export async function singleFetchAction(
// let non-Response return values through
if (isResponse(result)) {
return {
- result: getSingleFetchRedirect(result.status, result.headers),
+ result: getSingleFetchRedirect(
+ result.status,
+ result.headers,
+ build.basename
+ ),
headers: result.headers,
status: SINGLE_FETCH_REDIRECT_STATUS,
};
@@ -122,7 +127,11 @@ export async function singleFetchAction(
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) {
return {
- result: getSingleFetchRedirect(context.statusCode, headers),
+ result: getSingleFetchRedirect(
+ context.statusCode,
+ headers,
+ build.basename
+ ),
headers,
status: SINGLE_FETCH_REDIRECT_STATUS,
};
@@ -192,7 +201,8 @@ export async function singleFetchLoaders(
result: {
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
result.status,
- result.headers
+ result.headers,
+ build.basename
),
},
headers: result.headers,
@@ -208,7 +218,8 @@ export async function singleFetchLoaders(
result: {
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
context.statusCode,
- headers
+ headers,
+ build.basename
),
},
headers,
@@ -264,10 +275,17 @@ export async function singleFetchLoaders(
export function getSingleFetchRedirect(
status: number,
- headers: Headers
+ headers: Headers,
+ basename: string | undefined
): SingleFetchRedirectResult {
+ let redirect = headers.get("Location")!;
+
+ if (basename) {
+ redirect = stripBasename(redirect, basename) || redirect;
+ }
+
return {
- redirect: headers.get("Location")!,
+ redirect,
status,
revalidate:
// Technically X-Remix-Revalidate isn't needed here - that was an implementation