diff --git a/app/root.tsx b/app/root.tsx index b495cfe1..e1110c1c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -22,7 +22,7 @@ export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }]; export const loader = ({ context, request }: LoaderFunctionArgs) => { const url = new URL(request.url); return json({ - version: context.cloudflare.env.CF_PAGES_COMMIT_SHA, + version: context.cloudflare?.env?.CF_PAGES_COMMIT_SHA, origin: url.origin, url: request.url, }); diff --git a/app/routes/__tests__/dashboard.test.tsx b/app/routes/__tests__/dashboard.test.tsx index 8fdab73b..4ba95e89 100644 --- a/app/routes/__tests__/dashboard.test.tsx +++ b/app/routes/__tests__/dashboard.test.tsx @@ -1,5 +1,5 @@ // @vitest-environment jsdom -import { json } from "@remix-run/node"; +import { json, LoaderFunctionArgs } from "@remix-run/node"; import { vi, test, @@ -36,29 +36,57 @@ describe("Dashboard route", () => { }); describe("loader", () => { - test("throws an exception if no Cloudflare credentials are provided", async () => { - // empty strings - await expect( - loader({ - context: { - analyticsEngine: new AnalyticsEngineAPI( - "testAccountId", - "testApiToken", - ), - cloudflare: { - // @ts-expect-error we don't need to provide all the properties of the cloudflare object - env: { - CF_BEARER_TOKEN: "", - CF_ACCOUNT_ID: "", - }, + test("throws a 501 Response if no Cloudflare credentials are provided", async () => { + const mockLoaderParams: LoaderFunctionArgs = { + context: { + analyticsEngine: new AnalyticsEngineAPI( + "testAccountId", + "testApiToken", + ), + cloudflare: { + // @ts-expect-error we don't need to provide all the properties of the cloudflare object + env: { + CF_ACCOUNT_ID: "", + CF_BEARER_TOKEN: "", }, }, - // @ts-expect-error we don't need to provide all the properties of the request object - request: { - url: "http://localhost:3000/dashboard", - }, - }), - ).rejects.toThrow("Missing Cloudflare credentials"); + }, + // @ts-expect-error we don't need to provide all the properties of the request object + request: { + url: "http://localhost:3000/dashboard", + }, + }; + + try { + await loader(mockLoaderParams); + } catch (error) { + expect(error).toBeInstanceOf(Response); + const response = error as Response; + expect(await response.text()).toBe( + "Missing credentials: CF_ACCOUNT_ID is not set.", + ); + expect(response.status).toBe(501); + } + + // run it again, this time with account ID present, but bearer token absent + mockLoaderParams.context.cloudflare = { + // @ts-expect-error we don't need to provide all the properties of the cloudflare object + env: { + CF_ACCOUNT_ID: "testAccountId", + CF_BEARER_TOKEN: "", + }, + }; + + try { + await loader(mockLoaderParams); + } catch (error) { + expect(error).toBeInstanceOf(Response); + const response = error as Response; + expect(await response.text()).toBe( + "Missing credentials: CF_BEARER_TOKEN is not set.", + ); + expect(response.status).toBe(501); + } }); test("redirects to ?site=siteId if no siteId is provided via query string", async () => { diff --git a/app/routes/dashboard.tsx b/app/routes/dashboard.tsx index b5661823..c3f21eae 100644 --- a/app/routes/dashboard.tsx +++ b/app/routes/dashboard.tsx @@ -9,8 +9,10 @@ import { import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/cloudflare"; import { json, redirect } from "@remix-run/cloudflare"; import { + isRouteErrorResponse, useLoaderData, useNavigation, + useRouteError, useSearchParams, } from "@remix-run/react"; @@ -41,13 +43,16 @@ const MAX_RETENTION_DAYS = 90; export const loader = async ({ context, request }: LoaderFunctionArgs) => { // NOTE: probably duped from getLoadContext / need to de-duplicate - if ( - !context.cloudflare.env.CF_BEARER_TOKEN || - !context.cloudflare.env.CF_ACCOUNT_ID - ) { - throw new Error("Missing Cloudflare credentials"); + if (!context.cloudflare?.env?.CF_ACCOUNT_ID) { + throw new Response("Missing credentials: CF_ACCOUNT_ID is not set.", { + status: 501, + }); + } + if (!context.cloudflare?.env?.CF_BEARER_TOKEN) { + throw new Response("Missing credentials: CF_BEARER_TOKEN is not set.", { + status: 501, + }); } - const { analyticsEngine } = context; const url = new URL(request.url); @@ -273,3 +278,21 @@ export default function Dashboard() { ); } + +export function ErrorBoundary() { + const error = useRouteError(); + + const errorTitle = isRouteErrorResponse(error) ? error.status : "Error"; + const errorBody = isRouteErrorResponse(error) + ? error.data + : error instanceof Error + ? error.message + : "Unknown error"; + + return ( +
{errorBody}
+