From 55cbca102422adcf8dd1442f31d3d38ad11ea801 Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:27:30 +0800 Subject: [PATCH 1/3] fix(frontend): surface auth config fetch failures instead of treating them as auth-disabled fetchAuthConfig collapsed both non-2xx responses and network errors into an empty AuthConfig, which AuthProvider interpreted as "authentication disabled". When /api/auth/config failed transiently while protected APIs returned 401, the app rendered the normal shell with raw "Missing or invalid Authorization header" text and no login control or error surface (#2441). The two failure paths now throw so the existing AuthProvider catch renders its Authentication Error page. A 200 response with an empty config still means auth is disabled (local dev), unchanged. Fixes #2441 Signed-off-by: fei <204683769+feiiiiii5@users.noreply.github.com> --- frontend/src/auth/msalConfig.test.ts | 16 +++++++++------- frontend/src/auth/msalConfig.ts | 22 +++++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/frontend/src/auth/msalConfig.test.ts b/frontend/src/auth/msalConfig.test.ts index bda21bf5fa..2ed9472849 100644 --- a/frontend/src/auth/msalConfig.test.ts +++ b/frontend/src/auth/msalConfig.test.ts @@ -73,22 +73,24 @@ describe("msalConfig", () => { expect(global.fetch).toHaveBeenCalledWith("/api/auth/config"); }); - it("returns empty config when response is not ok", async () => { - (global.fetch as jest.Mock).mockResolvedValue({ ok: false }); + it("throws when response is not ok (transient failure is not auth-disabled)", async () => { + (global.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 503, + statusText: "Service Unavailable", + }); const { fetchAuthConfig } = await import("./msalConfig"); - const result = await fetchAuthConfig(); - expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" }); + await expect(fetchAuthConfig()).rejects.toThrow("/api/auth/config returned 503 Service Unavailable"); }); - it("returns empty config on network error", async () => { + it("throws on network error (transient failure is not auth-disabled)", async () => { (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error")); const { fetchAuthConfig } = await import("./msalConfig"); - const result = await fetchAuthConfig(); - expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" }); + await expect(fetchAuthConfig()).rejects.toThrow("Failed to reach /api/auth/config"); }); }); }); diff --git a/frontend/src/auth/msalConfig.ts b/frontend/src/auth/msalConfig.ts index c63e3a68cf..8addef4f72 100644 --- a/frontend/src/auth/msalConfig.ts +++ b/frontend/src/auth/msalConfig.ts @@ -24,17 +24,21 @@ export interface AuthConfig { } export async function fetchAuthConfig(): Promise { + let response: Response try { - const response = await fetch('/api/auth/config') - if (!response.ok) { - // Auth endpoint not available — treat as auth disabled - return { clientId: '', tenantId: '', allowedGroupIds: '' } - } - return (await response.json()) as AuthConfig - } catch { - // Network error (e.g., backend not running yet) — treat as auth disabled - return { clientId: '', tenantId: '', allowedGroupIds: '' } + response = await fetch('/api/auth/config') + } catch (e) { + // A network error (e.g., backend not running yet) is a transient + // infrastructure failure, not proof that auth is disabled. Surface it so + // AuthProvider can show its error state instead of rendering the shell + // while protected APIs return 401. + throw new Error(`Failed to reach /api/auth/config: ${e instanceof Error ? e.message : String(e)}`) } + if (!response.ok) { + // HTTP-level failures on the config endpoint are equally inconclusive. + throw new Error(`/api/auth/config returned ${response.status} ${response.statusText}`) + } + return (await response.json()) as AuthConfig } export function buildMsalConfig(authConfig: AuthConfig): Configuration { From a6519ab3987c1c18911063a9c15830fad8fac8f5 Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:52:02 -0700 Subject: [PATCH 2/3] fix: preserve auth config fetch error context Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- frontend/src/auth/msalConfig.test.ts | 8 ++++++-- frontend/src/auth/msalConfig.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/frontend/src/auth/msalConfig.test.ts b/frontend/src/auth/msalConfig.test.ts index 2ed9472849..c9eb612b3b 100644 --- a/frontend/src/auth/msalConfig.test.ts +++ b/frontend/src/auth/msalConfig.test.ts @@ -86,11 +86,15 @@ describe("msalConfig", () => { }); it("throws on network error (transient failure is not auth-disabled)", async () => { - (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error")); + const networkError = new Error("Network error"); + (global.fetch as jest.Mock).mockRejectedValue(networkError); const { fetchAuthConfig } = await import("./msalConfig"); - await expect(fetchAuthConfig()).rejects.toThrow("Failed to reach /api/auth/config"); + await expect(fetchAuthConfig()).rejects.toMatchObject({ + message: "Failed to reach /api/auth/config: Network error", + cause: networkError, + }); }); }); }); diff --git a/frontend/src/auth/msalConfig.ts b/frontend/src/auth/msalConfig.ts index 8addef4f72..a99aebc3fd 100644 --- a/frontend/src/auth/msalConfig.ts +++ b/frontend/src/auth/msalConfig.ts @@ -27,12 +27,17 @@ export async function fetchAuthConfig(): Promise { let response: Response try { response = await fetch('/api/auth/config') - } catch (e) { + } catch (error) { // A network error (e.g., backend not running yet) is a transient // infrastructure failure, not proof that auth is disabled. Surface it so // AuthProvider can show its error state instead of rendering the shell // while protected APIs return 401. - throw new Error(`Failed to reach /api/auth/config: ${e instanceof Error ? e.message : String(e)}`) + const fetchError = new Error( + `Failed to reach /api/auth/config: ${error instanceof Error ? error.message : String(error)}`, + ) + // ErrorOptions requires ES2022, while this project targets ES2020. + Object.defineProperty(fetchError, 'cause', { value: error }) + throw fetchError } if (!response.ok) { // HTTP-level failures on the config endpoint are equally inconclusive. From d2cf5a02a5f0ce66d6f86f769ab7ff46ccb99f8c Mon Sep 17 00:00:00 2001 From: Copilot <223556219+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 06:28:47 -0700 Subject: [PATCH 3/3] fix(frontend): add auth failure recovery guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- frontend/src/auth/AuthProvider.test.tsx | 1 + frontend/src/auth/AuthProvider.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/frontend/src/auth/AuthProvider.test.tsx b/frontend/src/auth/AuthProvider.test.tsx index e3023b1e28..8ddae4ca70 100644 --- a/frontend/src/auth/AuthProvider.test.tsx +++ b/frontend/src/auth/AuthProvider.test.tsx @@ -161,6 +161,7 @@ describe("AuthProvider", () => { await waitFor(() => { expect(screen.getByText("Authentication Error")).toBeVisible(); expect(screen.getByText("Config fetch failed")).toBeVisible(); + expect(screen.getByText("Reload the page to try again.")).toBeVisible(); }); }); diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx index 34577c7b9b..f707ffa159 100644 --- a/frontend/src/auth/AuthProvider.tsx +++ b/frontend/src/auth/AuthProvider.tsx @@ -119,6 +119,7 @@ export function AuthProvider({ children }: AuthProviderProps) {

Authentication Error

{error}

+

Reload the page to try again.

) }