From 1abf0f827bf586a74832ed2928eeead10fa6c023 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 5 Apr 2026 03:02:32 +0200 Subject: [PATCH] fix: remove 401 hard redirect, let React Query retry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api/me call races with TokenSync — fires before the token provider is set. Removed the hard window.location redirect on 401 from the API client. React Query retries with backoff instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/api/client.ts | 9 ++------- ui/src/api/hooks.ts | 2 ++ ui/src/main.tsx | 15 +++------------ 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/ui/src/api/client.ts b/ui/src/api/client.ts index 28c96ec..e15d0b5 100644 --- a/ui/src/api/client.ts +++ b/ui/src/api/client.ts @@ -6,11 +6,6 @@ export function setTokenProvider(provider: (() => Promise) | tokenProvider = provider; } -let logoutHandler: (() => void) | null = null; - -export function setLogoutHandler(handler: (() => void) | null) { - logoutHandler = handler; -} async function apiFetch(path: string, options: RequestInit = {}): Promise { const token = tokenProvider ? await tokenProvider() : null; @@ -27,8 +22,8 @@ async function apiFetch(path: string, options: RequestInit = {}): Promise const response = await fetch(`${API_BASE}${path}`, { ...options, headers }); if (response.status === 401) { - if (logoutHandler) logoutHandler(); - window.location.href = '/login'; + // Don't hard-redirect — let React Query retry (token may not be ready yet). + // The ProtectedRoute handles unauthenticated state. throw new Error('Unauthorized'); } diff --git a/ui/src/api/hooks.ts b/ui/src/api/hooks.ts index 7b530ba..ceb7bda 100644 --- a/ui/src/api/hooks.ts +++ b/ui/src/api/hooks.ts @@ -190,6 +190,8 @@ export function useMe() { queryKey: ['me'], queryFn: () => api.get('/me'), staleTime: 60_000, + retry: 3, + retryDelay: 1000, }); } diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 2a17a35..480d4b0 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; import { LogtoProvider, UserScope, useLogto } from '@logto/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -7,7 +7,7 @@ import { ThemeProvider, ToastProvider, BreadcrumbProvider, GlobalFilterProvider, import '@cameleer/design-system/style.css'; import { AppRouter } from './router'; import { fetchConfig } from './config'; -import { setTokenProvider, setLogoutHandler } from './api/client'; +import { setTokenProvider } from './api/client'; import { useOrgStore } from './auth/useOrganization'; const queryClient = new QueryClient({ @@ -20,7 +20,7 @@ const queryClient = new QueryClient({ }); function TokenSync({ resource }: { resource: string }) { - const { getAccessToken, isAuthenticated, signOut, fetchUserInfo } = useLogto(); + const { getAccessToken, isAuthenticated, fetchUserInfo } = useLogto(); const { currentOrgId, setCurrentOrg, setOrganizations } = useOrgStore(); // After auth, resolve user's organizations from Logto @@ -59,15 +59,6 @@ function TokenSync({ resource }: { resource: string }) { } }, [isAuthenticated, getAccessToken, resource, currentOrgId]); - const handleLogout = useCallback(() => { - signOut(window.location.origin + '/login'); - }, [signOut]); - - useEffect(() => { - setLogoutHandler(handleLogout); - return () => setLogoutHandler(null); - }, [handleLogout]); - return null; }