fix(frontend): show a banner when LoginGate can't reach the backend
LoginGate's initial session check swallowed every failure into the same "unauthenticated" branch, so a genuinely unreachable backend looked identical to a normal logged-out state -- the one screen in the app where "backend not here" showed no feedback at all, since every banner-migrated component only renders after authentication. api/client.ts's request() now throws a dedicated NetworkError (a distinct type, not just a distinguishable message) for a fetch() failure specifically, so LoginGate.tsx's catch can tell that apart from a real 401 via instanceof and show a banner before falling through to "unauthenticated" either way. Verified with a real headless-browser run against the dev server with no backend: the banner renders correctly and coexists with the existing ?auth_error= banner without conflict. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, BASE_URL } from "./api/client";
|
||||
import { api, BASE_URL, NetworkError } from "./api/client";
|
||||
import { showError } from "./banner";
|
||||
import "./LoginGate.css";
|
||||
import App from "./App";
|
||||
@@ -34,7 +34,17 @@ export function LoginGate() {
|
||||
setSession(s);
|
||||
setStatus("authenticated");
|
||||
})
|
||||
.catch(() => setStatus("unauthenticated"));
|
||||
.catch((e) => {
|
||||
// A NetworkError here means the backend itself isn't reachable, not
|
||||
// that this browser is genuinely logged out -- worth a banner, since
|
||||
// otherwise this is the one place in the app where "backend not
|
||||
// here" would show no feedback at all. Either way the screen still
|
||||
// falls through to "unauthenticated" (showing the Log in button),
|
||||
// since there's no session to trust regardless of why the check
|
||||
// failed.
|
||||
if (e instanceof NetworkError) showError(e.message);
|
||||
setStatus("unauthenticated");
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Keycloak redirects back here with ?auth_error=<code> when the
|
||||
|
||||
@@ -15,6 +15,15 @@ import type {
|
||||
|
||||
export const BASE_URL = import.meta.env.VITE_API_BASE_URL ?? "http://localhost:8080";
|
||||
|
||||
// Thrown when fetch() itself rejects (offline, connection refused, CORS --
|
||||
// the backend genuinely isn't reachable), as opposed to a normal non-2xx
|
||||
// HTTP response. A distinct type, not just a distinguishable message
|
||||
// string, so a caller that needs to tell the two apart (LoginGate.tsx's
|
||||
// initial session check, which must not treat "backend unreachable" the
|
||||
// same as "genuinely not logged in") can check `instanceof` rather than
|
||||
// matching on text that's free to change.
|
||||
export class NetworkError extends Error {}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
let res: Response;
|
||||
try {
|
||||
@@ -24,13 +33,11 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
...init,
|
||||
});
|
||||
} catch {
|
||||
// fetch() itself rejecting (not a non-2xx response, which is handled
|
||||
// below) means the backend genuinely isn't reachable -- offline,
|
||||
// connection refused, CORS, etc. The raw browser error text here
|
||||
// (e.g. "TypeError: Failed to fetch") is not something to show a user;
|
||||
// every caller's catch-and-showError already displays whatever this
|
||||
// throws, so the friendly text only needs to be written once, here.
|
||||
throw new Error("Can't reach the server — check your connection.");
|
||||
// The raw browser error text here (e.g. "TypeError: Failed to fetch")
|
||||
// is not something to show a user; every caller's catch-and-showError
|
||||
// already displays whatever this throws, so the friendly text only
|
||||
// needs to be written once, here.
|
||||
throw new NetworkError("Can't reach the server — check your connection.");
|
||||
}
|
||||
if (res.status === 401 && path !== "/api/session/me") {
|
||||
// An established session expired mid-use (not the initial "am I logged
|
||||
|
||||
Reference in New Issue
Block a user