🔃 fix: Token Refresh in Browser Only, Redirect on Refresh Failure (#9583)

* 🔃 fix: Token Refresh in Browser Only, Redirect on Refresh Failure

* chore: Update import for SearchResultData and fix FormattedToolResponse type build warning
This commit is contained in:
Danny Avila 2025-09-11 16:51:40 -04:00 committed by GitHub
parent 180046a3c5
commit e3a645e8fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 61 deletions

View file

@ -8,7 +8,7 @@ import {
StreamableHTTPOptionsSchema, StreamableHTTPOptionsSchema,
Tools, Tools,
} from 'librechat-data-provider'; } 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 * as t from '@modelcontextprotocol/sdk/types.js';
import type { TokenMethods } from '@librechat/data-schemas'; import type { TokenMethods } from '@librechat/data-schemas';
import type { FlowStateManager } from '~/flow/manager'; import type { FlowStateManager } from '~/flow/manager';
@ -133,7 +133,7 @@ export type Artifacts =
sources: FileSearchSource[]; sources: FileSearchSource[];
fileCitations?: boolean; fileCitations?: boolean;
}; };
[Tools.web_search]?: import('librechat-data-provider').SearchResultData; [Tools.web_search]?: SearchResultData;
files?: Array<{ id: string; name: string }>; files?: Array<{ id: string; name: string }>;
session_id?: string; session_id?: string;
file_ids?: string[]; file_ids?: string[];
@ -144,10 +144,7 @@ export type FormattedContentResult = [string | FormattedContent[], undefined | A
export type ImageFormatter = (item: ImageContent) => FormattedContent; export type ImageFormatter = (item: ImageContent) => FormattedContent;
export type FormattedToolResponse = [ export type FormattedToolResponse = FormattedContentResult;
string | FormattedContent[],
{ content: FormattedContent[] } | undefined,
];
export type ParsedServerConfig = MCPOptions & { export type ParsedServerConfig = MCPOptions & {
url?: string; url?: string;

View file

@ -83,70 +83,75 @@ const processQueue = (error: AxiosError | null, token: string | null = null) =>
failedQueue = []; failedQueue = [];
}; };
axios.interceptors.response.use( if (typeof window !== 'undefined') {
(response) => response, axios.interceptors.response.use(
async (error) => { (response) => response,
const originalRequest = error.config; async (error) => {
if (!error.response) { const originalRequest = error.config;
return Promise.reject(error); if (!error.response) {
} return Promise.reject(error);
}
if (originalRequest.url?.includes('/api/auth/2fa') === true) { if (originalRequest.url?.includes('/api/auth/2fa') === true) {
return Promise.reject(error); return Promise.reject(error);
} }
if (originalRequest.url?.includes('/api/auth/logout') === true) { if (originalRequest.url?.includes('/api/auth/logout') === true) {
return Promise.reject(error); 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) { if (error.response.status === 401 && !originalRequest._retry) {
console.warn('401 error, refreshing token'); console.warn('401 error, refreshing token');
originalRequest._retry = true; 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 { try {
const token = await new Promise((resolve, reject) => { const response = await refreshToken();
failedQueue.push({ resolve, reject });
}); const token = response?.token ?? '';
originalRequest.headers['Authorization'] = 'Bearer ' + token;
return await axios(originalRequest); 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) { } catch (err) {
processQueue(err as AxiosError, null);
return Promise.reject(err); return Promise.reject(err);
} finally {
isRefreshing = false;
} }
} }
isRefreshing = true; return Promise.reject(error);
},
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);
},
);
export default { export default {
get: _get, get: _get,