diff --git a/.server-changes/env-not-found-404.md b/.server-changes/env-not-found-404.md new file mode 100644 index 00000000000..89e50726606 --- /dev/null +++ b/.server-changes/env-not-found-404.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +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. 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..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"; @@ -59,6 +60,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 +79,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const project = await findProjectBySlug(organizationSlug, projectParam, userId); if (!project) { - throw new Error("Project not found"); + return redirectWithErrorMessage("/", request, "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"); + }); +});