From 908543b8afab7eb2ae39f70a5454c5104a3460b1 Mon Sep 17 00:00:00 2001 From: tom goriunov Date: Wed, 2 Aug 2023 19:45:06 -0400 Subject: [PATCH] support 429 "Too many requests" API error (#1004) * get client key on 429 error * set key to cookie and pass it in query params * move csrf-token header to useApiFetch * pin host for preview * actual layout * support new changes in API * proxy headers from API * add header to request * remove next.js proxy flag * fix ts * refactor * add tests --- configs/app/config.ts | 1 + docs/ENVS.md | 1 + icons/email-sent.svg | 2 +- icons/error-pages/429.svg | 3 + lib/api/isBodyAllowed.ts | 3 + lib/api/isNeedProxy.ts | 4 + lib/api/resources.ts | 5 ++ lib/api/useApiFetch.tsx | 26 ++++-- lib/hooks/useFetch.tsx | 19 ++--- pages/_app.tsx | 43 +++++++--- pages/api/proxy.ts | 20 +++-- .../AppError/AppErrorTooManyRequests.pw.tsx | 21 +++++ .../AppError/AppErrorTooManyRequests.tsx | 74 ++++++++++++++++++ ...s.pw.tsx_default_default-view-mobile-1.png | Bin 0 -> 23148 bytes ...ts.pw.tsx_mobile_default-view-mobile-1.png | Bin 0 -> 20941 bytes ui/shared/Page/Page.tsx | 2 +- ui/shared/nft/NftMedia.tsx | 6 +- 17 files changed, 185 insertions(+), 45 deletions(-) create mode 100644 icons/error-pages/429.svg create mode 100644 lib/api/isBodyAllowed.ts create mode 100644 ui/shared/AppError/AppErrorTooManyRequests.pw.tsx create mode 100644 ui/shared/AppError/AppErrorTooManyRequests.tsx create mode 100644 ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_default_default-view-mobile-1.png create mode 100644 ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_mobile_default-view-mobile-1.png diff --git a/configs/app/config.ts b/configs/app/config.ts index 89d5b768d2..6892ec07b0 100644 --- a/configs/app/config.ts +++ b/configs/app/config.ts @@ -136,6 +136,7 @@ const config = Object.freeze({ host: appHost, port: appPort, baseUrl, + useNextJsProxy: getEnvValue(process.env.NEXT_PUBLIC_USE_NEXT_JS_PROXY) === 'true', }, ad: { adBannerProvider: getAdBannerProvider(), diff --git a/docs/ENVS.md b/docs/ENVS.md index e16891ca40..392167ce29 100644 --- a/docs/ENVS.md +++ b/docs/ENVS.md @@ -128,6 +128,7 @@ In order to enable "My Account" feature you have to configure following set of v | NEXT_PUBLIC_APP_HOST | `string` | App host | yes | - | `blockscout.com` | | NEXT_PUBLIC_APP_PORT | `number` | Port where app is running | - | `3000` | `3001` | | NEXT_PUBLIC_APP_ENV | `string` | Current app env (e.g development, review or production). Used for Sentry.io configuration | - | equals to `process.env.NODE_ENV` | `production` | +| NEXT_PUBLIC_USE_NEXT_JS_PROXY | `boolean` | Tells the app to proxy all APIs request through the NextJs app. **We strongly advise not to use it in the production environment** | - | `false` | `true` | ## API configuration diff --git a/icons/email-sent.svg b/icons/email-sent.svg index 9fb03dd81a..d31e30f244 100644 --- a/icons/email-sent.svg +++ b/icons/email-sent.svg @@ -1,3 +1,3 @@ - + diff --git a/icons/error-pages/429.svg b/icons/error-pages/429.svg new file mode 100644 index 0000000000..9ae110e192 --- /dev/null +++ b/icons/error-pages/429.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/api/isBodyAllowed.ts b/lib/api/isBodyAllowed.ts new file mode 100644 index 0000000000..aad52fedb7 --- /dev/null +++ b/lib/api/isBodyAllowed.ts @@ -0,0 +1,3 @@ +export default function isBodyAllowed(method: string | undefined | null) { + return method && ![ 'GET', 'HEAD' ].includes(method); +} diff --git a/lib/api/isNeedProxy.ts b/lib/api/isNeedProxy.ts index e2ae76fdb8..a348a196f7 100644 --- a/lib/api/isNeedProxy.ts +++ b/lib/api/isNeedProxy.ts @@ -5,5 +5,9 @@ import appConfig from 'configs/app/config'; // unsuccessfully tried different ways, even custom local dev domain // so for local development we have to use next.js api as proxy server export default function isNeedProxy() { + if (appConfig.app.useNextJsProxy) { + return true; + } + return appConfig.app.host === 'localhost' && appConfig.app.host !== appConfig.api.host; } diff --git a/lib/api/resources.ts b/lib/api/resources.ts index ffa751f06c..7a9ed2ce7c 100644 --- a/lib/api/resources.ts +++ b/lib/api/resources.ts @@ -474,6 +474,11 @@ export const RESOURCES = { path: '/api/v2/config/backend-version', }, + // OTHER + api_v2_key: { + path: '/api/v2/key', + }, + // DEPRECATED old_api: { path: '/api', diff --git a/lib/api/useApiFetch.tsx b/lib/api/useApiFetch.tsx index f879b982e9..71a0cf93a0 100644 --- a/lib/api/useApiFetch.tsx +++ b/lib/api/useApiFetch.tsx @@ -1,6 +1,12 @@ +import { useQueryClient } from '@tanstack/react-query'; +import _pickBy from 'lodash/pickBy'; import React from 'react'; +import type { CsrfData } from 'types/client/account'; + +import isBodyAllowed from 'lib/api/isBodyAllowed'; import isNeedProxy from 'lib/api/isNeedProxy'; +import { getResourceKey } from 'lib/api/useApiQuery'; import * as cookies from 'lib/cookies'; import type { Params as FetchParams } from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch'; @@ -17,28 +23,34 @@ export interface Params { export default function useApiFetch() { const fetch = useFetch(); + const queryClient = useQueryClient(); + const { token: csrfToken } = queryClient.getQueryData(getResourceKey('csrf')) || {}; return React.useCallback(( resourceName: R, { pathParams, queryParams, fetchParams }: Params = {}, ) => { + const apiToken = cookies.get(cookies.NAMES.API_TOKEN); + const resource: ApiResource = RESOURCES[resourceName]; const url = buildUrl(resourceName, pathParams, queryParams); + const withBody = isBodyAllowed(fetchParams?.method); + const headers = _pickBy({ + 'x-endpoint': resource.endpoint && isNeedProxy() ? resource.endpoint : undefined, + Authorization: resource.endpoint && resource.needAuth ? apiToken : undefined, + 'x-csrf-token': withBody && csrfToken ? csrfToken : undefined, + }, Boolean) as HeadersInit; + return fetch( url, { credentials: 'include', - ...(resource.endpoint ? { - headers: { - ...(isNeedProxy() ? { 'x-endpoint': resource.endpoint } : {}), - ...(resource.needAuth ? { Authorization: cookies.get(cookies.NAMES.API_TOKEN) } : {}), - }, - } : {}), + headers, ...fetchParams, }, { resource: resource.path, }, ); - }, [ fetch ]); + }, [ fetch, csrfToken ]); } diff --git a/lib/hooks/useFetch.tsx b/lib/hooks/useFetch.tsx index 5f77ec3a1a..b399b36992 100644 --- a/lib/hooks/useFetch.tsx +++ b/lib/hooks/useFetch.tsx @@ -1,11 +1,8 @@ import * as Sentry from '@sentry/react'; -import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; -import type { CsrfData } from 'types/client/account'; - +import isBodyAllowed from 'lib/api/isBodyAllowed'; import type { ResourceError, ResourcePath } from 'lib/api/resources'; -import { getResourceKey } from 'lib/api/useApiQuery'; export interface Params { method?: RequestInit['method']; @@ -20,16 +17,13 @@ interface Meta { } export default function useFetch() { - const queryClient = useQueryClient(); - const { token } = queryClient.getQueryData(getResourceKey('csrf')) || {}; - return React.useCallback((path: string, params?: Params, meta?: Meta): Promise> => { const _body = params?.body; const isFormData = _body instanceof FormData; - const isBodyAllowed = params?.method && ![ 'GET', 'HEAD' ].includes(params.method); + const withBody = isBodyAllowed(params?.method); const body: FormData | string | undefined = (() => { - if (!isBodyAllowed) { + if (!withBody) { return; } @@ -44,8 +38,7 @@ export default function useFetch() { ...params, body, headers: { - ...(isBodyAllowed && !isFormData ? { 'Content-type': 'application/json' } : undefined), - ...(isBodyAllowed && token ? { 'x-csrf-token': token } : undefined), + ...(withBody && !isFormData ? { 'Content-type': 'application/json' } : undefined), ...params?.headers, }, }; @@ -56,7 +49,7 @@ export default function useFetch() { status: response.status, statusText: response.statusText, }; - Sentry.captureException(new Error('Client fetch failed'), { extra: { ...error, ...meta }, tags: { source: 'api_fetch' } }); + Sentry.captureException(new Error('Client fetch failed'), { extra: { ...error, ...meta }, tags: { source: 'fetch' } }); return response.json().then( (jsonError) => Promise.reject({ @@ -73,5 +66,5 @@ export default function useFetch() { return response.json() as Promise; } }); - }, [ token ]); + }, [ ]); } diff --git a/pages/_app.tsx b/pages/_app.tsx index 4eebabb0ac..79269f938a 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,3 +1,4 @@ +import type { ChakraProps } from '@chakra-ui/react'; import * as Sentry from '@sentry/react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; @@ -5,15 +6,17 @@ import type { AppProps } from 'next/app'; import React, { useState } from 'react'; import appConfig from 'configs/app/config'; -import type { ResourceError } from 'lib/api/resources'; import { AppContextProvider } from 'lib/contexts/app'; import { ChakraProvider } from 'lib/contexts/chakra'; import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; +import getErrorObjPayload from 'lib/errors/getErrorObjPayload'; +import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import useConfigSentry from 'lib/hooks/useConfigSentry'; import { SocketProvider } from 'lib/socket/context'; import theme from 'theme'; import AppError from 'ui/shared/AppError/AppError'; +import AppErrorTooManyRequests from 'ui/shared/AppError/AppErrorTooManyRequests'; import ErrorBoundary from 'ui/shared/ErrorBoundary'; import GoogleAnalytics from 'ui/shared/GoogleAnalytics'; @@ -27,33 +30,47 @@ function MyApp({ Component, pageProps }: AppProps) { defaultOptions: { queries: { refetchOnWindowFocus: false, - retry: (failureCount, _error) => { - const error = _error as ResourceError<{ status: number }>; - const status = error?.payload?.status || error?.status; + retry: (failureCount, error) => { + const errorPayload = getErrorObjPayload<{ status: number }>(error); + const status = errorPayload?.status || getErrorObjStatusCode(error); if (status && status >= 400 && status < 500) { // don't do retry for client error responses return false; } - return failureCount < 2; }, + useErrorBoundary: (error) => { + const status = getErrorObjStatusCode(error); + // don't catch error for "Too many requests" response + return status === 429; + }, }, }, })); const renderErrorScreen = React.useCallback((error?: Error) => { - const statusCode = getErrorCauseStatusCode(error); + const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error); + + const styles: ChakraProps = { + h: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + justifyContent: 'center', + width: 'fit-content', + maxW: '800px', + margin: '0 auto', + p: { base: 4, lg: 0 }, + }; + + if (statusCode === 429) { + return ; + } return ( ); }, []); diff --git a/pages/api/proxy.ts b/pages/api/proxy.ts index f40439ffa6..3e30983f10 100644 --- a/pages/api/proxy.ts +++ b/pages/api/proxy.ts @@ -5,22 +5,26 @@ import type { NextApiRequest, NextApiResponse } from 'next'; import appConfig from 'configs/app/config'; import fetchFactory from 'lib/api/nodeFetch'; -const handler = async(_req: NextApiRequest, res: NextApiResponse) => { - if (!_req.url) { - res.status(500).json({ error: 'no url provided' }); +const handler = async(nextReq: NextApiRequest, nextRes: NextApiResponse) => { + if (!nextReq.url) { + nextRes.status(500).json({ error: 'no url provided' }); return; } const url = new URL( - _req.url.replace(/^\/node-api\/proxy/, ''), - _req.headers['x-endpoint']?.toString() || appConfig.api.endpoint, + nextReq.url.replace(/^\/node-api\/proxy/, ''), + nextReq.headers['x-endpoint']?.toString() || appConfig.api.endpoint, ); - const response = await fetchFactory(_req)( + const apiRes = await fetchFactory(nextReq)( url.toString(), - _pickBy(_pick(_req, [ 'body', 'method' ]), Boolean), + _pickBy(_pick(nextReq, [ 'body', 'method' ]), Boolean), ); - res.status(response.status).send(response.body); + // proxy some headers from API + nextRes.setHeader('x-request-id', apiRes.headers.get('x-request-id') || ''); + nextRes.setHeader('set-cookie', apiRes.headers.get('set-cookie') || ''); + + nextRes.status(apiRes.status).send(apiRes.body); }; export default handler; diff --git a/ui/shared/AppError/AppErrorTooManyRequests.pw.tsx b/ui/shared/AppError/AppErrorTooManyRequests.pw.tsx new file mode 100644 index 0000000000..5647b281d2 --- /dev/null +++ b/ui/shared/AppError/AppErrorTooManyRequests.pw.tsx @@ -0,0 +1,21 @@ +import { test, expect } from '@playwright/experimental-ct-react'; +import React from 'react'; + +import TestApp from 'playwright/TestApp'; +import * as configs from 'playwright/utils/configs'; + +import AppErrorTooManyRequests from './AppErrorTooManyRequests'; + +test('default view +@mobile', async({ mount, page }) => { + const component = await mount( + + + , + ); + await page.waitForResponse('https://www.google.com/recaptcha/api2/**'); + + await expect(component).toHaveScreenshot({ + mask: [ page.locator('.recaptcha') ], + maskColor: configs.maskColor, + }); +}); diff --git a/ui/shared/AppError/AppErrorTooManyRequests.tsx b/ui/shared/AppError/AppErrorTooManyRequests.tsx new file mode 100644 index 0000000000..bc51becaf7 --- /dev/null +++ b/ui/shared/AppError/AppErrorTooManyRequests.tsx @@ -0,0 +1,74 @@ +import { Box, Heading, Icon, Text, chakra } from '@chakra-ui/react'; +import React from 'react'; +import ReCaptcha from 'react-google-recaptcha'; + +import appConfig from 'configs/app/config'; +import icon429 from 'icons/error-pages/429.svg'; +import buildUrl from 'lib/api/buildUrl'; +import useFetch from 'lib/hooks/useFetch'; +import useToast from 'lib/hooks/useToast'; + +interface Props { + className?: string; +} + +const AppErrorTooManyRequests = ({ className }: Props) => { + const toast = useToast(); + const fetch = useFetch(); + + const handleReCaptchaChange = React.useCallback(async(token: string | null) => { + + if (token) { + try { + const url = buildUrl('api_v2_key'); + + await fetch(url, { + method: 'POST', + body: { recaptcha_response: token }, + credentials: 'include', + }, { + resource: 'api_v2_key', + }); + + window.location.reload(); + + } catch (error) { + toast({ + position: 'top-right', + title: 'Error', + description: 'Unable to get client key.', + status: 'error', + variant: 'subtle', + isClosable: true, + }); + } + } + }, [ toast, fetch ]); + + return ( + + + Too many requests + + You have exceeded the request rate for a given time period. Please reduce the number of requests and try again soon. + + { appConfig.reCaptcha.siteKey && ( + + ) } + + ); +}; + +export default chakra(AppErrorTooManyRequests); diff --git a/ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_default_default-view-mobile-1.png b/ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_default_default-view-mobile-1.png new file mode 100644 index 0000000000000000000000000000000000000000..337f3d44eacfdeced6d66f002aa06d01d8842d16 GIT binary patch literal 23148 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 literal 0 HcmV?d00001 diff --git a/ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_mobile_default-view-mobile-1.png b/ui/shared/AppError/__screenshots__/AppErrorTooManyRequests.pw.tsx_mobile_default-view-mobile-1.png new file mode 100644 index 0000000000000000000000000000000000000000..df9ec1f57a08b7681642bf4772e02e6d8ebfc608 GIT binary patch literal 20941 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 { const [ type, setType ] = React.useState(!animationUrl ? 'image' : undefined); + const fetch = useFetch(); React.useEffect(() => { if (!animationUrl || isLoading) { @@ -38,7 +41,6 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => { const url = route({ pathname: '/node-api/media-type' as StaticRoute<'/api/media-type'>['pathname'], query: { url: animationUrl } }); fetch(url) - .then((response) => response.json()) .then((_data) => { const data = _data as { type: MediaType | undefined }; setType(data.type || 'image'); @@ -47,7 +49,7 @@ const NftMedia = ({ imageUrl, animationUrl, className, isLoading }: Props) => { setType('image'); }); - }, [ animationUrl, isLoading ]); + }, [ animationUrl, isLoading, fetch ]); if (!type || isLoading) { return (