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:
parent
7cc2c13694
commit
4e92db6910
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: <UnauthorizedPage />
|
||||
},
|
||||
{
|
||||
path: '/rate-limited',
|
||||
element: <RateLimitedPage />
|
||||
},
|
||||
{
|
||||
path: '/organization-setup',
|
||||
element: <OrganizationSetupPage />
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</Alert>
|
||||
)}
|
||||
{authRateLimitError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authRateLimitError}
|
||||
</Alert>
|
||||
)}
|
||||
{responseMsg && responseMsg?.type !== 'error' && (
|
||||
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
|
||||
{responseMsg.msg}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 = () => {
|
|||
)}
|
||||
</Alert>
|
||||
)}
|
||||
{authRateLimitError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authRateLimitError}
|
||||
</Alert>
|
||||
)}
|
||||
{successMsg && (
|
||||
<Alert icon={<IconCircleCheck />} variant='filled' severity='success'>
|
||||
{successMsg}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<MainCard>
|
||||
|
|
@ -155,6 +166,11 @@ const ResetPasswordPage = () => {
|
|||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
{authRateLimitError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authRateLimitError}
|
||||
</Alert>
|
||||
)}
|
||||
<Stack sx={{ gap: 1 }}>
|
||||
<Typography variant='h1'>Reset Password</Typography>
|
||||
<Typography variant='body2' sx={{ color: theme.palette.grey[600] }}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
</Alert>
|
||||
)}
|
||||
{authRateLimitError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authRateLimitError}
|
||||
</Alert>
|
||||
)}
|
||||
{authError && (
|
||||
<Alert icon={<IconExclamationCircle />} variant='filled' severity='error'>
|
||||
{authError}
|
||||
|
|
|
|||
Loading…
Reference in New Issue