Skip to content

Commit f6e6ce6

Browse files
d-csclaude
andcommitted
fix(webapp): return 404 instead of 500 for missing env/project/schedule loaders
Dashboard loaders for runs, sessions, batches and schedule detail threw bare `Error("X not found")` when a slug didn't resolve, which Remix surfaces as a 500 and Sentry captures via auto-instrumentation. Real users following stale preview-branch or deleted-resource links keep generating noise. Introduce a `throwNotFound(statusText)` helper that throws a Response with status 404, matching the pattern used in sibling routes (agents, alerts, bulk-actions), and migrate the affected loaders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 906d5fa commit f6e6ce6

8 files changed

Lines changed: 49 additions & 6 deletions

File tree

  • .server-changes
  • apps/webapp
    • app
      • routes
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam
        • _app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index
      • utils
    • test
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Return 404 instead of 500 when a dashboard loader is hit with a slug that no longer exists. Affected loaders (runs, sessions, batches, schedule detail) threw bare `Error("Environment not found")` / `Error("Project not found")` / `Error("Schedule not found")`, which Remix surfaces as 500 and Sentry's auto-instrumentation captures, creating ongoing noise from real users following stale preview-branch or deleted-resource links. Replaced with a `throwNotFound(statusText)` helper that throws a Response with status 404, matching the established pattern in sibling routes (agents, alerts, bulk-actions, etc.).

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {
5454
v3BatchPath,
5555
v3BatchRunsPath,
5656
} from "~/utils/pathBuilder";
57+
import { throwNotFound } from "~/utils/httpErrors";
5758

5859
export const meta: MetaFunction = () => {
5960
return [
@@ -74,7 +75,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7475

7576
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
7677
if (!environment) {
77-
throw new Error("Environment not found");
78+
throwNotFound("Environment not found");
7879
}
7980

8081
const url = new URL(request.url);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
v3TestPath,
6060
v3TestTaskPath,
6161
} from "~/utils/pathBuilder";
62+
import { throwNotFound } from "~/utils/httpErrors";
6263
import { ListPagination } from "../../components/ListPagination";
6364
import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction";
6465
import { Callout } from "~/components/primitives/Callout";
@@ -77,12 +78,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7778

7879
const project = await findProjectBySlug(organizationSlug, projectParam, userId);
7980
if (!project) {
80-
throw new Error("Project not found");
81+
throwNotFound("Project not found");
8182
}
8283

8384
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
8485
if (!environment) {
85-
throw new Error("Environment not found");
86+
throwNotFound("Environment not found");
8687
}
8788

8889
const filters = await getRunFiltersFromRequest(request);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
v3SchedulePath,
5656
v3SchedulesPath,
5757
} from "~/utils/pathBuilder";
58+
import { throwNotFound } from "~/utils/httpErrors";
5859
import { DeleteTaskScheduleService } from "~/v3/services/deleteTaskSchedule.server";
5960
import { SetActiveOnTaskScheduleService } from "~/v3/services/setActiveOnTaskSchedule.server";
6061

@@ -84,7 +85,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
8485
});
8586

8687
if (!result) {
87-
throw new Error("Schedule not found");
88+
throwNotFound("Schedule not found");
8889
}
8990

9091
return typedjson({ schedule: result.schedule });

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
v3RunsPath,
5252
v3SessionsPath,
5353
} from "~/utils/pathBuilder";
54+
import { throwNotFound } from "~/utils/httpErrors";
5455

5556
const ParamsSchema = EnvironmentParamSchema.extend({
5657
sessionParam: z.string(),
@@ -71,7 +72,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
7172

7273
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
7374
if (!environment) {
74-
throw new Error("Environment not found");
75+
throwNotFound("Environment not found");
7576
}
7677

7778
const presenter = new SessionPresenter($replica);

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { SessionListPresenter } from "~/presenters/v3/SessionListPresenter.serve
1919
import { clickhouseClient } from "~/services/clickhouseInstance.server";
2020
import { requireUserId } from "~/services/session.server";
2121
import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder";
22+
import { throwNotFound } from "~/utils/httpErrors";
2223

2324
export const meta: MetaFunction = () => {
2425
return [
@@ -39,7 +40,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
3940

4041
const environment = await findEnvironmentBySlug(project.id, envParam, userId);
4142
if (!environment) {
42-
throw new Error("Environment not found");
43+
throwNotFound("Environment not found");
4344
}
4445

4546
const filters = getSessionFiltersFromRequest(request);

apps/webapp/app/utils/httpErrors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
export function throwNotFound(statusText: string): never {
2+
throw new Response(undefined, { status: 404, statusText });
3+
}
4+
15
export function friendlyErrorDisplay(statusCode: number, statusText?: string) {
26
switch (statusCode) {
37
case 400:
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from "vitest";
2+
import { throwNotFound } from "~/utils/httpErrors";
3+
4+
describe("throwNotFound", () => {
5+
it("throws a Response with status 404 and the provided statusText", () => {
6+
let thrown: unknown;
7+
try {
8+
throwNotFound("Environment not found");
9+
} catch (e) {
10+
thrown = e;
11+
}
12+
13+
expect(thrown).toBeInstanceOf(Response);
14+
expect((thrown as Response).status).toBe(404);
15+
expect((thrown as Response).statusText).toBe("Environment not found");
16+
});
17+
18+
it("passes through whatever statusText the caller provides", () => {
19+
let thrown: unknown;
20+
try {
21+
throwNotFound("Project not found");
22+
} catch (e) {
23+
thrown = e;
24+
}
25+
26+
expect((thrown as Response).statusText).toBe("Project not found");
27+
});
28+
});

0 commit comments

Comments
 (0)