Commit 55ac1eab authored by tom goriunov's avatar tom goriunov Committed by GitHub

navigation doesn't work on error pages (#1519)

* navigation doesn't work on error pages

Fixes #1518

* unify API resource thrown errors and disable Sentry logging for them

* test fix
parent c7dc03bb
...@@ -57,6 +57,7 @@ export default function useApiFetch() { ...@@ -57,6 +57,7 @@ export default function useApiFetch() {
}, },
{ {
resource: resource.path, resource: resource.path,
omitSentryErrorLog: true, // disable logging of API errors to Sentry
}, },
); );
}, [ fetch, csrfToken ]); }, [ fetch, csrfToken ]);
......
export default function throwOnAbsentParamError(param: unknown) {
if (!param) {
throw new Error('Required param not provided', { cause: { status: 404 } });
}
}
import type { ResourceError, ResourceName } from 'lib/api/resources';
type Params = ({
isError: true;
error: ResourceError<unknown>;
} | {
isError: false;
error: null;
}) & {
resource?: ResourceName;
}
export const RESOURCE_LOAD_ERROR_MESSAGE = 'Resource load error';
export default function throwOnResourceLoadError({ isError, error, resource }: Params) {
if (isError) {
throw Error(RESOURCE_LOAD_ERROR_MESSAGE, { cause: { ...error, resource } as unknown as Error });
}
}
...@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react'; ...@@ -2,6 +2,7 @@ import * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing'; import { BrowserTracing } from '@sentry/tracing';
import appConfig from 'configs/app'; import appConfig from 'configs/app';
import { RESOURCE_LOAD_ERROR_MESSAGE } from 'lib/errors/throwOnResourceLoadError';
const feature = appConfig.features.sentry; const feature = appConfig.features.sentry;
...@@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => { ...@@ -59,6 +60,9 @@ export const config: Sentry.BrowserOptions | undefined = (() => {
'The quota has been exceeded', 'The quota has been exceeded',
'Attempt to connect to relay via', 'Attempt to connect to relay via',
'WebSocket connection failed for URL: wss://relay.walletconnect.com', 'WebSocket connection failed for URL: wss://relay.walletconnect.com',
// API errors
RESOURCE_LOAD_ERROR_MESSAGE,
], ],
denyUrls: [ denyUrls: [
// Facebook flakiness // Facebook flakiness
......
...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address'; ...@@ -7,6 +7,7 @@ import type { Address as TAddress } from 'types/api/address';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ADDRESS_COUNTERS } from 'stubs/address'; import { ADDRESS_COUNTERS } from 'stubs/address';
import AddressCounterItem from 'ui/address/details/AddressCounterItem'; import AddressCounterItem from 'ui/address/details/AddressCounterItem';
...@@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => { ...@@ -68,7 +69,7 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422; const is422Error = addressQuery.isError && 'status' in addressQuery.error && addressQuery.error.status === 422;
if (addressQuery.isError && is422Error) { if (addressQuery.isError && is422Error) {
throw Error('Address fetch error', { cause: addressQuery.error as unknown as Error }); throwOnResourceLoadError(addressQuery);
} }
if (addressQuery.isError && !is404Error) { if (addressQuery.isError && !is404Error) {
......
...@@ -15,6 +15,7 @@ import type { ResourceError } from 'lib/api/resources'; ...@@ -15,6 +15,7 @@ import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward'; import getBlockReward from 'lib/block/getBlockReward';
import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts'; import { GWEI, WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import { space } from 'lib/html-entities'; import { space } from 'lib/html-entities';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -68,12 +69,8 @@ const BlockDetails = ({ query }: Props) => { ...@@ -68,12 +69,8 @@ const BlockDetails = ({ query }: Props) => {
}, [ data, router ]); }, [ data, router ]);
if (isError) { if (isError) {
if (error?.status === 404) { if (error?.status === 404 || error?.status === 422) {
throw Error('Block not found', { cause: error as unknown as Error }); throwOnResourceLoadError({ isError, error });
}
if (error?.status === 422) {
throw Error('Invalid block number', { cause: error as unknown as Error });
} }
return <DataFetchAlert/>; return <DataFetchAlert/>;
......
...@@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -8,6 +8,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { BLOCK } from 'stubs/block'; import { BLOCK } from 'stubs/block';
...@@ -72,14 +74,6 @@ const BlockPageContent = () => { ...@@ -72,14 +74,6 @@ const BlockPageContent = () => {
}, },
}); });
if (!heightOrHash) {
throw new Error('Block not found', { cause: { status: 404 } });
}
if (blockQuery.isError) {
throw new Error(undefined, { cause: blockQuery.error });
}
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> }, { id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> }, { id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
...@@ -113,6 +107,9 @@ const BlockPageContent = () => { ...@@ -113,6 +107,9 @@ const BlockPageContent = () => {
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
throwOnAbsentParamError(heightOrHash);
throwOnResourceLoadError(blockQuery);
const title = (() => { const title = (() => {
switch (blockQuery.data?.type) { switch (blockQuery.data?.type) {
case 'reorg': case 'reorg':
......
...@@ -5,6 +5,7 @@ import type { SmartContractVerificationMethod } from 'types/api/contract'; ...@@ -5,6 +5,7 @@ import type { SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm'; import ContractVerificationForm from 'ui/contractVerification/ContractVerificationForm';
import useFormConfigQuery from 'ui/contractVerification/useFormConfigQuery'; import useFormConfigQuery from 'ui/contractVerification/useFormConfigQuery';
...@@ -27,9 +28,7 @@ const ContractVerificationForAddress = () => { ...@@ -27,9 +28,7 @@ const ContractVerificationForAddress = () => {
}, },
}); });
if (contractQuery.isError && contractQuery.error.status === 404) { throwOnResourceLoadError(contractQuery);
throw Error('Not found', { cause: contractQuery.error as unknown as Error });
}
const configQuery = useFormConfigQuery(Boolean(hash)); const configQuery = useFormConfigQuery(Boolean(hash));
......
...@@ -8,11 +8,12 @@ import type { CsvExportParams } from 'types/client/address'; ...@@ -8,11 +8,12 @@ import type { CsvExportParams } from 'types/client/address';
import type { ResourceName } from 'lib/api/resources'; import type { ResourceName } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities'; import { nbsp } from 'lib/html-entities';
import CsvExportForm from 'ui/csvExport/CsvExportForm'; import CsvExportForm from 'ui/csvExport/CsvExportForm';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import AddressEntity from 'ui/shared/entities/address/AddressEntity'; import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
...@@ -62,7 +63,8 @@ const CsvExport = () => { ...@@ -62,7 +63,8 @@ const CsvExport = () => {
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const addressHash = router.query.address?.toString() || ''; const addressHash = router.query.address?.toString() || '';
const exportType = router.query.type?.toString() || ''; const exportTypeParam = router.query.type?.toString() || '';
const exportType = isCorrectExportType(exportTypeParam) ? EXPORT_TYPES[exportTypeParam] : null;
const filterTypeFromQuery = router.query.filterType?.toString() || null; const filterTypeFromQuery = router.query.filterType?.toString() || null;
const filterValueFromQuery = router.query.filterValue?.toString(); const filterValueFromQuery = router.query.filterValue?.toString();
...@@ -86,17 +88,20 @@ const CsvExport = () => { ...@@ -86,17 +88,20 @@ const CsvExport = () => {
}; };
}, [ appProps.referrer ]); }, [ appProps.referrer ]);
if (!isCorrectExportType(exportType) || !addressHash || addressQuery.error?.status === 400) { throwOnAbsentParamError(addressHash);
throw Error('Not found', { cause: { status: 404 } }); throwOnAbsentParamError(exportType);
if (!exportType) {
return null;
} }
const filterType = filterTypeFromQuery === EXPORT_TYPES[exportType].filterType ? filterTypeFromQuery : null; const filterType = filterTypeFromQuery === exportType.filterType ? filterTypeFromQuery : null;
const filterValue = (() => { const filterValue = (() => {
if (!filterType || !filterValueFromQuery) { if (!filterType || !filterValueFromQuery) {
return null; return null;
} }
if (EXPORT_TYPES[exportType].filterValues && !EXPORT_TYPES[exportType].filterValues?.includes(filterValueFromQuery)) { if (exportType.filterValues && !exportType.filterValues?.includes(filterValueFromQuery)) {
return null; return null;
} }
...@@ -104,9 +109,7 @@ const CsvExport = () => { ...@@ -104,9 +109,7 @@ const CsvExport = () => {
})(); })();
const content = (() => { const content = (() => {
if (addressQuery.isError) { throwOnResourceLoadError(addressQuery);
return <DataFetchAlert/>;
}
if (addressQuery.isPending) { if (addressQuery.isPending) {
return <ContentLoader/>; return <ContentLoader/>;
...@@ -115,10 +118,10 @@ const CsvExport = () => { ...@@ -115,10 +118,10 @@ const CsvExport = () => {
return ( return (
<CsvExportForm <CsvExportForm
hash={ addressHash } hash={ addressHash }
resource={ EXPORT_TYPES[exportType].resource } resource={ exportType.resource }
filterType={ filterType } filterType={ filterType }
filterValue={ filterValue } filterValue={ filterValue }
fileNameTemplate={ EXPORT_TYPES[exportType].fileNameTemplate } fileNameTemplate={ exportType.fileNameTemplate }
/> />
); );
})(); })();
...@@ -130,7 +133,7 @@ const CsvExport = () => { ...@@ -130,7 +133,7 @@ const CsvExport = () => {
backLink={ backLink } backLink={ backLink }
/> />
<Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap"> <Flex mb={ 10 } whiteSpace="pre-wrap" flexWrap="wrap">
<span>Export { EXPORT_TYPES[exportType].text } for address </span> <span>Export { exportType.text } for address </span>
<AddressEntity <AddressEntity
address={{ hash: addressHash, is_contract: true, implementation_name: null }} address={{ hash: addressHash, is_contract: true, implementation_name: null }}
truncation={ isMobile ? 'constant' : 'dynamic' } truncation={ isMobile ? 'constant' : 'dynamic' }
...@@ -139,7 +142,7 @@ const CsvExport = () => { ...@@ -139,7 +142,7 @@ const CsvExport = () => {
<span>{ nbsp }</span> <span>{ nbsp }</span>
{ filterType && filterValue && <span>with applied filter by { filterType } ({ filterValue }) </span> } { filterType && filterValue && <span>with applied filter by { filterType } ({ filterValue }) </span> }
<span>to CSV file. </span> <span>to CSV file. </span>
<span>Exports are limited to the last 10K { EXPORT_TYPES[exportType].text }.</span> <span>Exports are limited to the last 10K { exportType.text }.</span>
</Flex> </Flex>
{ content } { content }
</> </>
......
...@@ -2,6 +2,7 @@ import { Box } from '@chakra-ui/react'; ...@@ -2,6 +2,7 @@ import { Box } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import config from 'configs/app'; import config from 'configs/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal'; import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu'; import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal'; import MarketplaceDisclaimerModal from 'ui/marketplace/MarketplaceDisclaimerModal';
...@@ -32,9 +33,7 @@ const Marketplace = () => { ...@@ -32,9 +33,7 @@ const Marketplace = () => {
showDisclaimer, showDisclaimer,
} = useMarketplace(); } = useMarketplace();
if (isError) { throwOnResourceLoadError(isError && error ? { isError, error } : { isError: false, error: null });
throw new Error('Unable to get apps list', { cause: error });
}
if (!feature.isEnabled) { if (!feature.isEnabled) {
return null; return null;
......
...@@ -10,6 +10,7 @@ import { route } from 'nextjs-routes'; ...@@ -10,6 +10,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useApiFetch from 'lib/hooks/useFetch'; import useApiFetch from 'lib/hooks/useFetch';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
...@@ -100,7 +101,7 @@ const MarketplaceApp = () => { ...@@ -100,7 +101,7 @@ const MarketplaceApp = () => {
const router = useRouter(); const router = useRouter();
const id = getQueryParamString(router.query.id); const id = getQueryParamString(router.query.id);
const { isPending, isError, error, data } = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({ const query = useQuery<unknown, ResourceError<unknown>, MarketplaceAppOverview>({
queryKey: [ 'marketplace-apps', id ], queryKey: [ 'marketplace-apps', id ],
queryFn: async() => { queryFn: async() => {
const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' }); const result = await apiFetch<Array<MarketplaceAppOverview>, unknown>(configUrl, undefined, { resource: 'marketplace-apps' });
...@@ -116,6 +117,7 @@ const MarketplaceApp = () => { ...@@ -116,6 +117,7 @@ const MarketplaceApp = () => {
}, },
enabled: feature.isEnabled, enabled: feature.isEnabled,
}); });
const { data, isPending } = query;
useEffect(() => { useEffect(() => {
if (data) { if (data) {
...@@ -126,9 +128,7 @@ const MarketplaceApp = () => { ...@@ -126,9 +128,7 @@ const MarketplaceApp = () => {
} }
}, [ data ]); }, [ data ]);
if (isError) { throwOnResourceLoadError(query);
throw new Error('Unable to load app', { cause: error });
}
return ( return (
<DappscoutIframeProvider <DappscoutIframeProvider
......
...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes'; ...@@ -8,6 +8,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { ENS_DOMAIN } from 'stubs/ENS'; import { ENS_DOMAIN } from 'stubs/ENS';
...@@ -42,9 +43,7 @@ const NameDomain = () => { ...@@ -42,9 +43,7 @@ const NameDomain = () => {
const tabIndex = useTabIndexFromQuery(tabs); const tabIndex = useTabIndexFromQuery(tabs);
if (infoQuery.isError) { throwOnResourceLoadError(infoQuery);
throw new Error(undefined, { cause: infoQuery.error });
}
const isLoading = infoQuery.isPlaceholderData; const isLoading = infoQuery.isPlaceholderData;
......
...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -7,6 +7,7 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as metadata from 'lib/metadata'; import * as metadata from 'lib/metadata';
import * as regexp from 'lib/regexp'; import * as regexp from 'lib/regexp';
...@@ -129,9 +130,7 @@ const TokenInstanceContent = () => { ...@@ -129,9 +130,7 @@ const TokenInstanceContent = () => {
) }, ) },
].filter(Boolean); ].filter(Boolean);
if (tokenInstanceQuery.isError) { throwOnResourceLoadError(tokenInstanceQuery);
throw Error('Token instance fetch failed', { cause: tokenInstanceQuery.error });
}
const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenQuery.data?.type }</Tag>; const tokenTag = <Tag isLoading={ tokenInstanceQuery.isPlaceholderData }>{ tokenQuery.data?.type }</Tag>;
......
...@@ -5,6 +5,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types'; ...@@ -5,6 +5,8 @@ import type { RoutedTab } from 'ui/shared/Tabs/types';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import { TX_ZKEVM_L2 } from 'stubs/tx'; import { TX_ZKEVM_L2 } from 'stubs/tx';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
...@@ -41,13 +43,8 @@ const ZkEvmL2TxnBatch = () => { ...@@ -41,13 +43,8 @@ const ZkEvmL2TxnBatch = () => {
}, },
}); });
if (!number) { throwOnAbsentParamError(number);
throw new Error('Tx batch not found', { cause: { status: 404 } }); throwOnResourceLoadError(batchQuery);
}
if (batchQuery.isError) {
throw new Error(undefined, { cause: batchQuery.error });
}
const tabs: Array<RoutedTab> = React.useMemo(() => ([ const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> }, { id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
......
...@@ -37,7 +37,7 @@ test('status code 500', async({ mount }) => { ...@@ -37,7 +37,7 @@ test('status code 500', async({ mount }) => {
}); });
test('invalid tx hash', async({ mount }) => { test('invalid tx hash', async({ mount }) => {
const error = { message: 'Invalid tx hash', cause: { status: 404 } } as Error; const error = { message: 'Invalid tx hash', cause: { status: 422, resource: 'tx' } } as Error;
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<AppError error={ error }/> <AppError error={ error }/>
......
...@@ -3,6 +3,7 @@ import React from 'react'; ...@@ -3,6 +3,7 @@ import React from 'react';
import { route } from 'nextjs-routes'; import { route } from 'nextjs-routes';
import getErrorCause from 'lib/errors/getErrorCause';
import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode'; import getErrorCauseStatusCode from 'lib/errors/getErrorCauseStatusCode';
import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode'; import getErrorObjStatusCode from 'lib/errors/getErrorObjStatusCode';
import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload'; import getResourceErrorPayload from 'lib/errors/getResourceErrorPayload';
...@@ -36,6 +37,7 @@ const ERROR_TEXTS: Record<string, { title: string; text: string }> = { ...@@ -36,6 +37,7 @@ const ERROR_TEXTS: Record<string, { title: string; text: string }> = {
const AppError = ({ error, className }: Props) => { const AppError = ({ error, className }: Props) => {
const content = (() => { const content = (() => {
const resourceErrorPayload = getResourceErrorPayload(error); const resourceErrorPayload = getResourceErrorPayload(error);
const cause = getErrorCause(error);
const messageInPayload = const messageInPayload =
resourceErrorPayload && resourceErrorPayload &&
typeof resourceErrorPayload === 'object' && typeof resourceErrorPayload === 'object' &&
...@@ -43,8 +45,9 @@ const AppError = ({ error, className }: Props) => { ...@@ -43,8 +45,9 @@ const AppError = ({ error, className }: Props) => {
typeof resourceErrorPayload.message === 'string' ? typeof resourceErrorPayload.message === 'string' ?
resourceErrorPayload.message : resourceErrorPayload.message :
undefined; undefined;
const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error);
const isInvalidTxHash = error?.message?.includes('Invalid tx hash'); const isInvalidTxHash = cause && 'resource' in cause && cause.resource === 'tx' && statusCode === 422;
const isBlockConsensus = messageInPayload?.includes('Block lost consensus'); const isBlockConsensus = messageInPayload?.includes('Block lost consensus');
if (isInvalidTxHash) { if (isInvalidTxHash) {
...@@ -62,8 +65,6 @@ const AppError = ({ error, className }: Props) => { ...@@ -62,8 +65,6 @@ const AppError = ({ error, className }: Props) => {
return <AppErrorBlockConsensus hash={ hash }/>; return <AppErrorBlockConsensus hash={ hash }/>;
} }
const statusCode = getErrorCauseStatusCode(error) || getErrorObjStatusCode(error);
switch (statusCode) { switch (statusCode) {
case 429: { case 429: {
return <AppErrorTooManyRequests/>; return <AppErrorTooManyRequests/>;
......
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
interface Props { interface Props {
...@@ -6,21 +8,33 @@ interface Props { ...@@ -6,21 +8,33 @@ interface Props {
onError?: (error: Error) => void; onError?: (error: Error) => void;
} }
interface PropsWithRouter extends Props {
router: NextRouter;
}
interface State { interface State {
hasError: boolean; hasError: boolean;
error?: Error; error?: Error;
errorPathname?: string;
} }
class ErrorBoundary extends React.PureComponent<Props, State> { class ErrorBoundary extends React.PureComponent<PropsWithRouter, State> {
state: State = { state: State = {
hasError: false, hasError: false,
}; };
static getDerivedStateFromError(error: Error) { static getDerivedStateFromProps(props: PropsWithRouter, state: State) {
return { hasError: true, error }; if (state.hasError && state.errorPathname) {
if (props.router.pathname !== state.errorPathname) {
return { hasError: false, error: undefined, errorPathname: undefined };
}
}
return null;
} }
componentDidCatch(error: Error) { componentDidCatch(error: Error) {
this.setState({ hasError: true, error, errorPathname: this.props.router.pathname });
this.props.onError?.(error); this.props.onError?.(error);
} }
...@@ -33,4 +47,9 @@ class ErrorBoundary extends React.PureComponent<Props, State> { ...@@ -33,4 +47,9 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
} }
} }
export default ErrorBoundary; const WrappedErrorBoundary = (props: Props) => {
const router = useRouter();
return <ErrorBoundary { ...props } router={ router }/>;
};
export default React.memo(WrappedErrorBoundary);
...@@ -7,15 +7,15 @@ import { NETWORK_GROUPS } from 'types/networks'; ...@@ -7,15 +7,15 @@ import { NETWORK_GROUPS } from 'types/networks';
import config from 'configs/app'; import config from 'configs/app';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
export default function useNetworkMenu() { export default function useNetworkMenu() {
const { isOpen, onClose, onOpen, onToggle } = useDisclosure(); const { isOpen, onClose, onOpen, onToggle } = useDisclosure();
const apiFetch = useApiFetch(); const fetch = useFetch();
const { isPending, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>({ const { isPending, data } = useQuery<unknown, ResourceError<unknown>, Array<FeaturedNetwork>>({
queryKey: [ 'featured-network' ], queryKey: [ 'featured-network' ],
queryFn: async() => apiFetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }), queryFn: async() => fetch(config.UI.sidebar.featuredNetworks || '', undefined, { resource: 'featured-network' }),
enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen, enabled: Boolean(config.UI.sidebar.featuredNetworks) && isOpen,
staleTime: Infinity, staleTime: Infinity,
}); });
......
...@@ -5,6 +5,8 @@ import type { SmartContract } from 'types/api/contract'; ...@@ -5,6 +5,8 @@ import type { SmartContract } from 'types/api/contract';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnAbsentParamError from 'lib/errors/throwOnAbsentParamError';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import ContentLoader from 'ui/shared/ContentLoader'; import ContentLoader from 'ui/shared/ContentLoader';
interface Props { interface Props {
...@@ -59,23 +61,15 @@ const Sol2UmlDiagram = ({ addressHash }: Props) => { ...@@ -59,23 +61,15 @@ const Sol2UmlDiagram = ({ addressHash }: Props) => {
newWindow?.document.write(image.outerHTML); newWindow?.document.write(image.outerHTML);
}, [ imgUrl ]); }, [ imgUrl ]);
if (!addressHash) { throwOnAbsentParamError(addressHash);
throw Error('Contract address is not provided', { cause: { status: 404 } as unknown as Error }); throwOnResourceLoadError(contractQuery);
} throwOnResourceLoadError(umlQuery);
if (contractQuery.isError) {
throw Error('Contract fetch error', { cause: contractQuery.error as unknown as Error });
}
if (umlQuery.isError) {
throw Error('Uml diagram fetch error', { cause: contractQuery.error as unknown as Error });
}
if (contractQuery.isPending || umlQuery.isPending) { if (contractQuery.isPending || umlQuery.isPending) {
return <ContentLoader/>; return <ContentLoader/>;
} }
if (!umlQuery.data.svg) { if (!umlQuery.data?.svg || !contractQuery.data) {
return <span>No data for visualization</span>; return <span>No data for visualization</span>;
} }
......
...@@ -9,6 +9,7 @@ import type { TokenInfo } from 'types/api/token'; ...@@ -9,6 +9,7 @@ import type { TokenInfo } from 'types/api/token';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import useApiQuery from 'lib/api/useApiQuery'; import useApiQuery from 'lib/api/useApiQuery';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getCurrencyValue from 'lib/getCurrencyValue'; import getCurrencyValue from 'lib/getCurrencyValue';
import { TOKEN_COUNTERS } from 'stubs/token'; import { TOKEN_COUNTERS } from 'stubs/token';
import type { TokenTabs } from 'ui/pages/Token'; import type { TokenTabs } from 'ui/pages/Token';
...@@ -63,9 +64,7 @@ const TokenDetails = ({ tokenQuery }: Props) => { ...@@ -63,9 +64,7 @@ const TokenDetails = ({ tokenQuery }: Props) => {
); );
}, [ tokenCountersQuery.data, tokenCountersQuery.isPlaceholderData, changeUrlAndScroll ]); }, [ tokenCountersQuery.data, tokenCountersQuery.isPlaceholderData, changeUrlAndScroll ]);
if (tokenQuery.isError) { throwOnResourceLoadError(tokenQuery);
throw Error('Token fetch error', { cause: tokenQuery.error as unknown as Error });
}
const { const {
exchange_rate: exchangeRate, exchange_rate: exchangeRate,
......
...@@ -23,6 +23,7 @@ import { route } from 'nextjs-routes'; ...@@ -23,6 +23,7 @@ import { route } from 'nextjs-routes';
import config from 'configs/app'; import config from 'configs/app';
import { WEI, WEI_IN_GWEI } from 'lib/consts'; import { WEI, WEI_IN_GWEI } from 'lib/consts';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle'; import getNetworkValidatorTitle from 'lib/networks/getNetworkValidatorTitle';
import getConfirmationDuration from 'lib/tx/getConfirmationDuration'; import getConfirmationDuration from 'lib/tx/getConfirmationDuration';
import { currencyUnits } from 'lib/units'; import { currencyUnits } from 'lib/units';
...@@ -71,12 +72,8 @@ const TxDetails = () => { ...@@ -71,12 +72,8 @@ const TxDetails = () => {
const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800'); const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
if (isError) { if (isError) {
if (error?.status === 422) { if (error?.status === 422 || error?.status === 404) {
throw Error('Invalid tx hash', { cause: error as unknown as Error }); throwOnResourceLoadError({ isError, error, resource: 'tx' });
}
if (error?.status === 404) {
throw Error('Tx not found', { cause: error as unknown as Error });
} }
return <DataFetchAlert/>; return <DataFetchAlert/>;
......
...@@ -10,6 +10,7 @@ import { route } from 'nextjs-routes'; ...@@ -10,6 +10,7 @@ import { route } from 'nextjs-routes';
import type { ResourceError } from 'lib/api/resources'; import type { ResourceError } from 'lib/api/resources';
import dayjs from 'lib/date/dayjs'; import dayjs from 'lib/date/dayjs';
import throwOnResourceLoadError from 'lib/errors/throwOnResourceLoadError';
import CopyToClipboard from 'ui/shared/CopyToClipboard'; import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert'; import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
...@@ -43,12 +44,8 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => { ...@@ -43,12 +44,8 @@ const ZkEvmL2TxnBatchDetails = ({ query }: Props) => {
}, [ data, router ]); }, [ data, router ]);
if (isError) { if (isError) {
if (error?.status === 404) { if (error?.status === 404 || error?.status === 422) {
throw Error('Tx Batch not found', { cause: error as unknown as Error }); throwOnResourceLoadError({ isError, error });
}
if (error?.status === 422) {
throw Error('Invalid tx batch number', { cause: error as unknown as Error });
} }
return <DataFetchAlert/>; return <DataFetchAlert/>;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment