Skip to content

Commit 80b9399

Browse files
authored
feat: return 404 status for statically-determined not-found routes (#3798) (#3801)
1 parent ab9a9c2 commit 80b9399

File tree

5 files changed

+131
-4
lines changed

5 files changed

+131
-4
lines changed

client/src/not-found/NotFound.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,20 @@ import { isRenkuLegacy } from "../utils/helpers/HelperFunctionsV2";
3434
import "./NotFound.css";
3535

3636
interface NotFoundProps {
37-
title?: string;
38-
description?: string | ReactNode;
3937
children?: ReactNode;
38+
description?: string | ReactNode;
39+
forceV2?: boolean;
40+
title?: string;
4041
}
4142

4243
export default function NotFound({
4344
title: title_,
4445
description: description_,
4546
children,
47+
forceV2,
4648
}: NotFoundProps) {
4749
const location = useLocation();
48-
const isV2 = !isRenkuLegacy(location.pathname);
50+
const isV2 = forceV2 || !isRenkuLegacy(location.pathname);
4951
const title = title_ ?? "Page not found";
5052
const description =
5153
description_ ??

client/src/root.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ import {
2323
Outlet,
2424
Scripts,
2525
ScrollRestoration,
26+
isRouteErrorResponse,
2627
type MetaDescriptor,
2728
type MetaFunction,
2829
} from "react-router";
2930

31+
import v2Styles from "~/styles/renku_bootstrap.scss?url";
32+
import NotFound from "./not-found/NotFound";
33+
34+
import type { Route } from "./+types/root";
35+
3036
export const DEFAULT_META_TITLE: string =
3137
"Reproducible Data Science | Open Research | Renku";
3238

@@ -52,6 +58,40 @@ export const DEFAULT_META: MetaDescriptor[] = [
5258
},
5359
];
5460

61+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
62+
if (isRouteErrorResponse(error)) {
63+
return (
64+
<html lang="en">
65+
<head>
66+
<link rel="stylesheet" type="text/css" href={v2Styles} />
67+
<title>Page Not Found | Renku</title>
68+
<Links />
69+
</head>
70+
<body>
71+
<NotFound forceV2={true} />
72+
</body>
73+
</html>
74+
);
75+
} else if (error instanceof Error) {
76+
return (
77+
<html lang="en">
78+
<head>
79+
<link rel="stylesheet" type="text/css" href={v2Styles} />
80+
<title>Error | Renku</title>
81+
<Links />
82+
</head>
83+
<body>
84+
<div>
85+
<h1>Error</h1>
86+
<p>{error.message}</p>
87+
</div>
88+
</body>
89+
</html>
90+
);
91+
}
92+
return <h1>Unknown Error</h1>;
93+
}
94+
5595
export const meta: MetaFunction = () => {
5696
return DEFAULT_META;
5797
};

client/src/routes/catchall.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
import AppRoot from "~/index";
22

3+
import { data, type LoaderFunctionArgs } from "react-router";
4+
5+
import { ABSOLUTE_ROUTES } from "~/routing/routes.constants";
6+
7+
type RouteGroup = Record<string, string> | Record<string, unknown>;
8+
type Route = string | RouteGroup;
9+
10+
function routeGroupToPaths(routeGroup: RouteGroup): string[] {
11+
return Object.entries(routeGroup).flatMap(([, route]) =>
12+
routeToPaths(route as Route)
13+
);
14+
}
15+
16+
function routeToPaths(route: Route) {
17+
if (typeof route === "string") {
18+
return route == "/" ? [] : [route];
19+
}
20+
return routeGroupToPaths(route as RouteGroup);
21+
}
22+
23+
function routeToStaticPart(route: string) {
24+
const parts = route.split("/");
25+
const staticParts = [];
26+
for (const part of parts) {
27+
if (part.startsWith(":") || part.startsWith("*")) break;
28+
staticParts.push(part);
29+
}
30+
return staticParts.join("/");
31+
}
32+
33+
const KNOWN_ROUTES_SET = new Set(
34+
routeGroupToPaths(ABSOLUTE_ROUTES).map((route) => routeToStaticPart(route))
35+
);
36+
const KNOWN_ROUTES = [...Array.from(KNOWN_ROUTES_SET), "/v2", "/admin"];
37+
38+
export async function loader({ request }: LoaderFunctionArgs) {
39+
const url = new URL(request.url);
40+
const path = url.pathname;
41+
const isKnownRoute =
42+
path == "/" || KNOWN_ROUTES.some((route) => path.startsWith(route));
43+
if (!isKnownRoute) {
44+
throw data("Not Found", { status: 404 });
45+
}
46+
return data({});
47+
}
48+
349
export default function Component() {
450
return <AppRoot />;
551
}

tests/cypress/e2e/home.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("display the home page", () => {
3535
describe("404 page", () => {
3636
beforeEach(() => {
3737
fixtures.config().versions().userNone();
38-
cy.visit("/xzy");
38+
cy.visit("/xzy", { failOnStatusCode: false });
3939
});
4040

4141
it("show error page", () => {

tests/cypress/e2e/routing.spec.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*!
2+
* Copyright 2025 - Swiss Data Science Center (SDSC)
3+
* A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and
4+
* Eidgenössische Technische Hochschule Zürich (ETHZ).
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import fixtures from "../support/renkulab-fixtures";
20+
21+
describe("unknown routes", () => {
22+
beforeEach(() => {
23+
fixtures.config().versions();
24+
});
25+
26+
it("displays a 404 on an unknown url", () => {
27+
fixtures.userTest().dataServicesUser({
28+
response: {
29+
id: "user2-uuid",
30+
username: "user2",
31+
},
32+
});
33+
cy.intercept("GET", "/test/does-not-exist").as("visit");
34+
cy.visit("/test/does-not-exist", { failOnStatusCode: false });
35+
cy.wait("@visit").its("response.statusCode").should("eq", 404);
36+
37+
cy.contains("Page not found").should("exist");
38+
});
39+
});

0 commit comments

Comments
 (0)