Skip to content

Commit 4dfb883

Browse files
authored
Delay turbo-stream serialization of .data redirects (#14205)
1 parent c208585 commit 4dfb883

File tree

4 files changed

+126
-100
lines changed

4 files changed

+126
-100
lines changed

.changeset/quiet-cows-build.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"react-router": patch
3+
---
4+
5+
[UNSTABLE] Delay serialization of `.data` redirects to 202 responses until after middleware chain

integration/middleware-test.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import type {
33
Response as PlaywrightResponse,
44
} from "@playwright/test";
55
import { test, expect } from "@playwright/test";
6-
import { UNSAFE_ErrorResponseImpl, UNSAFE_ServerMode } from "react-router";
6+
import {
7+
UNSAFE_ErrorResponseImpl,
8+
UNSAFE_ServerMode,
9+
UNSAFE_SingleFetchRedirectSymbol,
10+
} from "react-router";
711

812
import {
913
createAppFixture,
@@ -1876,8 +1880,12 @@ test.describe("Middleware", () => {
18761880
},
18771881
});
18781882

1879-
let appFixture = await createAppFixture(fixture);
1883+
let res = await fixture.requestDocument("/redirect");
1884+
expect(res.status).toBe(302);
1885+
expect(res.headers.get("location")).toBe("/target");
1886+
expect(res.body).toBeNull();
18801887

1888+
let appFixture = await createAppFixture(fixture);
18811889
let app = new PlaywrightFixture(appFixture, page);
18821890
await app.goto("/");
18831891
await page.waitForSelector('a:has-text("Link")');
@@ -1933,8 +1941,12 @@ test.describe("Middleware", () => {
19331941
},
19341942
});
19351943

1936-
let appFixture = await createAppFixture(fixture);
1944+
let res = await fixture.requestDocument("/redirect");
1945+
expect(res.status).toBe(302);
1946+
expect(res.headers.get("location")).toBe("/target");
1947+
expect(res.body).toBeNull();
19371948

1949+
let appFixture = await createAppFixture(fixture);
19381950
let app = new PlaywrightFixture(appFixture, page);
19391951
await app.goto("/");
19401952
await page.waitForSelector('a:has-text("Link")');
@@ -1945,6 +1957,66 @@ test.describe("Middleware", () => {
19451957
appFixture.close();
19461958
});
19471959

