Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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/fix-route-error-display-null-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: fix
---

Prevent dashboard error pages from crashing when a route error has no data payload
6 changes: 2 additions & 4 deletions apps/webapp/app/components/ErrorDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HomeIcon } from "@heroicons/react/20/solid";
import { isRouteErrorResponse, useRouteError } from "@remix-run/react";
import { friendlyErrorDisplay } from "~/utils/httpErrors";
import { friendlyErrorDisplay, getRouteErrorMessage } from "~/utils/httpErrors";
import { permissionDeniedMessage } from "~/utils/permissionDenied";
import { LinkButton } from "./primitives/Buttons";
import { Header1 } from "./primitives/Headers";
Expand Down Expand Up @@ -39,9 +39,7 @@ export function RouteErrorDisplay(options?: ErrorDisplayOptions) {
{isRouteErrorResponse(error) ? (
<ErrorDisplay
title={friendlyErrorDisplay(error.status, error.statusText).title}
message={
error.data.message ?? friendlyErrorDisplay(error.status, error.statusText).message
}
message={getRouteErrorMessage(error.status, error.statusText, error.data)}
{...options}
/>
) : error instanceof Error ? (
Expand Down
33 changes: 33 additions & 0 deletions apps/webapp/app/utils/httpErrors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest";
import { getRouteErrorMessage } from "./httpErrors";

describe("getRouteErrorMessage", () => {
it("returns data.message when present", () => {
expect(getRouteErrorMessage(500, "Internal Server Error", { message: "boom" })).toBe("boom");
});

it("falls back when data is null", () => {
expect(getRouteErrorMessage(500, "Internal Server Error", null)).toBe(
"Something went wrong on our end. Please try again later."
);
});

it("falls back when data is undefined", () => {
expect(getRouteErrorMessage(404, "Not Found", undefined)).toBe(
"The page you're looking for doesn't exist."
);
});

it("uses a string data body", () => {
expect(getRouteErrorMessage(400, "Bad Request", "Invalid payload")).toBe("Invalid payload");
});

it("falls back when message is missing or empty", () => {
expect(getRouteErrorMessage(403, "Forbidden", {})).toBe(
"You don't have permission to access this resource."
);
expect(getRouteErrorMessage(403, "Forbidden", { message: "" })).toBe(
"You don't have permission to access this resource."
);
});
Comment on lines +9 to +32

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 New error-message tests fail because fallback returns the status text, not the generic message

The fallback message is built from the shared helper (friendlyErrorDisplay(status, statusText).message at apps/webapp/app/utils/httpErrors.ts:50), which returns the provided status text whenever one is given rather than the generic default the new tests expect, so the added test suite fails.
Impact: The project's test run breaks, blocking the change from passing CI.

Mechanism: statusText short-circuits the default message

friendlyErrorDisplay returns statusText ?? <default> for every case (apps/webapp/app/utils/httpErrors.ts:5-43). Every new test passes a truthy statusText (e.g. "Internal Server Error", "Not Found", "Forbidden"), so friendlyErrorDisplay(...).message evaluates to that status text, never the generic default.

Concretely:

  • getRouteErrorMessage(500, "Internal Server Error", null) returns "Internal Server Error", but the test at apps/webapp/app/utils/httpErrors.test.ts:10-12 expects "Something went wrong on our end. Please try again later."
  • getRouteErrorMessage(404, "Not Found", undefined) returns "Not Found", but apps/webapp/app/utils/httpErrors.test.ts:16-18 expects "The page you're looking for doesn't exist."
  • getRouteErrorMessage(403, "Forbidden", {}) and (403, "Forbidden", { message: "" }) return "Forbidden", but apps/webapp/app/utils/httpErrors.test.ts:26-31 expects "You don't have permission to access this resource."

Either the implementation must resolve the generic default when data is missing (e.g. call friendlyErrorDisplay(status) without statusText), or the test expectations must be corrected to the status-text values.

Prompt for agents
The new tests in apps/webapp/app/utils/httpErrors.test.ts will fail because getRouteErrorMessage computes its fallback as friendlyErrorDisplay(status, statusText).message, and friendlyErrorDisplay returns `statusText ?? <default>` — so whenever a truthy statusText is supplied (as every test does), the fallback equals the status text, not the generic default the assertions expect. Decide the intended behavior: (1) if callers should see the friendly generic message when error.data is missing, change getRouteErrorMessage's fallback to friendlyErrorDisplay(status).message (omit statusText) so the default is used, and note this also changes RouteErrorDisplay's runtime output; or (2) if showing the status text is intended (matching the prior behavior of error.data.message ?? friendlyErrorDisplay(...).message), update the test expectations in httpErrors.test.ts to expect the passed statusText strings (e.g. 'Internal Server Error', 'Not Found', 'Forbidden'). Ensure the tests and implementation agree.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

});
21 changes: 21 additions & 0 deletions apps/webapp/app/utils/httpErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,24 @@ export function friendlyErrorDisplay(statusCode: number, statusText?: string) {
};
}
}

/**
* Safely extract a user-facing message from a Remix route error response.
* `error.data` can be null, a string, or an object — never assume `.message`.
*/
export function getRouteErrorMessage(status: number, statusText: string, data: unknown): string {
const fallback = friendlyErrorDisplay(status, statusText).message;

if (typeof data === "string" && data.length > 0) {
return data;
}

if (data && typeof data === "object" && "message" in data) {
const message = (data as { message: unknown }).message;
if (typeof message === "string" && message.length > 0) {
return message;
}
}

return fallback;
}