From 4e92db69104de8dffac984e7212722c6dc2afcc0 Mon Sep 17 00:00:00 2001 From: Ilango Date: Thu, 27 Nov 2025 16:53:49 +0530 Subject: [PATCH] feat: handle 429 errors and redirect to rate-limited page (#5440) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: handle 429 errors and redirect to rate-limited page * fix: simplify rate-limited page and better 429 error handling * fix: status code in quotaUsage * update: add back to home button rate-limited page * chore: fix typos in docker/worker/Dockerfile (#5435) Fix typos in docker/worker/Dockerfile * chore: fix typos in packages/components/nodes/agentflow/Condition/Condition.ts (#5436) Fix typos in packages/components/nodes/agentflow/Condition/Condition.ts * chore: fix typos in packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts (#5437) Fix typos in packages/components/nodes/chatmodels/ChatHuggingFace/ChatHuggingFace.ts * chore: fix typos in packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts (#5438) Fix typos in packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts * docs: fix typos in packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx (#5444) Fix typos in packages/ui/src/layout/MainLayout/Sidebar/MenuList/NavGroup/index.jsx * docs: fix typos in packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts (#5446) Fix typos in packages/components/nodes/engine/SubQuestionQueryEngine/SubQuestionQueryEngine.ts * docs: fix typos in packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts (#5447) Fix typos in packages/components/nodes/embeddings/AWSBedrockEmbedding/AWSBedrockEmbedding.ts * docs: fix typos in packages/server/README.md (#5445) Fix typos in packages/server/README.md * Bugfix/Supervisor Node AzureChatOpenAI (#5448) Integrate AzureChatOpenAI into the Supervisor node to handle user requests alongside ChatOpenAI. This enhancement allows for improved multi-agent conversation management. * Chore/JSON Array (#5467) * add separate by JSON object * add file check for Unstructured * Enhance JSON DocumentLoader: Update label and description for 'Separate by JSON Object' option, and add type check for JSON objects in array processing. * Chore/Remove Deprecated File Path Unstructured (#5478) * Refactor UnstructuredFile and UnstructuredFolder loaders to remove deprecated file path handling and enhance folder path validation. Ensure folder paths are sanitized and validated against path traversal attacks. * Update UnstructuredFolder.ts * feat(security): enhance file path validation and implement non-root D… (#5474) * feat(security): enhance file path validation and implement non-root Docker user - Validate resolved full file paths including workspace boundaries in SecureFileStore - Resolve paths before validation in readFile and writeFile operations - Run Docker container as non-root flowise user (uid/gid 1001) - Apply proper file ownership and permissions for application files Prevents path traversal attacks and follows container security best practices * Add sensitive system directory validation and Flowise internal file protection * Update Dockerfile to use default node user * update validation patterns to include additional system binary directories (/usr/bin, /usr/sbin, /usr/local/bin) * added isSafeBrowserExecutable function to validate browser executable paths for Playwright and Puppeteer loaders --------- Co-authored-by: taraka-vishnumolakala Co-authored-by: Henry Heng Co-authored-by: Henry * Chore/docker file non root (#5479) * update dockerfile * Update Dockerfile * remove read write file tools and imports (#5480) * Bugfix/Custom Function Libraries (#5472) * Updated the executeJavaScriptCode function to automatically detect and install required libraries from import/require statements in the provided code. * Update utils.ts * lint-fix * Release/3.0.11 (#5481) flowise@3.0.11 * flowise@3.0.11 * Chore/Disable Unstructure Folder (#5483) * commented out unstructure folder node * Update packages/components/nodes/documentloaders/Unstructured/UnstructuredFolder.ts Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * update: condition for handling 429 errors * update: handle rate limit errors in auth pages * fix: crash due to missing import --------- Co-authored-by: Lê Nam Khánh <55955273+khanhkhanhlele@users.noreply.github.com> Co-authored-by: Henry Heng Co-authored-by: Taraka Vishnumolakala Co-authored-by: taraka-vishnumolakala Co-authored-by: Henry Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- packages/server/src/utils/quotaUsage.ts | 6 +-- packages/ui/src/routes/AuthRoutes.jsx | 5 ++ .../ui/src/store/context/ErrorContext.jsx | 24 ++++++++- packages/ui/src/views/auth/forgotPassword.jsx | 14 +++++ packages/ui/src/views/auth/rateLimited.jsx | 51 +++++++++++++++++++ packages/ui/src/views/auth/register.jsx | 12 ++++- packages/ui/src/views/auth/resetPassword.jsx | 18 ++++++- packages/ui/src/views/auth/signIn.jsx | 12 ++++- 8 files changed, 134 insertions(+), 8 deletions(-) create mode 100644 packages/ui/src/views/auth/rateLimited.jsx diff --git a/packages/server/src/utils/quotaUsage.ts b/packages/server/src/utils/quotaUsage.ts index e2cf382d4..35cf855aa 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 { diff --git a/packages/ui/src/routes/AuthRoutes.jsx b/packages/ui/src/routes/AuthRoutes.jsx index 2d63fc387..eb303b98c 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 e41070a15..8f97bb980 100644 --- a/packages/ui/src/store/context/ErrorContext.jsx +++ b/packages/ui/src/store/context/ErrorContext.jsx @@ -10,11 +10,29 @@ const ErrorContext = createContext() export const ErrorProvider = ({ children }) => { const [error, setError] = useState(null) + const [authRateLimitError, setAuthRateLimitError] = useState(null) const navigate = useNavigate() const handleError = async (err) => { console.error(err) - if (err?.response?.status === 403) { + if (err?.response?.status === 429 && err?.response?.data?.type === 'authentication_rate_limit') { + setAuthRateLimitError("You're making a lot of requests. Please wait and try again later.") + } else if (err?.response?.status === 429 && err?.response?.data?.type !== 'authentication_rate_limit') { + 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') } else if (err?.response?.status === 401) { if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) { @@ -44,7 +62,9 @@ export const ErrorProvider = ({ children }) => { value={{ error, setError, - handleError + handleError, + authRateLimitError, + setAuthRateLimitError }} > {children} diff --git a/packages/ui/src/views/auth/forgotPassword.jsx b/packages/ui/src/views/auth/forgotPassword.jsx index 9e17f2436..7e375a125 100644 --- a/packages/ui/src/views/auth/forgotPassword.jsx +++ b/packages/ui/src/views/auth/forgotPassword.jsx @@ -16,6 +16,7 @@ import accountApi from '@/api/account.api' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // utils import useNotifier from '@/utils/useNotifier' @@ -41,10 +42,13 @@ const ForgotPasswordPage = () => { const [isLoading, setLoading] = useState(false) const [responseMsg, setResponseMsg] = useState(undefined) + const { authRateLimitError, setAuthRateLimitError } = useError() + const forgotPasswordApi = useApi(accountApi.forgotPassword) const sendResetRequest = async (event) => { event.preventDefault() + setAuthRateLimitError(null) const body = { user: { email: usernameVal @@ -54,6 +58,11 @@ const ForgotPasswordPage = () => { await forgotPasswordApi.request(body) } + useEffect(() => { + setAuthRateLimitError(null) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setAuthRateLimitError]) + useEffect(() => { if (forgotPasswordApi.error) { const errMessage = @@ -89,6 +98,11 @@ const ForgotPasswordPage = () => { {responseMsg.msg} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {responseMsg && responseMsg?.type !== 'error' && ( } variant='filled' severity='success'> {responseMsg.msg} diff --git a/packages/ui/src/views/auth/rateLimited.jsx b/packages/ui/src/views/auth/rateLimited.jsx new file mode 100644 index 000000000..44b8a85dd --- /dev/null +++ b/packages/ui/src/views/auth/rateLimited.jsx @@ -0,0 +1,51 @@ +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' + +// ==============================|| RateLimitedPage ||============================== // + +const RateLimitedPage = () => { + const location = useLocation() + + const retryAfter = location.state?.retryAfter || 60 + + return ( + + + + + rateLimitedSVG + + + 429 Too Many Requests + + + {`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`} + + + + + + + + ) +} + +export default RateLimitedPage diff --git a/packages/ui/src/views/auth/register.jsx b/packages/ui/src/views/auth/register.jsx index 30c18b12e..3273f3402 100644 --- a/packages/ui/src/views/auth/register.jsx +++ b/packages/ui/src/views/auth/register.jsx @@ -18,6 +18,7 @@ import ssoApi from '@/api/sso' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // utils import useNotifier from '@/utils/useNotifier' @@ -111,7 +112,9 @@ const RegisterPage = () => { const [loading, setLoading] = useState(false) const [authError, setAuthError] = useState('') - const [successMsg, setSuccessMsg] = useState(undefined) + const [successMsg, setSuccessMsg] = useState('') + + const { authRateLimitError, setAuthRateLimitError } = useError() const registerApi = useApi(accountApi.registerAccount) const ssoLoginApi = useApi(ssoApi.ssoLogin) @@ -120,6 +123,7 @@ const RegisterPage = () => { const register = async (event) => { event.preventDefault() + setAuthRateLimitError(null) if (isEnterpriseLicensed) { const result = RegisterEnterpriseUserSchema.safeParse({ username, @@ -192,6 +196,7 @@ const RegisterPage = () => { }, [registerApi.error]) useEffect(() => { + setAuthRateLimitError(null) if (!isOpenSource) { getDefaultProvidersApi.request() } @@ -274,6 +279,11 @@ const RegisterPage = () => { )} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {successMsg && ( } variant='filled' severity='success'> {successMsg} diff --git a/packages/ui/src/views/auth/resetPassword.jsx b/packages/ui/src/views/auth/resetPassword.jsx index 3ca33f8cd..f451b8a20 100644 --- a/packages/ui/src/views/auth/resetPassword.jsx +++ b/packages/ui/src/views/auth/resetPassword.jsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import { Link, useNavigate, useSearchParams } from 'react-router-dom' @@ -19,6 +19,9 @@ import accountApi from '@/api/account.api' import useNotifier from '@/utils/useNotifier' import { validatePassword } from '@/utils/validation' +// Hooks +import { useError } from '@/store/context/ErrorContext' + // Icons import { IconExclamationCircle, IconX } from '@tabler/icons-react' @@ -70,6 +73,8 @@ const ResetPasswordPage = () => { const [loading, setLoading] = useState(false) const [authErrors, setAuthErrors] = useState([]) + const { authRateLimitError, setAuthRateLimitError } = useError() + const goLogin = () => { navigate('/signin', { replace: true }) } @@ -78,6 +83,7 @@ const ResetPasswordPage = () => { event.preventDefault() const validationErrors = [] setAuthErrors([]) + setAuthRateLimitError(null) if (!tokenVal) { validationErrors.push('Token cannot be left blank!') } @@ -142,6 +148,11 @@ const ResetPasswordPage = () => { } } + useEffect(() => { + setAuthRateLimitError(null) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + return ( <> @@ -155,6 +166,11 @@ const ResetPasswordPage = () => { )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} Reset Password diff --git a/packages/ui/src/views/auth/signIn.jsx b/packages/ui/src/views/auth/signIn.jsx index 1e0a7d3cf..f097cd425 100644 --- a/packages/ui/src/views/auth/signIn.jsx +++ b/packages/ui/src/views/auth/signIn.jsx @@ -14,6 +14,7 @@ import { Input } from '@/ui-component/input/Input' // Hooks import useApi from '@/hooks/useApi' import { useConfig } from '@/store/context/ConfigContext' +import { useError } from '@/store/context/ErrorContext' // API import authApi from '@/api/auth' @@ -62,6 +63,8 @@ const SignInPage = () => { const [showResendButton, setShowResendButton] = useState(false) const [successMessage, setSuccessMessage] = useState('') + const { authRateLimitError, setAuthRateLimitError } = useError() + const loginApi = useApi(authApi.login) const ssoLoginApi = useApi(ssoApi.ssoLogin) const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods) @@ -71,6 +74,7 @@ const SignInPage = () => { const doLogin = (event) => { event.preventDefault() + setAuthRateLimitError(null) setLoading(true) const body = { email: usernameVal, @@ -92,11 +96,12 @@ const SignInPage = () => { useEffect(() => { store.dispatch(logoutSuccess()) + setAuthRateLimitError(null) if (!isOpenSource) { getDefaultProvidersApi.request() } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [setAuthRateLimitError, isOpenSource]) useEffect(() => { // Parse the "user" query parameter from the URL @@ -179,6 +184,11 @@ const SignInPage = () => { {successMessage} )} + {authRateLimitError && ( + } variant='filled' severity='error'> + {authRateLimitError} + + )} {authError && ( } variant='filled' severity='error'> {authError}