fix(#338): Log auth errors and distinguish backend down from logged out

- Add error logging for auth check failures in development mode
- Distinguish network/backend errors from normal unauthenticated state
- Expose authError state to UI (network | backend | null)
- Add comprehensive tests for error handling scenarios

Refs #338

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jason Woltje
2026-02-05 17:23:07 -06:00
parent 587272e2d0
commit 63a622cbef
3 changed files with 248 additions and 2 deletions

View File

@@ -4,25 +4,92 @@ import { createContext, useContext, useState, useEffect, useCallback, type React
import type { AuthUser, AuthSession } from "@mosaic/shared";
import { apiGet, apiPost } from "../api/client";
/**
* Error types for auth session checks
*/
export type AuthErrorType = "network" | "backend" | null;
interface AuthContextValue {
user: AuthUser | null;
isLoading: boolean;
isAuthenticated: boolean;
authError: AuthErrorType;
signOut: () => Promise<void>;
refreshSession: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
/**
* Check if an error indicates a network/backend issue vs normal "not authenticated"
*/
function isBackendError(error: unknown): { isBackendDown: boolean; errorType: AuthErrorType } {
// Network errors (fetch failed, DNS, connection refused, etc.)
if (error instanceof TypeError && error.message.includes("fetch")) {
return { isBackendDown: true, errorType: "network" };
}
// Check for specific error messages that indicate backend issues
if (error instanceof Error) {
const message = error.message.toLowerCase();
// Network-level errors
if (
message.includes("network") ||
message.includes("failed to fetch") ||
message.includes("connection refused") ||
message.includes("econnrefused") ||
message.includes("timeout")
) {
return { isBackendDown: true, errorType: "network" };
}
// Backend errors (5xx status codes typically result in these messages)
if (
message.includes("internal server error") ||
message.includes("service unavailable") ||
message.includes("bad gateway") ||
message.includes("gateway timeout")
) {
return { isBackendDown: true, errorType: "backend" };
}
}
// Normal auth errors (401, 403, etc.) - user is just not logged in
return { isBackendDown: false, errorType: null };
}
/**
* Log auth errors in development mode
*/
function logAuthError(message: string, error: unknown): void {
if (process.env.NODE_ENV === "development") {
console.error(`[Auth] ${message}:`, error);
}
}
export function AuthProvider({ children }: { children: ReactNode }): React.JSX.Element {
const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [authError, setAuthError] = useState<AuthErrorType>(null);
const checkSession = useCallback(async () => {
try {
const session = await apiGet<AuthSession>("/auth/session");
setUser(session.user);
} catch {
setAuthError(null);
} catch (error) {
const { isBackendDown, errorType } = isBackendError(error);
if (isBackendDown) {
// Backend/network issue - log and expose error to UI
logAuthError("Session check failed due to backend/network issue", error);
setAuthError(errorType);
} else {
// Normal "not authenticated" state - no logging needed
setAuthError(null);
}
setUser(null);
} finally {
setIsLoading(false);
@@ -51,6 +118,7 @@ export function AuthProvider({ children }: { children: ReactNode }): React.JSX.E
user,
isLoading,
isAuthenticated: user !== null,
authError,
signOut,
refreshSession,
};