From cfd000531954dc7afc0561909637acc0aa7cb028 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Wed, 15 Apr 2026 07:54:11 -0400 Subject: [PATCH] fix(web): handle 404 and 501 gracefully in API loaders Demo mode returns 404 for unimplemented endpoints instead of 501. Rename isNotImplemented to isNotAvailable covering both status codes, and use apiJsonOrNull in workflow-runs loader. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/fabro-web/app/api.test.ts | 16 ++++++++-------- apps/fabro-web/app/api.ts | 6 +++--- apps/fabro-web/app/routes/workflow-runs.tsx | 5 +++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/fabro-web/app/api.test.ts b/apps/fabro-web/app/api.test.ts index ef9a618e7..2e9dacb56 100644 --- a/apps/fabro-web/app/api.test.ts +++ b/apps/fabro-web/app/api.test.ts @@ -1,21 +1,21 @@ import { afterEach, describe, expect, mock, test } from "bun:test"; -import { getAuthConfig, isNotImplemented, loginDevToken } from "./api"; +import { getAuthConfig, isNotAvailable, loginDevToken } from "./api"; afterEach(() => { mock.restore(); }); -describe("isNotImplemented", () => { +describe("isNotAvailable", () => { test("returns true for 501 status", () => { - expect(isNotImplemented(501)).toBe(true); + expect(isNotAvailable(501)).toBe(true); + }); + + test("returns true for 404 status", () => { + expect(isNotAvailable(404)).toBe(true); }); test("returns false for 200 status", () => { - expect(isNotImplemented(200)).toBe(false); - }); - - test("returns false for 404 status", () => { - expect(isNotImplemented(404)).toBe(false); + expect(isNotAvailable(200)).toBe(false); }); }); diff --git a/apps/fabro-web/app/api.ts b/apps/fabro-web/app/api.ts index 4e9cbb9d0..a66fac875 100644 --- a/apps/fabro-web/app/api.ts +++ b/apps/fabro-web/app/api.ts @@ -27,8 +27,8 @@ export async function apiJson(path: string, options?: ApiOptions): Promise return response.json() as Promise; } -export function isNotImplemented(status: number): boolean { - return status === 501; +export function isNotAvailable(status: number): boolean { + return status === 404 || status === 501; } export async function apiJsonOrNull( @@ -36,7 +36,7 @@ export async function apiJsonOrNull( options?: ApiOptions, ): Promise { const response = await apiFetch(path, options); - if (isNotImplemented(response.status)) { + if (isNotAvailable(response.status)) { return null; } if (!response.ok) { diff --git a/apps/fabro-web/app/routes/workflow-runs.tsx b/apps/fabro-web/app/routes/workflow-runs.tsx index ab55a55b0..4af7240b6 100644 --- a/apps/fabro-web/app/routes/workflow-runs.tsx +++ b/apps/fabro-web/app/routes/workflow-runs.tsx @@ -3,11 +3,12 @@ import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outlin import { Link, useParams } from "react-router"; import { ciConfig, columnNames, deriveCiStatus, mapRunListItem, statusColors } from "../data/runs"; import type { ColumnStatus, RunWithStatus } from "../data/runs"; -import { apiJson } from "../api"; +import { apiJsonOrNull } from "../api"; import type { PaginatedRunList } from "@qltysh/fabro-api-client"; export async function loader({ request, params }: any) { - const { data: apiRuns } = await apiJson(`/workflows/${params.name}/runs`, { request }); + const result = await apiJsonOrNull(`/workflows/${params.name}/runs`, { request }); + const apiRuns = result?.data ?? []; const runs: RunWithStatus[] = apiRuns.map((r) => ({ ...mapRunListItem(r), status: r.status as ColumnStatus,