From 166c6fef0e3ae23b9012a2449ea7692e4f5459bb Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Fri, 15 Oct 2021 09:21:38 +0200 Subject: [PATCH] Write a generic http thrower for status > 299 (#405) * Write a generic http thrower for status > 299 * Perform location reload if user is no longer authorized, i.e if status === 401 --- frontend/src/component/App.tsx | 65 +++++++++++++------ .../FeatureOverviewMetrics.tsx | 2 +- .../api/getters/httpErrorResponseHandler.ts | 21 ++++++ .../api/getters/useApiTokens/useApiTokens.ts | 3 +- .../useEnvironments/useEnvironments.ts | 3 +- .../api/getters/useFeature/useFeature.ts | 21 +----- .../useFeatureStrategy/useFeatureStrategy.ts | 3 +- .../useFeatureTypes/useFeatureTypes.ts | 3 +- .../useHealthReport/useHealthReport.ts | 3 +- .../getters/useProject/getProjectFetcher.ts | 3 +- .../api/getters/useProjects/useProjects.ts | 3 +- .../useResetPassword/useResetPassword.ts | 3 +- .../getters/useStrategies/useStrategies.ts | 3 +- .../api/getters/useTagTypes/useTagTypes.ts | 3 +- .../src/hooks/api/getters/useTags/useTags.ts | 3 +- .../api/getters/useUiConfig/useUiConfig.ts | 3 +- .../useUnleashContext/useUnleashContext.ts | 3 +- .../src/hooks/api/getters/useUser/useUser.ts | 3 +- .../hooks/api/getters/useUsers/useUsers.ts | 3 +- 19 files changed, 99 insertions(+), 55 deletions(-) create mode 100644 frontend/src/hooks/api/getters/httpErrorResponseHandler.ts diff --git a/frontend/src/component/App.tsx b/frontend/src/component/App.tsx index 7e4b9d90ae..8180e4a393 100644 --- a/frontend/src/component/App.tsx +++ b/frontend/src/component/App.tsx @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { Route, Switch, Redirect } from 'react-router-dom'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { RouteComponentProps } from 'react-router'; import ProtectedRoute from './common/ProtectedRoute/ProtectedRoute'; @@ -13,6 +13,9 @@ import IAuthStatus from '../interfaces/user'; import { useEffect } from 'react'; import NotFound from './common/NotFound/NotFound'; import Feedback from './common/Feedback'; +import { SWRConfig } from 'swr'; +import useToast from '../hooks/useToast'; + interface IAppProps extends RouteComponentProps { user: IAuthStatus; fetchUiBootstrap: any; @@ -20,6 +23,7 @@ interface IAppProps extends RouteComponentProps { } const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => { + const { toast, setToastData } = useToast(); useEffect(() => { fetchUiBootstrap(); /* eslint-disable-next-line */ @@ -71,27 +75,46 @@ const App = ({ location, user, fetchUiBootstrap, feedback }: IAppProps) => { }; return ( -
- - - { + if (!isUnauthorized()) { + if (error.status === 401) { + // If we've been in an authorized state, + // but cookie has been deleted (server or client side, + // perform a window reload to reload app + window.location.reload(); + } + setToastData({ + show: true, + type: 'error', + text: error.message, + }); + } + }, + }}> +
+ + + + {renderMainLayoutRoutes()} + {renderStandaloneRoutes()} + + + + - {renderMainLayoutRoutes()} - {renderStandaloneRoutes()} - - - - - -
+
+ {toast} +
+ ); }; // Set state to any for now, to avoid typing up entire state object while converting to tsx. diff --git a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx index 629f93e178..767ecd8741 100644 --- a/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx +++ b/frontend/src/component/feature/FeatureView2/FeatureOverview/FeatureOverviewMetrics/FeatureOverviewMetrics.tsx @@ -63,7 +63,7 @@ const FeatureOverviewMetrics = () => { }); } - /* We display maxium three environments metrics */ + /* We display maximum three environments metrics */ if (featureMetrics.length >= 3) { return featureMetrics.slice(0, 3).map((metric, index) => { if (index === 0) { diff --git a/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts new file mode 100644 index 0000000000..4222588146 --- /dev/null +++ b/frontend/src/hooks/api/getters/httpErrorResponseHandler.ts @@ -0,0 +1,21 @@ +const handleErrorResponses = (target: string) => async (res: Response) => { + if (!res.ok) { + const error = new Error(`An error occurred while trying to get ${target}`); + // Try to resolve body, but don't rethrow res.json is not a function + try { + // @ts-ignore + error.info = await res.json(); + } catch (e) { + // @ts-ignore + error.info = {}; + } + // @ts-ignore + error.status = res.status; + // @ts-ignore + error.statusText = res.statusText; + throw error; + } + return res; +} + +export default handleErrorResponses; diff --git a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts index 7c9b891a73..7192511d08 100644 --- a/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts +++ b/frontend/src/hooks/api/getters/useApiTokens/useApiTokens.ts @@ -1,13 +1,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useApiTokens = () => { const fetcher = async () => { const path = formatApiPath(`api/admin/api-tokens`); const res = await fetch(path, { method: 'GET', - }); + }).then(handleErrorResponses('Api tokens')); return res.json(); }; diff --git a/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts b/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts index 864abc33a5..4d71e95a00 100644 --- a/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts +++ b/frontend/src/hooks/api/getters/useEnvironments/useEnvironments.ts @@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { IEnvironmentResponse } from '../../../../interfaces/environments'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; export const ENVIRONMENT_CACHE_KEY = `api/admin/environments`; @@ -10,7 +11,7 @@ const useEnvironments = () => { const path = formatApiPath(`api/admin/environments`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Environments')).then(res => res.json()); }; const { data, error } = useSWR( diff --git a/frontend/src/hooks/api/getters/useFeature/useFeature.ts b/frontend/src/hooks/api/getters/useFeature/useFeature.ts index 8c96c83938..acad0a6260 100644 --- a/frontend/src/hooks/api/getters/useFeature/useFeature.ts +++ b/frontend/src/hooks/api/getters/useFeature/useFeature.ts @@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IFeatureToggle } from '../../../../interfaces/featureToggle'; import { defaultFeature } from './defaultFeature'; +import handleErrorResponses from '../httpErrorResponseHandler'; interface IUseFeatureOptions { refreshInterval?: number; @@ -21,25 +22,9 @@ const useFeature = ( const path = formatApiPath( `api/admin/projects/${projectId}/features/${id}` ); - - const res = await fetch(path, { + return fetch(path, { method: 'GET', - }); - - - // If the status code is not in the range 200-299, - // we still try to parse and throw it. - if (!res.ok) { - const error = new Error('An error occurred while fetching the data.') - // Attach extra info to the error object. - // @ts-ignore - error.info = await res.json(); - // @ts-ignore - error.status = res.status; - throw error; - } - - return res.json() + }).then(handleErrorResponses('Feature toggle data')).then(res => res.json()); }; const FEATURE_CACHE_KEY = `api/admin/projects/${projectId}/features/${id}`; diff --git a/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts index 60a870ec00..673143a389 100644 --- a/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts +++ b/frontend/src/hooks/api/getters/useFeatureStrategy/useFeatureStrategy.ts @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IFeatureStrategy } from '../../../../interfaces/strategy'; +import handleErrorResponses from '../httpErrorResponseHandler'; interface IUseFeatureOptions { refreshInterval?: number; @@ -25,7 +26,7 @@ const useFeatureStrategy = ( ); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses(`Strategies for ${featureId}`)).then(res => res.json()); }; const FEATURE_STRATEGY_CACHE_KEY = strategyId; diff --git a/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts index 2962450a8a..9ae20ce2bf 100644 --- a/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts +++ b/frontend/src/hooks/api/getters/useFeatureTypes/useFeatureTypes.ts @@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IFeatureType } from '../../../../interfaces/featureTypes'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useFeatureTypes = () => { const fetcher = async () => { const path = formatApiPath(`api/admin/feature-types`); const res = await fetch(path, { method: 'GET', - }); + }).then(handleErrorResponses('Feature types')); return res.json(); }; diff --git a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts b/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts index 9c1a033bbd..4ee8247be0 100644 --- a/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts +++ b/frontend/src/hooks/api/getters/useHealthReport/useHealthReport.ts @@ -4,6 +4,7 @@ import { IProjectHealthReport } from '../../../../interfaces/project'; import { fallbackProject } from '../useProject/fallbackProject'; import useSort from '../../../useSort'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useHealthReport = (id: string) => { const KEY = `api/admin/projects/${id}/health-report`; @@ -12,7 +13,7 @@ const useHealthReport = (id: string) => { const path = formatApiPath(`api/admin/projects/${id}/health-report`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Health report')).then(res => res.json()); }; const [sort] = useSort(); diff --git a/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts b/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts index 1fe3e760ec..302c24ccd1 100644 --- a/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts +++ b/frontend/src/hooks/api/getters/useProject/getProjectFetcher.ts @@ -1,11 +1,12 @@ import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; export const getProjectFetcher = (id: string) => { const fetcher = () => { const path = formatApiPath(`api/admin/projects/${id}`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Project overview')).then(res => res.json()); }; const KEY = `api/admin/projects/${id}`; diff --git a/frontend/src/hooks/api/getters/useProjects/useProjects.ts b/frontend/src/hooks/api/getters/useProjects/useProjects.ts index a45f2b2655..2c3ab1c768 100644 --- a/frontend/src/hooks/api/getters/useProjects/useProjects.ts +++ b/frontend/src/hooks/api/getters/useProjects/useProjects.ts @@ -3,13 +3,14 @@ import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IProjectCard } from '../../../../interfaces/project'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useProjects = () => { const fetcher = () => { const path = formatApiPath(`api/admin/projects`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Projects')).then(res => res.json()); }; const KEY = `api/admin/projects`; diff --git a/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts b/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts index b1b5cab7c6..11b89afa12 100644 --- a/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts +++ b/frontend/src/hooks/api/getters/useResetPassword/useResetPassword.ts @@ -2,12 +2,13 @@ import useSWR from 'swr'; import useQueryParams from '../../../useQueryParams'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; const getFetcher = (token: string) => () => { const path = formatApiPath(`auth/reset/validate?token=${token}`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Password reset')).then(res => res.json()); }; const INVALID_TOKEN_ERROR = 'InvalidTokenError'; diff --git a/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts index 9638c58409..c1d005e560 100644 --- a/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts +++ b/frontend/src/hooks/api/getters/useStrategies/useStrategies.ts @@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr'; import { useEffect, useState } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IStrategy } from '../../../../interfaces/strategy'; +import handleErrorResponses from '../httpErrorResponseHandler'; export const STRATEGIES_CACHE_KEY = 'api/admin/strategies'; @@ -12,7 +13,7 @@ const useStrategies = () => { return fetch(path, { method: 'GET', credentials: 'include', - }).then(res => res.json()); + }).then(handleErrorResponses('Strategies')).then(res => res.json()); }; const { data, error } = useSWR<{ strategies: IStrategy[] }>( diff --git a/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts index 4dd0380807..8d6cae4b23 100644 --- a/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts +++ b/frontend/src/hooks/api/getters/useTagTypes/useTagTypes.ts @@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { ITagType } from '../../../../interfaces/tags'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useTagTypes = () => { const fetcher = async () => { const path = formatApiPath(`api/admin/tag-types`); const res = await fetch(path, { method: 'GET', - }); + }).then(handleErrorResponses('Tag types')); return res.json(); }; diff --git a/frontend/src/hooks/api/getters/useTags/useTags.ts b/frontend/src/hooks/api/getters/useTags/useTags.ts index 5e31443b3b..dcea601d79 100644 --- a/frontend/src/hooks/api/getters/useTags/useTags.ts +++ b/frontend/src/hooks/api/getters/useTags/useTags.ts @@ -2,13 +2,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { ITag } from '../../../../interfaces/tags'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useTags = (featureId: string) => { const fetcher = async () => { const path = formatApiPath(`api/admin/features/${featureId}/tags`); const res = await fetch(path, { method: 'GET', - }); + }).then(handleErrorResponses('Tags')); return res.json(); }; diff --git a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts index 2f50605943..e299155c6e 100644 --- a/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts +++ b/frontend/src/hooks/api/getters/useUiConfig/useUiConfig.ts @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { defaultValue } from './defaultValue'; import { IUiConfig } from '../../../../interfaces/uiConfig'; +import handleErrorResponses from '../httpErrorResponseHandler'; const REQUEST_KEY = 'api/admin/ui-config'; @@ -13,7 +14,7 @@ const useUiConfig = () => { return fetch(path, { method: 'GET', credentials: 'include', - }).then(res => res.json()); + }).then(handleErrorResponses('configuration')).then(res => res.json()); }; const { data, error } = useSWR(REQUEST_KEY, fetcher); diff --git a/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts index 35a99b661e..5701a7611f 100644 --- a/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts +++ b/frontend/src/hooks/api/getters/useUnleashContext/useUnleashContext.ts @@ -1,13 +1,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useUnleashContext = (revalidate = true) => { const fetcher = () => { const path = formatApiPath(`api/admin/context`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Context variables')).then(res => res.json()); }; const CONTEXT_CACHE_KEY = 'api/admin/context'; diff --git a/frontend/src/hooks/api/getters/useUser/useUser.ts b/frontend/src/hooks/api/getters/useUser/useUser.ts index 997354537b..69e855ca18 100644 --- a/frontend/src/hooks/api/getters/useUser/useUser.ts +++ b/frontend/src/hooks/api/getters/useUser/useUser.ts @@ -2,6 +2,7 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; import { IPermission } from '../../../../interfaces/user'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useUser = () => { const KEY = `api/admin/user`; @@ -9,7 +10,7 @@ const useUser = () => { const path = formatApiPath(`api/admin/user`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('User info')).then(res => res.json()); }; const { data, error } = useSWR(KEY, fetcher); diff --git a/frontend/src/hooks/api/getters/useUsers/useUsers.ts b/frontend/src/hooks/api/getters/useUsers/useUsers.ts index 87c0c9a4fc..45387c400a 100644 --- a/frontend/src/hooks/api/getters/useUsers/useUsers.ts +++ b/frontend/src/hooks/api/getters/useUsers/useUsers.ts @@ -1,13 +1,14 @@ import useSWR, { mutate } from 'swr'; import { useState, useEffect } from 'react'; import { formatApiPath } from '../../../../utils/format-path'; +import handleErrorResponses from '../httpErrorResponseHandler'; const useUsers = () => { const fetcher = () => { const path = formatApiPath(`api/admin/user-admin`); return fetch(path, { method: 'GET', - }).then(res => res.json()); + }).then(handleErrorResponses('Users')).then(res => res.json()); }; const { data, error } = useSWR(`api/admin/user-admin`, fetcher);