Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/env-not-found-404.md
Original file line number Diff line number Diff line change
@@ -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.).
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
v3BatchPath,
v3BatchRunsPath,
} from "~/utils/pathBuilder";
import { throwNotFound } from "~/utils/httpErrors";

export const meta: MetaFunction = () => {
return [
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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");
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
}

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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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 });
Expand Down
Comment thread
d-cs marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
v3RunsPath,
v3SessionsPath,
} from "~/utils/pathBuilder";
import { throwNotFound } from "~/utils/httpErrors";

const ParamsSchema = EnvironmentParamSchema.extend({
sessionParam: z.string(),
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 [
Expand All @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions apps/webapp/app/utils/httpErrors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export function throwNotFound(statusText: string): never {
throw new Response(undefined, { status: 404, statusText });
}
Comment thread
d-cs marked this conversation as resolved.

export function friendlyErrorDisplay(statusCode: number, statusText?: string) {
switch (statusCode) {
case 400:
Expand Down
28 changes: 28 additions & 0 deletions apps/webapp/test/httpErrors.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
Loading