From c71303b3ef112bed3e5567ad4372daf7988d9956 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 7 Nov 2025 14:47:48 +0530 Subject: [PATCH 1/4] feat: handle 429 errors and redirect to rate-limited page --- packages/ui/src/routes/AuthRoutes.jsx | 5 + .../ui/src/store/context/ErrorContext.jsx | 5 +- packages/ui/src/views/auth/rateLimited.jsx | 116 ++++++++++++++++++ 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 packages/ui/src/views/auth/rateLimited.jsx diff --git a/packages/ui/src/routes/AuthRoutes.jsx b/packages/ui/src/routes/AuthRoutes.jsx index 2d63fc38719..eb303b98c13 100644 --- a/packages/ui/src/routes/AuthRoutes.jsx +++ b/packages/ui/src/routes/AuthRoutes.jsx @@ -10,6 +10,7 @@ const VerifyEmailPage = Loadable(lazy(() => import('@/views/auth/verify-email')) const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword'))) const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword'))) const UnauthorizedPage = Loadable(lazy(() => import('@/views/auth/unauthorized'))) +const RateLimitedPage = Loadable(lazy(() => import('@/views/auth/rateLimited'))) const OrganizationSetupPage = Loadable(lazy(() => import('@/views/organization/index'))) const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired'))) @@ -45,6 +46,10 @@ const AuthRoutes = { path: '/unauthorized', element: }, + { + path: '/rate-limited', + element: + }, { path: '/organization-setup', element: diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index e41070a1516..4a4870d3f8f 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -14,7 +14,10 @@ export const ErrorProvider = ({ children }) => { const handleError = async (err) => { console.error(err) - if (err?.response?.status === 403) { + if (err?.response?.status === 429) { + const retryAfter = parseInt(err?.response?.headers?.['retry-after']) || 60 + navigate('/rate-limited', { state: { retryAfter } }) + } else if (err?.response?.status === 403) { navigate('/unauthorized') } else if (err?.response?.status === 401) { if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) { diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx new file mode 100644 index 00000000000..fdcb08cfadc --- /dev/null +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -0,0 +1,116 @@ +import { useEffect, useState } from 'react' +import MainCard from '@/ui-component/cards/MainCard' +import { Box, Stack, Typography, LinearProgress } from '@mui/material' +import unauthorizedSVG from '@/assets/images/unauthorized.svg' +import { StyledButton } from '@/ui-component/button/StyledButton' +import { useNavigate, useLocation } from 'react-router-dom' +import { useSelector } from 'react-redux' + +// ==============================|| RateLimitedPage ||============================== // + +const RateLimitedPage = () => { + const navigate = useNavigate() + const location = useLocation() + const currentUser = useSelector((state) => state.auth.user) + + const retryAfter = location.state?.retryAfter || 60 + const [countdown, setCountdown] = useState(retryAfter) + const [canRetry, setCanRetry] = useState(false) + + useEffect(() => { + if (countdown <= 0) { + setCanRetry(true) + return + } + + const timer = setInterval(() => { + setCountdown((prev) => { + if (prev <= 1) { + setCanRetry(true) + return 0 + } + return prev - 1 + }) + }, 1000) + + return () => clearInterval(timer) + }, [countdown]) + + const handleRetry = () => { + navigate(-1) + } + + const handleGoHome = () => { + navigate('/') + } + + const progress = ((retryAfter - countdown) / retryAfter) * 100 + + return ( + <> + + + + + rateLimitedSVG + + + 429 Too Many Requests + + + You've made too many requests in a short period of time. Please wait a moment before trying again. + + + {!canRetry && ( + + + Please wait {countdown} second{countdown !== 1 ? 's' : ''} + + + + )} + + + + Try Again + + {currentUser && ( + + Back to Home + + )} + + + + + + ) +} + +export default RateLimitedPage From bee8ce8c239c78a7a659d1c23d413861be799d27 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Fri, 7 Nov 2025 14:58:00 +0530 Subject: [PATCH 2/4] fix: simplify rate-limited page and better 429 error handling --- .../ui/src/store/context/ErrorContext.jsx | 14 +- packages/ui/src/views/auth/rateLimited.jsx | 128 ++++-------------- 2 files changed, 42 insertions(+), 100 deletions(-) diff --git a/packages/ui/src/store/context/ErrorContext.jsx b/packages/ui/src/store/context/ErrorContext.jsx index 4a4870d3f8f..9201242acf4 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -15,7 +15,19 @@ export const ErrorProvider = ({ children }) => { const handleError = async (err) => { console.error(err) if (err?.response?.status === 429) { - const retryAfter = parseInt(err?.response?.headers?.['retry-after']) || 60 + const retryAfterHeader = err?.response?.headers?.['retry-after'] + let retryAfter = 60 // Default in seconds + if (retryAfterHeader) { + const parsedSeconds = parseInt(retryAfterHeader, 10) + if (Number.isNaN(parsedSeconds)) { + const retryDate = new Date(retryAfterHeader) + if (!Number.isNaN(retryDate.getTime())) { + retryAfter = Math.max(0, Math.ceil((retryDate.getTime() - Date.now()) / 1000)) + } + } else { + retryAfter = parsedSeconds + } + } navigate('/rate-limited', { state: { retryAfter } }) } else if (err?.response?.status === 403) { navigate('/unauthorized') diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx index fdcb08cfadc..ac1093a11d3 100644 --- a/packages/ui/src/views/auth/rateLimited.jsx +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -1,115 +1,45 @@ -import { useEffect, useState } from 'react' -import MainCard from '@/ui-component/cards/MainCard' -import { Box, Stack, Typography, LinearProgress } from '@mui/material' +import { Box, Stack, Typography } from '@mui/material' +import { useLocation } from 'react-router-dom' import unauthorizedSVG from '@/assets/images/unauthorized.svg' -import { StyledButton } from '@/ui-component/button/StyledButton' -import { useNavigate, useLocation } from 'react-router-dom' -import { useSelector } from 'react-redux' +import MainCard from '@/ui-component/cards/MainCard' // ==============================|| RateLimitedPage ||============================== // const RateLimitedPage = () => { - const navigate = useNavigate() const location = useLocation() - const currentUser = useSelector((state) => state.auth.user) - - const retryAfter = location.state?.retryAfter || 60 - const [countdown, setCountdown] = useState(retryAfter) - const [canRetry, setCanRetry] = useState(false) - - useEffect(() => { - if (countdown <= 0) { - setCanRetry(true) - return - } - - const timer = setInterval(() => { - setCountdown((prev) => { - if (prev <= 1) { - setCanRetry(true) - return 0 - } - return prev - 1 - }) - }, 1000) - - return () => clearInterval(timer) - }, [countdown]) - const handleRetry = () => { - navigate(-1) - } - - const handleGoHome = () => { - navigate('/') - } - - const progress = ((retryAfter - countdown) / retryAfter) * 100 + const retryAfter = location.state?.retryAfter || 60 return ( - <> - - + + - - - rateLimitedSVG - - - 429 Too Many Requests - - - You've made too many requests in a short period of time. Please wait a moment before trying again. - - - {!canRetry && ( - - - Please wait {countdown} second{countdown !== 1 ? 's' : ''} - - - - )} - - - - Try Again - - {currentUser && ( - - Back to Home - - )} - - - - - + + rateLimitedSVG + + + 429 Too Many Requests + + + {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`} + + + + ) } From 2a08897107c274434f21099a70689440390f1e1c Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 10 Nov 2025 13:21:42 +0530 Subject: [PATCH 3/4] fix: status code in quotaUsage --- packages/server/src/utils/quotaUsage.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/server/src/utils/quotaUsage.ts b/packages/server/src/utils/quotaUsage.ts index e2cf382d4dc..35cf855aa1b 100644 --- a/packages/server/src/utils/quotaUsage.ts +++ b/packages/server/src/utils/quotaUsage.ts @@ -70,7 +70,7 @@ export const checkUsageLimit = async ( if (limit === -1) return if (currentUsage > limit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, `Limit exceeded: ${type}`) + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, `Limit exceeded: ${type}`) } } @@ -135,7 +135,7 @@ export const checkPredictions = async (orgId: string, subscriptionId: string, us if (predictionsLimit === -1) return if (currentPredictions >= predictionsLimit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Predictions limit exceeded') + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Predictions limit exceeded') } return { @@ -161,7 +161,7 @@ export const checkStorage = async (orgId: string, subscriptionId: string, usageC if (storageLimit === -1) return if (currentStorageUsage >= storageLimit) { - throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Storage limit exceeded') + throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Storage limit exceeded') } return { From f6b566c68fe09af0d8d54c0fddcaf6f0832eeb48 Mon Sep 17 00:00:00 2001 From: Ilango Rajagopal Date: Mon, 10 Nov 2025 14:50:53 +0530 Subject: [PATCH 4/4] update: add back to home button rate-limited page --- packages/ui/src/views/auth/rateLimited.jsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx index ac1093a11d3..44b8a85dd6f 100644 --- a/packages/ui/src/views/auth/rateLimited.jsx +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -1,5 +1,5 @@ -import { Box, Stack, Typography } from '@mui/material' -import { useLocation } from 'react-router-dom' +import { Box, Button, Stack, Typography } from '@mui/material' +import { Link, useLocation } from 'react-router-dom' import unauthorizedSVG from '@/assets/images/unauthorized.svg' import MainCard from '@/ui-component/cards/MainCard' @@ -37,6 +37,11 @@ const RateLimitedPage = () => { {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`} + + +