From e3a645e8fb098e235f9d392d8c7748c4049b24b9 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Thu, 11 Sep 2025 16:51:40 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=83=20fix:=20Token=20Refresh=20in=20Br?= =?UTF-8?q?owser=20Only,=20Redirect=20on=20Refresh=20Failure=20(#9583)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔃 fix: Token Refresh in Browser Only, Redirect on Refresh Failure * chore: Update import for SearchResultData and fix FormattedToolResponse type build warning --- packages/api/src/mcp/types/index.ts | 9 +- packages/data-provider/src/request.ts | 115 ++++++++++++++------------ 2 files changed, 63 insertions(+), 61 deletions(-) diff --git a/packages/api/src/mcp/types/index.ts b/packages/api/src/mcp/types/index.ts index b774a2fe1..7d137afd0 100644 --- a/packages/api/src/mcp/types/index.ts +++ b/packages/api/src/mcp/types/index.ts @@ -8,7 +8,7 @@ import { StreamableHTTPOptionsSchema, Tools, } from 'librechat-data-provider'; -import type { UIResource, TPlugin, TUser } from 'librechat-data-provider'; +import type { SearchResultData, UIResource, TPlugin, TUser } from 'librechat-data-provider'; import type * as t from '@modelcontextprotocol/sdk/types.js'; import type { TokenMethods } from '@librechat/data-schemas'; import type { FlowStateManager } from '~/flow/manager'; @@ -133,7 +133,7 @@ export type Artifacts = sources: FileSearchSource[]; fileCitations?: boolean; }; - [Tools.web_search]?: import('librechat-data-provider').SearchResultData; + [Tools.web_search]?: SearchResultData; files?: Array<{ id: string; name: string }>; session_id?: string; file_ids?: string[]; @@ -144,10 +144,7 @@ export type FormattedContentResult = [string | FormattedContent[], undefined | A export type ImageFormatter = (item: ImageContent) => FormattedContent; -export type FormattedToolResponse = [ - string | FormattedContent[], - { content: FormattedContent[] } | undefined, -]; +export type FormattedToolResponse = FormattedContentResult; export type ParsedServerConfig = MCPOptions & { url?: string; diff --git a/packages/data-provider/src/request.ts b/packages/data-provider/src/request.ts index e4dd53847..3aa2021ea 100644 --- a/packages/data-provider/src/request.ts +++ b/packages/data-provider/src/request.ts @@ -83,70 +83,75 @@ const processQueue = (error: AxiosError | null, token: string | null = null) => failedQueue = []; }; -axios.interceptors.response.use( - (response) => response, - async (error) => { - const originalRequest = error.config; - if (!error.response) { - return Promise.reject(error); - } +if (typeof window !== 'undefined') { + axios.interceptors.response.use( + (response) => response, + async (error) => { + const originalRequest = error.config; + if (!error.response) { + return Promise.reject(error); + } - if (originalRequest.url?.includes('/api/auth/2fa') === true) { - return Promise.reject(error); - } - if (originalRequest.url?.includes('/api/auth/logout') === true) { - return Promise.reject(error); - } + if (originalRequest.url?.includes('/api/auth/2fa') === true) { + return Promise.reject(error); + } + if (originalRequest.url?.includes('/api/auth/logout') === true) { + return Promise.reject(error); + } + if (originalRequest.url?.includes('/api/auth/refresh') === true) { + // Refresh token itself failed - redirect to login + console.log('Refresh token request failed, redirecting to login...'); + window.location.href = '/login'; + return Promise.reject(error); + } - if (error.response.status === 401 && !originalRequest._retry) { - console.warn('401 error, refreshing token'); - originalRequest._retry = true; + if (error.response.status === 401 && !originalRequest._retry) { + console.warn('401 error, refreshing token'); + originalRequest._retry = true; + + if (isRefreshing) { + try { + const token = await new Promise((resolve, reject) => { + failedQueue.push({ resolve, reject }); + }); + originalRequest.headers['Authorization'] = 'Bearer ' + token; + return await axios(originalRequest); + } catch (err) { + return Promise.reject(err); + } + } + + isRefreshing = true; - if (isRefreshing) { try { - const token = await new Promise((resolve, reject) => { - failedQueue.push({ resolve, reject }); - }); - originalRequest.headers['Authorization'] = 'Bearer ' + token; - return await axios(originalRequest); + const response = await refreshToken(); + + const token = response?.token ?? ''; + + if (token) { + originalRequest.headers['Authorization'] = 'Bearer ' + token; + dispatchTokenUpdatedEvent(token); + processQueue(null, token); + return await axios(originalRequest); + } else if (window.location.href.includes('share/')) { + console.log( + `Refresh token failed from shared link, attempting request to ${originalRequest.url}`, + ); + } else { + window.location.href = '/login'; + } } catch (err) { + processQueue(err as AxiosError, null); return Promise.reject(err); + } finally { + isRefreshing = false; } } - isRefreshing = true; - - try { - const response = await refreshToken( - // Handle edge case where we get a blank screen if the initial 401 error is from a refresh token request - originalRequest.url?.includes('api/auth/refresh') === true ? true : false, - ); - - const token = response?.token ?? ''; - - if (token) { - originalRequest.headers['Authorization'] = 'Bearer ' + token; - dispatchTokenUpdatedEvent(token); - processQueue(null, token); - return await axios(originalRequest); - } else if (window.location.href.includes('share/')) { - console.log( - `Refresh token failed from shared link, attempting request to ${originalRequest.url}`, - ); - } else { - window.location.href = '/login'; - } - } catch (err) { - processQueue(err as AxiosError, null); - return Promise.reject(err); - } finally { - isRefreshing = false; - } - } - - return Promise.reject(error); - }, -); + return Promise.reject(error); + }, + ); +} export default { get: _get,