From 3e48edd53fcc0bdc75bf5dc6f6e9426b668d758c Mon Sep 17 00:00:00 2001 From: Ayush7614 Date: Thu, 30 Jul 2026 04:32:23 +0530 Subject: [PATCH] fix(webapp): harden RouteErrorDisplay against null error.data Remix route errors can have null/empty data; reading .message threw inside the error boundary and blanked the page. Fall back safely. --- .../fix-route-error-display-null-data.md | 6 ++++ apps/webapp/app/components/ErrorDisplay.tsx | 6 ++-- apps/webapp/app/utils/httpErrors.test.ts | 33 +++++++++++++++++++ apps/webapp/app/utils/httpErrors.ts | 21 ++++++++++++ 4 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 .server-changes/fix-route-error-display-null-data.md create mode 100644 apps/webapp/app/utils/httpErrors.test.ts diff --git a/.server-changes/fix-route-error-display-null-data.md b/.server-changes/fix-route-error-display-null-data.md new file mode 100644 index 00000000000..1e10e44c24b --- /dev/null +++ b/.server-changes/fix-route-error-display-null-data.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +Prevent dashboard error pages from crashing when a route error has no data payload diff --git a/apps/webapp/app/components/ErrorDisplay.tsx b/apps/webapp/app/components/ErrorDisplay.tsx index 374f427c504..7d07a02b56e 100644 --- a/apps/webapp/app/components/ErrorDisplay.tsx +++ b/apps/webapp/app/components/ErrorDisplay.tsx @@ -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"; @@ -39,9 +39,7 @@ export function RouteErrorDisplay(options?: ErrorDisplayOptions) { {isRouteErrorResponse(error) ? ( ) : error instanceof Error ? ( diff --git a/apps/webapp/app/utils/httpErrors.test.ts b/apps/webapp/app/utils/httpErrors.test.ts new file mode 100644 index 00000000000..2d70c9584cc --- /dev/null +++ b/apps/webapp/app/utils/httpErrors.test.ts @@ -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." + ); + }); +}); diff --git a/apps/webapp/app/utils/httpErrors.ts b/apps/webapp/app/utils/httpErrors.ts index 2e41aa67eff..f3609f613c2 100644 --- a/apps/webapp/app/utils/httpErrors.ts +++ b/apps/webapp/app/utils/httpErrors.ts @@ -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; +}