🌐 fix: Preserve URL Query Params Through Auth Refresh and Conversation Init (#12028)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions

* 🔗 fix: Preserve URL query params during silent token refresh

The silent token refresh on hard navigation was redirecting to '/c/new'
without query params, wiping the URL before ChatRoute could read them.
Now preserves the current URL (pathname + search) as the redirect
fallback, with isSafeRedirect validation.

* 🧭 fix: Apply URL query params in ChatRoute initialization

ChatRoute now reads URL search params (endpoint, model, agent_id, etc.)
and merges them into the preset passed to newConversation(), so the
first conversation init already includes the URL param settings. This
eliminates the race where useQueryParams fired too late.

- Export processValidSettings from useQueryParams for reuse
- Add getNewConvoPreset helper in ChatRoute (used in both NEW_CONVO branches)
- Query params take precedence over model spec defaults
- useQueryParams now waits for endpointsConfig before processing
- Skip redundant newQueryConvo when settings are already applied
- Clean all URL params via setSearchParams after processing

*  test: Update useQueryParams tests for new URL cleanup behavior

- Assert setSearchParams called instead of window.history.replaceState
- Mock endpoints config in deferred submission and timeout tests

* ♻️ refactor: Move processValidSettings to ~/utils and address review findings

- Move processValidSettings/parseQueryValue to createChatSearchParams.ts
  (pure utility, not hook-specific)
- Fix processSubmission: use setSearchParams instead of replaceState,
  move URL cleanup outside data.text guard
- Narrow endpointsConfig guard: only block settings application, not
  prompt-only flows
- Convert areSettingsApplied to stable useCallback ([] deps) with
  conversationRef to avoid interval churn on conversation updates
- Replace console.log with logger.log in production paths
- Restore explanatory comment on pendingSubmitRef guard
- Use for...of in processValidSettings (CLAUDE.md preference)
- Remove unused imports from useQueryParams

* 🔧 fix: Add areSettingsApplied to effect deps and fix test mocks

- Restore areSettingsApplied in main effect deps (stable identity with
  [] deps, safe to include — satisfies exhaustive-deps lint rule)
- Fix all test getQueryData mocks to properly distinguish between
  startupConfig and endpoints keys
- Assert setSearchParams call arguments (URLSearchParams + replace:true)

*  test: Assert empty URLSearchParams in setSearchParams calls

Tighten setSearchParams assertions to verify the params are empty
(toString() === ''), not just that a URLSearchParams instance was passed.

* 🔧 test: Update AuthContext tests to navigate to current URL for redirects

- Modified test cases to assert navigation to the current URL instead of a hardcoded '/c/new' when no stored redirect exists or when falling back from unsafe stored redirects.
- Enhanced test setup to define window.location for accurate simulation of redirect behavior.
This commit is contained in:
Danny Avila 2026-03-02 23:32:53 -05:00 committed by GitHub
parent 7c71875da3
commit b1771e0a6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 180 additions and 137 deletions

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { Spinner, useToastContext } from '@librechat/client';
import { useParams, useSearchParams } from 'react-router-dom';
import { Constants, EModelEndpoint } from 'librechat-data-provider';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TPreset } from 'librechat-data-provider';
@ -13,7 +13,13 @@ import {
useLocalize,
} from '~/hooks';
import { useGetConvoIdQuery, useGetStartupConfig, useGetEndpointsQuery } from '~/data-provider';
import { getDefaultModelSpec, getModelSpecPreset, logger, isNotFoundError } from '~/utils';
import {
getDefaultModelSpec,
getModelSpecPreset,
processValidSettings,
logger,
isNotFoundError,
} from '~/utils';
import { ToolCallsMapProvider } from '~/Providers';
import ChatView from '~/components/Chat/ChatView';
import { NotificationSeverity } from '~/common';
@ -36,6 +42,7 @@ export default function ChatRoute() {
useAppStartup({ startupConfig, user });
const index = 0;
const [searchParams] = useSearchParams();
const { conversationId = '' } = useParams();
useIdChangeEffect(conversationId);
const { hasSetConversation, conversation } = store.useCreateConversationAtom(index);
@ -80,14 +87,34 @@ export default function ChatRoute() {
return;
}
if (conversationId === Constants.NEW_CONVO && endpointsQuery.data && modelsQuery.data) {
const isNewConvo = conversationId === Constants.NEW_CONVO;
const getNewConvoPreset = () => {
const result = getDefaultModelSpec(startupConfig);
const spec = result?.default ?? result?.last;
const specPreset = spec ? getModelSpecPreset(spec) : undefined;
const queryParams: Record<string, string> = {};
searchParams.forEach((value, key) => {
if (key !== 'prompt' && key !== 'q' && key !== 'submit') {
queryParams[key] = value;
}
});
const querySettings = processValidSettings(queryParams);
return Object.keys(querySettings).length > 0
? { ...specPreset, ...querySettings }
: specPreset;
};
if (isNewConvo && endpointsQuery.data && modelsQuery.data) {
const preset = getNewConvoPreset();
logger.log('conversation', 'ChatRoute, new convo effect', conversation);
newConversation({
modelsData: modelsQuery.data,
template: conversation ? conversation : undefined,
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
...(preset ? { preset } : {}),
});
hasSetConversation.current = true;
@ -125,17 +152,17 @@ export default function ChatRoute() {
});
hasSetConversation.current = true;
} else if (
conversationId === Constants.NEW_CONVO &&
isNewConvo &&
assistantListMap[EModelEndpoint.assistants] &&
assistantListMap[EModelEndpoint.azureAssistants]
) {
const result = getDefaultModelSpec(startupConfig);
const spec = result?.default ?? result?.last;
const preset = getNewConvoPreset();
logger.log('conversation', 'ChatRoute new convo, assistants effect', conversation);
newConversation({
modelsData: modelsQuery.data,
template: conversation ? conversation : undefined,
...(spec ? { preset: getModelSpecPreset(spec) } : {}),
...(preset ? { preset } : {}),
});
hasSetConversation.current = true;
} else if (