From 7b368916d5e613880e7b96caef8e3a7b40d94621 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 1 Apr 2026 17:20:39 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=91=20fix:=20Auth-Aware=20Startup=20Co?= =?UTF-8?q?nfig=20Caching=20for=20Fresh=20Sessions=20(#12505)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: auth-aware config caching for fresh sessions - Add auth state to startup config query key via shared `startupConfigKey` builder so login (unauthenticated) and chat (authenticated) configs are cached independently - Disable queries during login onMutate to prevent premature unauthenticated refetches after cache clear - Re-enable queries in setUserContext only after setTokenHeader runs, with positive-only guard to avoid redundant disable on logout - Update all getQueryData call sites to use the shared key builder - Fall back to getConfigDefaults().interface in useEndpoints, hoisted to module-level constant to avoid per-render recomputation * fix: address review findings for auth-aware config caching - Move defaultInterface const after all imports in ModelSelector.tsx - Remove dead QueryKeys import, use import type for TStartupConfig in ImportConversations.tsx - Spread real exports in useQueryParams.spec.ts mock to preserve startupConfigKey, fixing TypeError in all 6 tests * chore: import order * fix: re-enable queries on login failure When login fails, onSuccess never fires so queriesEnabled stays false. Re-enable in onError so the login page can re-fetch config (needed for LDAP username validation and social login options). --- .../Chat/Menus/Endpoints/ModelSelector.tsx | 4 ++- .../SettingsTabs/Data/ImportConversations.tsx | 6 ++-- client/src/data-provider/Auth/mutations.ts | 7 ++++- client/src/data-provider/Endpoints/queries.ts | 11 ++++++- client/src/hooks/AuthContext.tsx | 8 +++-- client/src/hooks/Chat/useChatFunctions.ts | 3 +- .../Conversations/useNavigateToConvo.tsx | 3 +- client/src/hooks/Endpoint/useEndpoints.ts | 5 +++- client/src/hooks/Input/useQueryParams.spec.ts | 30 +++++++++++-------- client/src/hooks/Input/useQueryParams.ts | 6 ++-- client/src/hooks/SSE/useEventHandlers.ts | 6 ++-- 11 files changed, 59 insertions(+), 30 deletions(-) diff --git a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx index 2c90f57598..b59b718743 100644 --- a/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx +++ b/client/src/components/Chat/Menus/Endpoints/ModelSelector.tsx @@ -15,6 +15,8 @@ import { CustomMenu as Menu } from './CustomMenu'; import DialogManager from './DialogManager'; import { useLocalize } from '~/hooks'; +const defaultInterface = getConfigDefaults().interface; + function ModelSelectorContent() { const localize = useLocalize(); @@ -122,7 +124,7 @@ function ModelSelectorContent() { } export default function ModelSelector({ startupConfig }: ModelSelectorProps) { - const interfaceConfig = startupConfig?.interface ?? getConfigDefaults().interface; + const interfaceConfig = startupConfig?.interface ?? defaultInterface; const modelSpecs = startupConfig?.modelSpecs?.list ?? []; // Hide the selector when modelSelect is false and there are no model specs to show diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index 3f0b288c93..73ab0345db 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -1,9 +1,9 @@ import { useState, useRef, useCallback } from 'react'; import { Import } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; -import { QueryKeys, TStartupConfig } from 'librechat-data-provider'; +import type { TStartupConfig } from 'librechat-data-provider'; import { Spinner, useToastContext, Label, Button } from '@librechat/client'; -import { useUploadConversationsMutation } from '~/data-provider'; +import { startupConfigKey, useUploadConversationsMutation } from '~/data-provider'; import { NotificationSeverity } from '~/common'; import { useLocalize } from '~/hooks'; import { cn, logger } from '~/utils'; @@ -51,7 +51,7 @@ function ImportConversations() { const handleFileUpload = useCallback( async (file: File) => { try { - const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); + const startupConfig = queryClient.getQueryData(startupConfigKey(true)); const maxFileSize = startupConfig?.conversationImportMaxFileSize; if (maxFileSize && file.size > maxFileSize) { const size = (maxFileSize / (1024 * 1024)).toFixed(2); diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts index 9930e42b4f..4ba79b94cb 100644 --- a/client/src/data-provider/Auth/mutations.ts +++ b/client/src/data-provider/Auth/mutations.ts @@ -40,15 +40,20 @@ export const useLoginUserMutation = ( mutationFn: (payload: t.TLoginUser) => dataService.login(payload), ...(options || {}), onMutate: (vars) => { + setQueriesEnabled(false); resetDefaultPreset(); clearStates(); queryClient.removeQueries(); options?.onMutate?.(vars); }, + // Queries re-enabled in setUserContext (AuthContext) after setTokenHeader runs onSuccess: (...args) => { - setQueriesEnabled(true); options?.onSuccess?.(...args); }, + onError: (...args) => { + setQueriesEnabled(true); + options?.onError?.(...args); + }, }); }; diff --git a/client/src/data-provider/Endpoints/queries.ts b/client/src/data-provider/Endpoints/queries.ts index 3f9c468fcf..3ce3d35110 100644 --- a/client/src/data-provider/Endpoints/queries.ts +++ b/client/src/data-provider/Endpoints/queries.ts @@ -23,12 +23,21 @@ export const useGetEndpointsQuery = ( ); }; +/** + * Auth-aware query key so unauthenticated (login page) and authenticated + * (chat page) configs are cached independently, preventing stale + * unauthenticated config from persisting after login. + */ +export const startupConfigKey = (isAuthenticated: boolean) => + [QueryKeys.startupConfig, isAuthenticated] as const; + export const useGetStartupConfig = ( config?: UseQueryOptions, ): QueryObserverResult => { const queriesEnabled = useRecoilValue(store.queriesEnabled); + const user = useRecoilValue(store.user); return useQuery( - [QueryKeys.startupConfig], + startupConfigKey(!!user), () => dataService.getStartupConfig(), { staleTime: Infinity, diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index c55980c0d2..2fb7dd7760 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -8,7 +8,7 @@ import { createContext, } from 'react'; import { debounce } from 'lodash'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { apiBaseUrl, @@ -45,6 +45,7 @@ const AuthContextProvider = ({ const [token, setToken] = useState(undefined); const [error, setError] = useState(undefined); const [isAuthenticated, setIsAuthenticated] = useState(false); + const setQueriesEnabled = useSetRecoilState(store.queriesEnabled); const { data: userRole = null } = useGetRole(SystemRoles.USER, { enabled: !!(isAuthenticated && (user?.role ?? '')), @@ -63,6 +64,9 @@ const AuthContextProvider = ({ setToken(token); setTokenHeader(token); setIsAuthenticated(isAuthenticated); + if (isAuthenticated) { + setQueriesEnabled(true); + } const searchParams = new URLSearchParams(window.location.search); const postLoginRedirect = getPostLoginRedirect(searchParams); @@ -81,7 +85,7 @@ const AuthContextProvider = ({ navigate(finalRedirect, { replace: true }); }, 50), - [navigate, setUser], + [navigate, setUser, setQueriesEnabled], ); const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) }); diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 7cf8c6bf25..18aaf0daee 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -30,6 +30,7 @@ import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete'; import useGetSender from '~/hooks/Conversations/useGetSender'; import { logger, createDualMessageContent } from '~/utils'; import store, { useGetEphemeralAgent } from '~/store'; +import { startupConfigKey } from '~/data-provider'; import useUserKey from '~/hooks/Input/useUserKey'; import { useAuthContext } from '~/hooks'; @@ -172,7 +173,7 @@ export default function useChatFunctions({ } const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); - const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); + const startupConfig = queryClient.getQueryData(startupConfigKey(true)); const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); const iconURL = conversation?.iconURL; const defaultParamsEndpoint = getDefaultParamsEndpoint(endpointsConfig, endpoint); diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index b9d188eaf0..ddb2bac6c9 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -23,6 +23,7 @@ import { logger, } from '~/utils'; import { useApplyModelSpecEffects } from '~/hooks/Agents'; +import { startupConfigKey } from '~/data-provider'; import store from '~/store'; const useNavigateToConvo = (index = 0) => { @@ -41,7 +42,7 @@ const useNavigateToConvo = (index = 0) => { return; } - const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); + const startupConfig = queryClient.getQueryData(startupConfigKey(true)); applyModelSpecEffects({ startupConfig, specName: conversation?.spec, diff --git a/client/src/hooks/Endpoint/useEndpoints.ts b/client/src/hooks/Endpoint/useEndpoints.ts index acc9810093..dc72c0ddec 100644 --- a/client/src/hooks/Endpoint/useEndpoints.ts +++ b/client/src/hooks/Endpoint/useEndpoints.ts @@ -6,6 +6,7 @@ import { EModelEndpoint, PermissionTypes, getEndpointField, + getConfigDefaults, } from 'librechat-data-provider'; import type { TEndpointsConfig, @@ -20,6 +21,8 @@ import { mapEndpoints, getIconKey } from '~/utils'; import { useHasAccess } from '~/hooks'; import { icons } from './Icons'; +const defaultInterface = getConfigDefaults().interface; + export const useEndpoints = ({ agents, assistantsMap, @@ -33,7 +36,7 @@ export const useEndpoints = ({ }) => { const modelsQuery = useGetModelsQuery(); const { data: endpoints = [] } = useGetEndpointsQuery({ select: mapEndpoints }); - const interfaceConfig = startupConfig?.interface ?? {}; + const interfaceConfig = startupConfig?.interface ?? defaultInterface; const includedEndpoints = useMemo( () => new Set(startupConfig?.modelSpecs?.addedEndpoints ?? []), [startupConfig?.modelSpecs?.addedEndpoints], diff --git a/client/src/hooks/Input/useQueryParams.spec.ts b/client/src/hooks/Input/useQueryParams.spec.ts index f8b30b2eda..1f39c1c4ba 100644 --- a/client/src/hooks/Input/useQueryParams.spec.ts +++ b/client/src/hooks/Input/useQueryParams.spec.ts @@ -93,19 +93,23 @@ jest.mock('librechat-data-provider', () => { }; }); -// Mock data-provider hooks -jest.mock('~/data-provider', () => ({ - useGetAgentByIdQuery: jest.fn(() => ({ - data: null, - isLoading: false, - error: null, - })), - useListAgentsQuery: jest.fn(() => ({ - data: null, - isLoading: false, - error: null, - })), -})); +// Mock data-provider hooks while preserving real exports like startupConfigKey +jest.mock('~/data-provider', () => { + const actual = jest.requireActual('~/data-provider'); + return { + ...actual, + useGetAgentByIdQuery: jest.fn(() => ({ + data: null, + isLoading: false, + error: null, + })), + useListAgentsQuery: jest.fn(() => ({ + data: null, + isLoading: false, + error: null, + })), + }; +}); // Mock global window.history global.window = Object.create(window); diff --git a/client/src/hooks/Input/useQueryParams.ts b/client/src/hooks/Input/useQueryParams.ts index 85b5d8838b..cea11dc351 100644 --- a/client/src/hooks/Input/useQueryParams.ts +++ b/client/src/hooks/Input/useQueryParams.ts @@ -19,8 +19,8 @@ import { logger, } from '~/utils'; import { useAuthContext, useAgentsMap, useDefaultConvo, useSubmitMessage } from '~/hooks'; +import { startupConfigKey, useGetAgentByIdQuery } from '~/data-provider'; import { useChatContext, useChatFormContext } from '~/Providers'; -import { useGetAgentByIdQuery } from '~/data-provider'; import store from '~/store'; const injectAgentIntoAgentsMap = (queryClient: QueryClient, agent: any) => { @@ -84,7 +84,7 @@ export default function useQueryParams({ } let newPreset = removeUnavailableTools(_newPreset, availableTools); if (newPreset.spec != null && newPreset.spec !== '') { - const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); + const startupConfig = queryClient.getQueryData(startupConfigKey(true)); const modelSpecs = startupConfig?.modelSpecs?.list ?? []; const spec = modelSpecs.find((s) => s.name === newPreset.spec); if (!spec) { @@ -258,7 +258,7 @@ export default function useQueryParams({ if (!textAreaRef.current) { return; } - const startupConfig = queryClient.getQueryData([QueryKeys.startupConfig]); + const startupConfig = queryClient.getQueryData(startupConfigKey(true)); if (!startupConfig) { return; } diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 325ee97315..366775c4c1 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -33,7 +33,7 @@ import { removeConvoFromAllQueries, findConversationInInfinite, } from '~/utils'; -import { queueTitleGeneration } from '~/data-provider/SSE/queries'; +import { startupConfigKey, queueTitleGeneration } from '~/data-provider'; import useAttachmentHandler from '~/hooks/SSE/useAttachmentHandler'; import useContentHandler from '~/hooks/SSE/useContentHandler'; import useStepHandler from '~/hooks/SSE/useStepHandler'; @@ -406,7 +406,7 @@ export default function useEventHandlers({ sourceId: submission.conversation?.conversationId, ephemeralAgent: submission.ephemeralAgent, specName: submission.conversation?.spec, - startupConfig: queryClient.getQueryData([QueryKeys.startupConfig]), + startupConfig: queryClient.getQueryData(startupConfigKey(true)), }); } @@ -588,7 +588,7 @@ export default function useEventHandlers({ sourceId: submissionConvo.conversationId, ephemeralAgent: submission.ephemeralAgent, specName: submission.conversation?.spec, - startupConfig: queryClient.getQueryData([QueryKeys.startupConfig]), + startupConfig: queryClient.getQueryData(startupConfigKey(true)), }); }