Skip to content

Commit 2d76210

Browse files
committed
Properly handle interrupted manifest requests
1 parent 80d61b2 commit 2d76210

File tree

5 files changed

+80
-10
lines changed

5 files changed

+80
-10
lines changed

.changeset/young-stingrays-begin.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+
Properly handle interrupted manifest requests in lazy route discovery

integration/fog-of-war-test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,4 +1273,59 @@ test.describe("Fog of War", () => {
12731273
),
12741274
]);
12751275
});
1276+
1277+
test.only("handles interruptions from back to back navigations", async ({
1278+
page,
1279+
}) => {
1280+
let fixture = await createFixture({
1281+
files: {
1282+
...getFiles(),
1283+
"app/routes/a.tsx": js`
1284+
import { Link, Outlet, useLoaderData, useNavigate } from "react-router";
1285+
export function loader({ request }) {
1286+
return { message: "A LOADER" };
1287+
}
1288+
export default function Index() {
1289+
let data = useLoaderData();
1290+
let navigate = useNavigate();
1291+
return (
1292+
<>
1293+
<h1 id="a">A: {data.message}</h1>
1294+
<button data-link onClick={async () => {
1295+
navigate('/a/b');
1296+
setTimeout(() => navigate('/a/b'), 0)
1297+
}}>
1298+
/a/b
1299+
</button>
1300+
<Outlet/>
1301+
</>
1302+
)
1303+
}
1304+
`,
1305+
},
1306+
});
1307+
let appFixture = await createAppFixture(fixture);
1308+
let app = new PlaywrightFixture(appFixture, page);
1309+
1310+
await app.goto("/a", true);
1311+
expect(
1312+
await page.evaluate(() =>
1313+
Object.keys((window as any).__reactRouterManifest.routes)
1314+
)
1315+
).toEqual(["root", "routes/a", "routes/_index"]);
1316+
1317+
// /a/b gets discovered on click
1318+
await app.clickElement("[data-link]");
1319+
await new Promise((resolve) => setTimeout(resolve, 1000));
1320+
expect(await (await page.$("body"))?.textContent()).not.toContain(
1321+
"Not Found"
1322+
);
1323+
await page.waitForSelector("#b");
1324+
1325+
expect(
1326+
await page.evaluate(() =>
1327+
Object.keys((window as any).__reactRouterManifest.routes)
1328+
)
1329+
).toEqual(["root", "routes/a", "routes/_index", "routes/a.b"]);
1330+
});
12761331
});

packages/react-router/lib/dom/ssr/fog-of-war.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ export function getPatchRoutesOnNavigationFunction(
7777
return undefined;
7878
}
7979

80-
return async ({ path, patch }) => {
80+
return async ({ path, patch, signal }) => {
8181
if (discoveredPaths.has(path)) {
8282
return;
8383
}
@@ -87,7 +87,8 @@ export function getPatchRoutesOnNavigationFunction(
8787
routeModules,
8888
isSpaMode,
8989
basename,
90-
patch
90+
patch,
91+
signal
9192
);
9293
};
9394
}
@@ -185,7 +186,8 @@ export async function fetchAndApplyManifestPatches(
185186
routeModules: RouteModules,
186187
isSpaMode: boolean,
187188
basename: string | undefined,
188-
patchRoutes: DataRouter["patchRoutes"]
189+
patchRoutes: DataRouter["patchRoutes"],
190+
signal?: AbortSignal
189191
): Promise<void> {
190192
let manifestPath = `${basename != null ? basename : "/"}/__manifest`.replace(
191193
/\/+/g,
@@ -203,15 +205,21 @@ export async function fetchAndApplyManifestPatches(
203205
return;
204206
}
205207

206-
let res = await fetch(url);
208+
let serverPatches: AssetsManifest["routes"];
209+
try {
210+
let res = await fetch(url, { signal });
207211

208-
if (!res.ok) {
209-
throw new Error(`${res.status} ${res.statusText}`);
210-
} else if (res.status >= 400) {
211-
throw new Error(await res.text());
212-
}
212+
if (!res.ok) {
213+
throw new Error(`${res.status} ${res.statusText}`);
214+
} else if (res.status >= 400) {
215+
throw new Error(await res.text());
216+
}
213217

214-
let serverPatches = (await res.json()) as AssetsManifest["routes"];
218+
serverPatches = (await res.json()) as AssetsManifest["routes"];
219+
} catch (e) {
220+
if (signal?.aborted) return;
221+
throw e;
222+
}
215223

216224
// Patch routes we don't know about yet into the manifest
217225
let knownRoutes = new Set(Object.keys(manifest.routes));

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3180,6 +3180,7 @@ export function createRouter(init: RouterInit): Router {
31803180
let localManifest = manifest;
31813181
try {
31823182
await patchRoutesOnNavigationImpl({
3183+
signal,
31833184
path: pathname,
31843185
matches: partialMatches,
31853186
patch: (routeId, children) => {

packages/react-router/lib/router/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ export type AgnosticPatchRoutesOnNavigationFunctionArgs<
224224
O extends AgnosticRouteObject = AgnosticRouteObject,
225225
M extends AgnosticRouteMatch = AgnosticRouteMatch
226226
> = {
227+
signal: AbortSignal;
227228
path: string;
228229
matches: M[];
229230
patch: (routeId: string | null, children: O[]) => void;

0 commit comments

Comments
 (0)