mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(ui): stop polling on expired/invalid key auth error in PriceDataReload
When a user's virtual key expires, the JWT cookie may still be valid so they remain logged in. The PriceDataReload component polls /schedule/model_cost_map_reload/status and /model/cost_map/source every 30 seconds, triggering repeated ProxyException (Expired Key, HTTP 400) errors that spam the server logs. Changes: - Extract isAuthError() helper (module-level) to detect HTTP 400/401 responses - Wrap fetchReloadStatus and fetchSourceInfo with useCallback - On auth error, call stopPolling() which clears the interval and sets pollingDisabled=true to prevent further polling - Show an Alert banner when polling is paused, prompting the user to re-login - useEffect now depends on pollingDisabled so it restarts correctly after a fresh login (accessToken change resets pollingDisabled to false) Fixes: repeated ProxyException auth errors from expired virtual keys
This commit is contained in:
parent
c89496f378
commit
85820df639
1 changed files with 56 additions and 21 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from "react";
|
||||
import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider } from "antd";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Button, Popconfirm, Modal, InputNumber, Space, Typography, Tag, Card, Tooltip, Divider, Alert } from "antd";
|
||||
import { ReloadOutlined, ClockCircleOutlined, StopOutlined, CloudOutlined, DatabaseOutlined, InfoCircleOutlined, WarningOutlined } from "@ant-design/icons";
|
||||
import {
|
||||
reloadModelCostMap,
|
||||
|
|
@ -12,6 +12,13 @@ import NotificationsManager from "./molecules/notifications_manager";
|
|||
|
||||
const { Text } = Typography;
|
||||
|
||||
const isAuthError = (error: unknown): boolean => {
|
||||
if (error instanceof Error) {
|
||||
return /HTTP (400|401)/.test(error.message);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
interface ReloadStatus {
|
||||
scheduled: boolean;
|
||||
interval_hours: number | null;
|
||||
|
|
@ -55,22 +62,18 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
const [loadingStatus, setLoadingStatus] = useState(false);
|
||||
const [sourceInfo, setSourceInfo] = useState<CostMapSourceInfo | null>(null);
|
||||
const [loadingSource, setLoadingSource] = useState(false);
|
||||
const [pollingDisabled, setPollingDisabled] = useState(false);
|
||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Fetch status on component mount and periodically
|
||||
useEffect(() => {
|
||||
fetchReloadStatus();
|
||||
fetchSourceInfo();
|
||||
const stopPolling = useCallback(() => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
setPollingDisabled(true);
|
||||
}, []);
|
||||
|
||||
// Refresh status every 30 seconds to keep it up to date
|
||||
const interval = setInterval(() => {
|
||||
fetchReloadStatus();
|
||||
fetchSourceInfo();
|
||||
}, 30000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [accessToken]);
|
||||
|
||||
const fetchReloadStatus = async () => {
|
||||
const fetchReloadStatus = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoadingStatus(true);
|
||||
|
|
@ -81,7 +84,10 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
setReloadStatus(status);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch reload status:", error);
|
||||
// Set a default status to prevent UI issues
|
||||
if (isAuthError(error)) {
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
setReloadStatus({
|
||||
scheduled: false,
|
||||
interval_hours: null,
|
||||
|
|
@ -91,9 +97,9 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
} finally {
|
||||
setLoadingStatus(false);
|
||||
}
|
||||
};
|
||||
}, [accessToken, stopPolling]);
|
||||
|
||||
const fetchSourceInfo = async () => {
|
||||
const fetchSourceInfo = useCallback(async () => {
|
||||
if (!accessToken) return;
|
||||
|
||||
setLoadingSource(true);
|
||||
|
|
@ -102,10 +108,32 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
setSourceInfo(info);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch cost map source info:", error);
|
||||
if (isAuthError(error)) {
|
||||
stopPolling();
|
||||
}
|
||||
} finally {
|
||||
setLoadingSource(false);
|
||||
}
|
||||
};
|
||||
}, [accessToken, stopPolling]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accessToken || pollingDisabled) return;
|
||||
|
||||
fetchReloadStatus();
|
||||
fetchSourceInfo();
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
fetchReloadStatus();
|
||||
fetchSourceInfo();
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current !== null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [accessToken, pollingDisabled, fetchReloadStatus, fetchSourceInfo]);
|
||||
|
||||
const handleHardRefresh = async () => {
|
||||
if (!accessToken) {
|
||||
|
|
@ -210,7 +238,14 @@ const PriceDataReload: React.FC<PriceDataReloadProps> = ({
|
|||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Action Buttons */}
|
||||
{pollingDisabled && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message="Auto-refresh paused — your session key has expired. Please re-login to restore live status."
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
)}
|
||||
<Space direction="horizontal" size="middle" style={{ marginBottom: 16 }}>
|
||||
{/* Hard Refresh Button - Always visible */}
|
||||
<Popconfirm
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue