feat: handle 429 errors and redirect to rate-limited page (#5440)

* 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 <taraka.vishnumolakala@workday.com>
Co-authored-by: Henry Heng <henryheng@flowiseai.com>
Co-authored-by: Henry <hzj94@hotmail.com>

* 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 <henryheng@flowiseai.com>
Co-authored-by: Taraka Vishnumolakala <tvishnumolakala@gmail.com>
Co-authored-by: taraka-vishnumolakala <taraka.vishnumolakala@workday.com>
Co-authored-by: Henry <hzj94@hotmail.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Ilango 2025-11-27 16:53:49 +05:30 committed by GitHub
parent 7cc2c13694
commit 4e92db6910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 134 additions and 8 deletions

View File

@ -70,7 +70,7 @@ export const checkUsageLimit = async (
if (limit === -1) return if (limit === -1) return
if (currentUsage > limit) { 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 (predictionsLimit === -1) return
if (currentPredictions >= predictionsLimit) { if (currentPredictions >= predictionsLimit) {
throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Predictions limit exceeded') throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Predictions limit exceeded')
} }
return { return {
@ -161,7 +161,7 @@ export const checkStorage = async (orgId: string, subscriptionId: string, usageC
if (storageLimit === -1) return if (storageLimit === -1) return
if (currentStorageUsage >= storageLimit) { if (currentStorageUsage >= storageLimit) {
throw new InternalFlowiseError(StatusCodes.TOO_MANY_REQUESTS, 'Storage limit exceeded') throw new InternalFlowiseError(StatusCodes.PAYMENT_REQUIRED, 'Storage limit exceeded')
} }
return { return {

View File

@ -10,6 +10,7 @@ const VerifyEmailPage = Loadable(lazy(() => import('@/views/auth/verify-email'))
const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword'))) const ForgotPasswordPage = Loadable(lazy(() => import('@/views/auth/forgotPassword')))
const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword'))) const ResetPasswordPage = Loadable(lazy(() => import('@/views/auth/resetPassword')))
const UnauthorizedPage = Loadable(lazy(() => import('@/views/auth/unauthorized'))) 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 OrganizationSetupPage = Loadable(lazy(() => import('@/views/organization/index')))
const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired'))) const LicenseExpiredPage = Loadable(lazy(() => import('@/views/auth/expired')))
@ -45,6 +46,10 @@ const AuthRoutes = {
path: '/unauthorized', path: '/unauthorized',
element: <UnauthorizedPage /> element: <UnauthorizedPage />
}, },
{
path: '/rate-limited',
element: <RateLimitedPage />
},
{ {
path: '/organization-setup', path: '/organization-setup',
element: <OrganizationSetupPage /> element: <OrganizationSetupPage />

View File

@ -10,11 +10,29 @@ const ErrorContext = createContext()
export const ErrorProvider = ({ children }) => { export const ErrorProvider = ({ children }) => {
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [authRateLimitError, setAuthRateLimitError] = useState(null)
const navigate = useNavigate() const navigate = useNavigate()
const handleError = async (err) => { const handleError = async (err) => {
console.error(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') navigate('/unauthorized')
} else if (err?.response?.status === 401) { } else if (err?.response?.status === 401) {
if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) { if (ErrorMessage.INVALID_MISSING_TOKEN === err?.response?.data?.message) {
@ -44,7 +62,9 @@ export const ErrorProvider = ({ children }) => {
value={{ value={{
error, error,
setError, setError,
handleError handleError,
authRateLimitError,
setAuthRateLimitError
}} }}
> >
{children} {children}

View File

@ -16,6 +16,7 @@ import accountApi from '@/api/account.api'
// Hooks // Hooks
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext' import { useConfig } from '@/store/context/ConfigContext'
import { useError } from '@/store/context/ErrorContext'
// utils // utils
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
@ -41,10 +42,13 @@ const ForgotPasswordPage = () => {
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const [responseMsg, setResponseMsg] = useState(undefined) const [responseMsg, setResponseMsg] = useState(undefined)
const { authRateLimitError, setAuthRateLimitError } = useError()
const forgotPasswordApi = useApi(accountApi.forgotPassword) const forgotPasswordApi = useApi(accountApi.forgotPassword)
const sendResetRequest = async (event) => { const sendResetRequest = async (event) => {
event.preventDefault() event.preventDefault()
setAuthRateLimitError(null)
const body = { const body = {
user: { user: {
email: usernameVal email: usernameVal
@ -54,6 +58,11 @@ const ForgotPasswordPage = () => {
await forgotPasswordApi.request(body) await forgotPasswordApi.request(body)
} }
useEffect(() => {
setAuthRateLimitError(null)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [setAuthRateLimitError])
useEffect(() => { useEffect(() => {
if (forgotPasswordApi.error) { if (forgotPasswordApi.error) {
const errMessage = const errMessage =
@ -89,6 +98,11 @@ const ForgotPasswordPage = () => {
{responseMsg.msg} {responseMsg.msg}
</Alert> </Alert>
)} )}
{authRateLimitError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authRateLimitError}
</Alert>
)}
{responseMsg && responseMsg?.type !== 'error' && ( {responseMsg && responseMsg?.type !== 'error' && (
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'> <Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
{responseMsg.msg} {responseMsg.msg}

View File

@ -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 (
<MainCard>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: 'calc(100vh - 210px)'
}}
>
<Stack
sx={{
alignItems: 'center',
justifyContent: 'center',
maxWidth: '500px'
}}
flexDirection='column'
>
<Box sx={{ p: 2, height: 'auto' }}>
<img style={{ objectFit: 'cover', height: '20vh', width: 'auto' }} src={unauthorizedSVG} alt='rateLimitedSVG' />
</Box>
<Typography sx={{ mb: 2 }} variant='h4' component='div' fontWeight='bold'>
429 Too Many Requests
</Typography>
<Typography variant='body1' component='div' sx={{ mb: 2, textAlign: 'center' }}>
{`You have made too many requests in a short period of time. Please wait ${retryAfter}s before trying again.`}
</Typography>
<Link to='/'>
<Button variant='contained' color='primary'>
Back to Home
</Button>
</Link>
</Stack>
</Box>
</MainCard>
)
}
export default RateLimitedPage

View File

@ -18,6 +18,7 @@ import ssoApi from '@/api/sso'
// Hooks // Hooks
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext' import { useConfig } from '@/store/context/ConfigContext'
import { useError } from '@/store/context/ErrorContext'
// utils // utils
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
@ -111,7 +112,9 @@ const RegisterPage = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [authError, setAuthError] = useState('') const [authError, setAuthError] = useState('')
const [successMsg, setSuccessMsg] = useState(undefined) const [successMsg, setSuccessMsg] = useState('')
const { authRateLimitError, setAuthRateLimitError } = useError()
const registerApi = useApi(accountApi.registerAccount) const registerApi = useApi(accountApi.registerAccount)
const ssoLoginApi = useApi(ssoApi.ssoLogin) const ssoLoginApi = useApi(ssoApi.ssoLogin)
@ -120,6 +123,7 @@ const RegisterPage = () => {
const register = async (event) => { const register = async (event) => {
event.preventDefault() event.preventDefault()
setAuthRateLimitError(null)
if (isEnterpriseLicensed) { if (isEnterpriseLicensed) {
const result = RegisterEnterpriseUserSchema.safeParse({ const result = RegisterEnterpriseUserSchema.safeParse({
username, username,
@ -192,6 +196,7 @@ const RegisterPage = () => {
}, [registerApi.error]) }, [registerApi.error])
useEffect(() => { useEffect(() => {
setAuthRateLimitError(null)
if (!isOpenSource) { if (!isOpenSource) {
getDefaultProvidersApi.request() getDefaultProvidersApi.request()
} }
@ -274,6 +279,11 @@ const RegisterPage = () => {
)} )}
</Alert> </Alert>
)} )}
{authRateLimitError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authRateLimitError}
</Alert>
)}
{successMsg && ( {successMsg && (
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'> <Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
{successMsg} {successMsg}

View File

@ -1,4 +1,4 @@
import { useState } from 'react' import { useEffect, useState } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { Link, useNavigate, useSearchParams } from 'react-router-dom' import { Link, useNavigate, useSearchParams } from 'react-router-dom'
@ -19,6 +19,9 @@ import accountApi from '@/api/account.api'
import useNotifier from '@/utils/useNotifier' import useNotifier from '@/utils/useNotifier'
import { validatePassword } from '@/utils/validation' import { validatePassword } from '@/utils/validation'
// Hooks
import { useError } from '@/store/context/ErrorContext'
// Icons // Icons
import { IconExclamationCircle, IconX } from '@tabler/icons-react' import { IconExclamationCircle, IconX } from '@tabler/icons-react'
@ -70,6 +73,8 @@ const ResetPasswordPage = () => {
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [authErrors, setAuthErrors] = useState([]) const [authErrors, setAuthErrors] = useState([])
const { authRateLimitError, setAuthRateLimitError } = useError()
const goLogin = () => { const goLogin = () => {
navigate('/signin', { replace: true }) navigate('/signin', { replace: true })
} }
@ -78,6 +83,7 @@ const ResetPasswordPage = () => {
event.preventDefault() event.preventDefault()
const validationErrors = [] const validationErrors = []
setAuthErrors([]) setAuthErrors([])
setAuthRateLimitError(null)
if (!tokenVal) { if (!tokenVal) {
validationErrors.push('Token cannot be left blank!') 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 ( return (
<> <>
<MainCard> <MainCard>
@ -155,6 +166,11 @@ const ResetPasswordPage = () => {
</ul> </ul>
</Alert> </Alert>
)} )}
{authRateLimitError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authRateLimitError}
</Alert>
)}
<Stack sx={{ gap: 1 }}> <Stack sx={{ gap: 1 }}>
<Typography variant='h1'>Reset Password</Typography> <Typography variant='h1'>Reset Password</Typography>
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}> <Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>

View File

@ -14,6 +14,7 @@ import { Input } from '@/ui-component/input/Input'
// Hooks // Hooks
import useApi from '@/hooks/useApi' import useApi from '@/hooks/useApi'
import { useConfig } from '@/store/context/ConfigContext' import { useConfig } from '@/store/context/ConfigContext'
import { useError } from '@/store/context/ErrorContext'
// API // API
import authApi from '@/api/auth' import authApi from '@/api/auth'
@ -62,6 +63,8 @@ const SignInPage = () => {
const [showResendButton, setShowResendButton] = useState(false) const [showResendButton, setShowResendButton] = useState(false)
const [successMessage, setSuccessMessage] = useState('') const [successMessage, setSuccessMessage] = useState('')
const { authRateLimitError, setAuthRateLimitError } = useError()
const loginApi = useApi(authApi.login) const loginApi = useApi(authApi.login)
const ssoLoginApi = useApi(ssoApi.ssoLogin) const ssoLoginApi = useApi(ssoApi.ssoLogin)
const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods) const getDefaultProvidersApi = useApi(loginMethodApi.getDefaultLoginMethods)
@ -71,6 +74,7 @@ const SignInPage = () => {
const doLogin = (event) => { const doLogin = (event) => {
event.preventDefault() event.preventDefault()
setAuthRateLimitError(null)
setLoading(true) setLoading(true)
const body = { const body = {
email: usernameVal, email: usernameVal,
@ -92,11 +96,12 @@ const SignInPage = () => {
useEffect(() => { useEffect(() => {
store.dispatch(logoutSuccess()) store.dispatch(logoutSuccess())
setAuthRateLimitError(null)
if (!isOpenSource) { if (!isOpenSource) {
getDefaultProvidersApi.request() getDefaultProvidersApi.request()
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []) }, [setAuthRateLimitError, isOpenSource])
useEffect(() => { useEffect(() => {
// Parse the "user" query parameter from the URL // Parse the "user" query parameter from the URL
@ -179,6 +184,11 @@ const SignInPage = () => {
{successMessage} {successMessage}
</Alert> </Alert>
)} )}
{authRateLimitError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authRateLimitError}
</Alert>
)}
{authError && ( {authError && (
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'> <Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
{authError} {authError}