From 85820df63920de9b7ca5cc24a6caed2da4ae9217 Mon Sep 17 00:00:00 2001 From: "xy.kong" Date: Sun, 22 Mar 2026 17:30:50 +0800 Subject: [PATCH] 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 --- .../src/components/price_data_reload.tsx | 77 ++++++++++++++----- 1 file changed, 56 insertions(+), 21 deletions(-) diff --git a/ui/litellm-dashboard/src/components/price_data_reload.tsx b/ui/litellm-dashboard/src/components/price_data_reload.tsx index f8b0ab24c36..973fcb7deb8 100644 --- a/ui/litellm-dashboard/src/components/price_data_reload.tsx +++ b/ui/litellm-dashboard/src/components/price_data_reload.tsx @@ -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 = ({ const [loadingStatus, setLoadingStatus] = useState(false); const [sourceInfo, setSourceInfo] = useState(null); const [loadingSource, setLoadingSource] = useState(false); + const [pollingDisabled, setPollingDisabled] = useState(false); + const intervalRef = useRef | 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 = ({ 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 = ({ } 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 = ({ 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 = ({ return (
- {/* Action Buttons */} + {pollingDisabled && ( + + )} {/* Hard Refresh Button - Always visible */}