From f6e6ce6c64a4ea7a08f4b7969b62442f89a85e69 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Mon, 18 May 2026 18:29:55 +0100 Subject: [PATCH 1/3] 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) --- .server-changes/env-not-found-404.md | 6 ++++ .../route.tsx | 3 +- .../route.tsx | 5 ++-- .../route.tsx | 3 +- .../route.tsx | 3 +- .../route.tsx | 3 +- apps/webapp/app/utils/httpErrors.ts | 4 +++ apps/webapp/test/httpErrors.test.ts | 28 +++++++++++++++++++ 8 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .server-changes/env-not-found-404.md create mode 100644 apps/webapp/test/httpErrors.test.ts diff --git a/.server-changes/env-not-found-404.md b/.server-changes/env-not-found-404.md new file mode 100644 index 00000000000..d6fb72e222a --- /dev/null +++ b/.server-changes/env-not-found-404.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +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.). diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx index 17dcfbc4619..47318edd355 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx @@ -54,6 +54,7 @@ import { v3BatchPath, v3BatchRunsPath, } from "~/utils/pathBuilder"; +import { throwNotFound } from "~/utils/httpErrors"; export const meta: MetaFunction = () => { return [ @@ -74,7 +75,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { - throw new Error("Environment not found"); + throwNotFound("Environment not found"); } const url = new URL(request.url); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index f555f98171e..12bf83d1e4b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -59,6 +59,7 @@ import { v3TestPath, v3TestTaskPath, } from "~/utils/pathBuilder"; +import { throwNotFound } from "~/utils/httpErrors"; import { ListPagination } from "../../components/ListPagination"; import { CreateBulkActionInspector } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction"; import { Callout } from "~/components/primitives/Callout"; @@ -77,12 +78,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { - throw new Error("Project not found"); + throwNotFound("Project not found"); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { - throw new Error("Environment not found"); + throwNotFound("Environment not found"); } const filters = await getRunFiltersFromRequest(request); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx index f4e663b1b7d..a837274222b 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx @@ -55,6 +55,7 @@ import { v3SchedulePath, v3SchedulesPath, } from "~/utils/pathBuilder"; +import { throwNotFound } from "~/utils/httpErrors"; import { DeleteTaskScheduleService } from "~/v3/services/deleteTaskSchedule.server"; import { SetActiveOnTaskScheduleService } from "~/v3/services/setActiveOnTaskSchedule.server"; @@ -84,7 +85,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { }); if (!result) { - throw new Error("Schedule not found"); + throwNotFound("Schedule not found"); } return typedjson({ schedule: result.schedule }); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx index c873dd9f406..688477281d6 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions.$sessionParam/route.tsx @@ -51,6 +51,7 @@ import { v3RunsPath, v3SessionsPath, } from "~/utils/pathBuilder"; +import { throwNotFound } from "~/utils/httpErrors"; const ParamsSchema = EnvironmentParamSchema.extend({ sessionParam: z.string(), @@ -71,7 +72,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { - throw new Error("Environment not found"); + throwNotFound("Environment not found"); } const presenter = new SessionPresenter($replica); diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx index 99b0a96b5d1..8d2fa6f7961 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.sessions._index/route.tsx @@ -19,6 +19,7 @@ import { SessionListPresenter } from "~/presenters/v3/SessionListPresenter.serve import { clickhouseClient } from "~/services/clickhouseInstance.server"; import { requireUserId } from "~/services/session.server"; import { docsPath, EnvironmentParamSchema } from "~/utils/pathBuilder"; +import { throwNotFound } from "~/utils/httpErrors"; export const meta: MetaFunction = () => { return [ @@ -39,7 +40,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const environment = await findEnvironmentBySlug(project.id, envParam, userId); if (!environment) { - throw new Error("Environment not found"); + throwNotFound("Environment not found"); } const filters = getSessionFiltersFromRequest(request); diff --git a/apps/webapp/app/utils/httpErrors.ts b/apps/webapp/app/utils/httpErrors.ts index 5131730e3bb..2e41aa67eff 100644 --- a/apps/webapp/app/utils/httpErrors.ts +++ b/apps/webapp/app/utils/httpErrors.ts @@ -1,3 +1,7 @@ +export function throwNotFound(statusText: string): never { + throw new Response(undefined, { status: 404, statusText }); +} + export function friendlyErrorDisplay(statusCode: number, statusText?: string) { switch (statusCode) { case 400: diff --git a/apps/webapp/test/httpErrors.test.ts b/apps/webapp/test/httpErrors.test.ts new file mode 100644 index 00000000000..dab90bdb024 --- /dev/null +++ b/apps/webapp/test/httpErrors.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { throwNotFound } from "~/utils/httpErrors"; + +describe("throwNotFound", () => { + it("throws a Response with status 404 and the provided statusText", () => { + let thrown: unknown; + try { + throwNotFound("Environment not found"); + } catch (e) { + thrown = e; + } + + expect(thrown).toBeInstanceOf(Response); + expect((thrown as Response).status).toBe(404); + expect((thrown as Response).statusText).toBe("Environment not found"); + }); + + it("passes through whatever statusText the caller provides", () => { + let thrown: unknown; + try { + throwNotFound("Project not found"); + } catch (e) { + thrown = e; + } + + expect((thrown as Response).statusText).toBe("Project not found"); + }); +}); From bb7a57a62df5c500bee84b530d1202d340d17f7a Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Mon, 18 May 2026 19:10:39 +0100 Subject: [PATCH 2/3] fix(webapp): use redirectWithErrorMessage for project-not-found in runs loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the convention used by every other dashboard route (batches, sessions, schedules): redirect to "/" with a toast on missing project, rather than rendering a bare 404. The environment-not-found branch remains a 404 via throwNotFound, since all peer routes for that case were previously broken (bare Error → 500) and are now consistent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../route.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx index 12bf83d1e4b..d271e6f2b22 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx @@ -40,6 +40,7 @@ import { useOrganization } from "~/hooks/useOrganizations"; import { useProject } from "~/hooks/useProject"; import { useSearchParams } from "~/hooks/useSearchParam"; import { useShortcutKeys } from "~/hooks/useShortcutKeys"; +import { redirectWithErrorMessage } from "~/models/message.server"; import { findProjectBySlug } from "~/models/project.server"; import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; import { getRunFiltersFromRequest } from "~/presenters/RunFilters.server"; @@ -78,7 +79,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { - throwNotFound("Project not found"); + return redirectWithErrorMessage("/", request, "Project not found"); } const environment = await findEnvironmentBySlug(project.id, envParam, userId); From af25bdda41dd728c11ec744c92794ff3cacc606a Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Tue, 19 May 2026 11:23:02 +0100 Subject: [PATCH 3/3] chore(server-changes): tighten env-not-found-404 entry to a single line Co-Authored-By: Claude Opus 4.7 (1M context) --- .server-changes/env-not-found-404.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.server-changes/env-not-found-404.md b/.server-changes/env-not-found-404.md index d6fb72e222a..89e50726606 100644 --- a/.server-changes/env-not-found-404.md +++ b/.server-changes/env-not-found-404.md @@ -3,4 +3,4 @@ area: webapp type: fix --- -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.). +Dashboard runs, sessions, batches, and schedule-detail loaders now return 404 (or redirect to the user's home with a toast for missing projects) instead of 500 when a slug doesn't resolve.