1960+
test("doesn't serialize single fetch redirects until after the middleware chain", async ({
1961+
page,
1962+
}) => {
1963+
let fixture = await createFixture({
1964+
files: {
1965+
"react-router.config.ts": reactRouterConfig({
1966+
middleware: true,
1967+
}),
1968+
"vite.config.ts": js`
1969+
import { defineConfig } from "vite";
1970+
import { reactRouter } from "@react-router/dev/vite";
1971+
1972+
export default defineConfig({
1973+
build: { manifest: true },
1974+
plugins: [reactRouter()],
1975+
});
1976+
`,
1977+
"app/routes/redirect.tsx": js`
1978+
import { Link, redirect } from 'react-router'
1979+
export const unstable_middleware = [
1980+
async ({ request, context }, next) => {
1981+
let res = await next();
1982+
// Should still be a normal redirect here, not yet encoded into
1983+
// a single fetch redirect
1984+
res.headers.set("X-Status", res.status);
1985+
res.headers.set("X-Location", res.headers.get('Location'));
1986+
return res;
1987+
}
1988+
]
1989+
export function loader() {
1990+
throw redirect('/target');
1991+
}
1992+
export default function Component() {
1993+
return <h1>Redirect</h1>
1994+
}
1995+
`,
1996+
"app/routes/target.tsx": js`
1997+
export default function Component() {
1998+
return <h1>Target</h1>
1999+
}
2000+
`,
2001+
},
2002+
});
2003+
2004+
let res = await fixture.requestSingleFetchData("/redirect.data");
2005+
expect(res.status).toBe(202);
2006+
expect(res.headers.get("location")).toBe(null);
2007+
expect(res.headers.get("x-status")).toBe("302");
2008+
expect(res.headers.get("x-location")).toBe("/target");
2009+
expect(res.data).toEqual({
2010+
[UNSAFE_SingleFetchRedirectSymbol]: {
2011+
redirect: "/target",
2012+
reload: false,
2013+
replace: false,
2014+
revalidate: false,
2015+
status: 302,
2016+
},
2017+
});
2018+
});
2019+
19482020
test("handles errors thrown on the way down (document)", async ({
19492021
page,
19502022
}) => {

packages/react-router/lib/server-runtime/server.ts

Lines changed: 15 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,13 @@ import { createServerHandoffString } from "./serverHandoff";
2727
import { getBuildTimeHeader, getDevServerHooks } from "./dev";
2828
import {
2929
encodeViaTurboStream,
30-
getSingleFetchRedirect,
3130
singleFetchAction,
3231
singleFetchLoaders,
3332
SERVER_NO_BODY_STATUS_CODES,
33+
generateSingleFetchRedirectResponse,
3434
} from "./single-fetch";
3535
import { getDocumentHeaders } from "./headers";
3636
import type { EntryRoute } from "../dom/ssr/routes";
37-
import type {
38-
SingleFetchResult,
39-
SingleFetchResults,
40-
} from "../dom/ssr/single-fetch";
41-
import {
42-
SINGLE_FETCH_REDIRECT_STATUS,
43-
SingleFetchRedirectSymbol,
44-
} from "../dom/ssr/single-fetch";
4537
import type { MiddlewareEnabled } from "../types/future";
4638
import { getManifestPath } from "../dom/ssr/fog-of-war";
4739

@@ -269,6 +261,15 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
269261
handleError,
270262
);
271263

264+
if (isRedirectResponse(response)) {
265+
response = generateSingleFetchRedirectResponse(
266+
response,
267+
request,
268+
_build,
269+
serverMode,
270+
);
271+
}
272+
272273
if (_build.entry.module.handleDataRequest) {
273274
response = await _build.entry.module.handleDataRequest(response, {
274275
context: loadContext,
@@ -277,32 +278,11 @@ export const createRequestHandler: CreateRequestHandlerFunction = (
277278
});
278279

279280
if (isRedirectResponse(response)) {
280-
let result: SingleFetchResult | SingleFetchResults =
281-
getSingleFetchRedirect(
282-
response.status,
283-
response.headers,
284-
_build.basename,
285-
);
286-
287-
if (request.method === "GET") {
288-
result = {
289-
[SingleFetchRedirectSymbol]: result,
290-
};
291-
}
292-
let headers = new Headers(response.headers);
293-
headers.set("Content-Type", "text/x-script");
294-
295-
return new Response(
296-
encodeViaTurboStream(
297-
result,
298-
request.signal,
299-
_build.entry.module.streamTimeout,
300-
serverMode,
301-
),
302-
{
303-
status: SINGLE_FETCH_REDIRECT_STATUS,
304-
headers,
305-
},
281+
response = generateSingleFetchRedirectResponse(
282+
response,
283+
request,
284+
_build,
285+
serverMode,
306286
);
307287
}
308288
}

packages/react-router/lib/server-runtime/single-fetch.ts

Lines changed: 31 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { encode } from "../../vendor/turbo-stream-v2/turbo-stream";
22
import type { StaticHandler, StaticHandlerContext } from "../router/router";
3-
import {
4-
isRedirectResponse,
5-
isRedirectStatusCode,
6-
isResponse,
7-
} from "../router/router";
3+
import { isRedirectStatusCode, isResponse } from "../router/router";
84
import type { unstable_RouterContextProvider } from "../router/utils";
95
import {
106
isRouteErrorResponse,
@@ -78,25 +74,7 @@ export async function singleFetchAction(
7874
function handleQueryResult(
7975
result: Awaited<ReturnType<StaticHandler["query"]>>,
8076
) {
81-
if (!isResponse(result)) {
82-
result = staticContextToResponse(result);
83-
}
84-
85-
// Unlike `handleDataRequest`, when singleFetch is enabled, query does
86-
// let non-Response return values through
87-
if (isRedirectResponse(result)) {
88-
return generateSingleFetchResponse(request, build, serverMode, {
89-
result: getSingleFetchRedirect(
90-
result.status,
91-
result.headers,
92-
build.basename,
93-
),
94-
headers: result.headers,
95-
status: SINGLE_FETCH_REDIRECT_STATUS,
96-
});
97-
}
98-
99-
return result;
77+
return isResponse(result) ? result : staticContextToResponse(result);
10078
}
10179

10280
function handleQueryError(error: unknown) {
@@ -113,15 +91,7 @@ export async function singleFetchAction(
11391
let headers = getDocumentHeaders(context, build);
11492

11593
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) {
116-
return generateSingleFetchResponse(request, build, serverMode, {
117-
result: getSingleFetchRedirect(
118-
context.statusCode,
119-
headers,
120-
build.basename,
121-
),
122-
headers,
123-
status: SINGLE_FETCH_REDIRECT_STATUS,
124-
});
94+
return new Response(null, { status: context.statusCode, headers });
12595
}
12696

12797
// Sanitize errors outside of development environments
@@ -194,24 +164,7 @@ export async function singleFetchLoaders(
194164
// Handle the query() result - either inside stream() with middleware enabled
195165
// or after query() without
196166
function handleQueryResult(result: StaticHandlerContext | Response) {
197-
let response = isResponse(result)
198-
? result
199-
: staticContextToResponse(result);
200-
if (isRedirectResponse(response)) {
201-
return generateSingleFetchResponse(request, build, serverMode, {
202-
result: {
203-
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
204-
response.status,
205-
response.headers,
206-
build.basename,
207-
),
208-
},
209-
headers: response.headers,
210-
status: SINGLE_FETCH_REDIRECT_STATUS,
211-
});
212-
}
213-
214-
return response;
167+
return isResponse(result) ? result : staticContextToResponse(result);
215168
}
216169

217170
// Handle any thrown errors from query() result - either inside stream() with
@@ -230,17 +183,7 @@ export async function singleFetchLoaders(
230183
let headers = getDocumentHeaders(context, build);
231184

232185
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) {
233-
return generateSingleFetchResponse(request, build, serverMode, {
234-
result: {
235-
[SingleFetchRedirectSymbol]: getSingleFetchRedirect(
236-
context.statusCode,
237-
headers,
238-
build.basename,
239-
),
240-
},
241-
headers,
242-
status: SINGLE_FETCH_REDIRECT_STATUS,
243-
});
186+
return new Response(null, { status: context.statusCode, headers });
244187
}
245188

246189
// Sanitize errors outside of development environments
@@ -334,6 +277,32 @@ function generateSingleFetchResponse(
334277
);
335278
}
336279

280+
export function generateSingleFetchRedirectResponse(
281+
redirectResponse: Response,
282+
request: Request,
283+
build: ServerBuild,
284+
serverMode: ServerMode,
285+
) {
286+
let redirect = getSingleFetchRedirect(
287+
redirectResponse.status,
288+
redirectResponse.headers,
289+
build.basename,
290+
);
291+
292+
let headers = new Headers(redirectResponse.headers);
293+
headers.delete("Location");
294+
headers.set("Content-Type", "text/x-script");
295+
296+
return generateSingleFetchResponse(request, build, serverMode, {
297+
result:
298+
request.method === "GET"
299+
? { [SingleFetchRedirectSymbol]: redirect }
300+
: redirect,
301+
headers,
302+
status: SINGLE_FETCH_REDIRECT_STATUS,
303+
});
304+
}
305+
337306
export function getSingleFetchRedirect(
338307
status: number,
339308
headers: Headers,

0 commit comments

Comments
 (0)