diff --git a/packages/console/src/hooks/use-api.ts b/packages/console/src/hooks/use-api.ts index 5be24f6f1..d86ea96bc 100644 --- a/packages/console/src/hooks/use-api.ts +++ b/packages/console/src/hooks/use-api.ts @@ -13,13 +13,8 @@ import { TenantsContext } from '@/contexts/TenantsProvider'; import { useConfirmModal } from './use-confirm-modal'; export class RequestError extends Error { - status: number; - body?: RequestErrorBody; - - constructor(status: number, body: RequestErrorBody) { + constructor(public readonly status: number, public readonly body?: RequestErrorBody) { super('Request error occurred.'); - this.status = status; - this.body = body; } } diff --git a/packages/console/src/hooks/use-swr-fetcher.ts b/packages/console/src/hooks/use-swr-fetcher.ts index 1851849ab..1db2c9fe2 100644 --- a/packages/console/src/hooks/use-swr-fetcher.ts +++ b/packages/console/src/hooks/use-swr-fetcher.ts @@ -1,4 +1,3 @@ -import type { RequestErrorBody } from '@logto/schemas'; import { HTTPError } from 'ky'; import type { KyInstance } from 'ky/distribution/types/ky'; import { useCallback } from 'react'; @@ -41,8 +40,9 @@ const useSwrFetcher: useSwrFetcherHook = (api: KyInstance) => { } catch (error: unknown) { if (error instanceof HTTPError) { const { response } = error; - const metadata = await response.json(); - throw new RequestError(response.status, metadata); + // See https://stackoverflow.com/questions/53511974/javascript-fetch-failed-to-execute-json-on-response-body-stream-is-locked + // for why `.clone()` is needed + throw new RequestError(response.status, await response.clone().json()); } throw error; } diff --git a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx index df1ccf1f6..3680a37fc 100644 --- a/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx +++ b/packages/console/src/pages/SignInExperience/tabs/Others/components/ManageLanguage/LanguageEditor/LanguageDetails.tsx @@ -20,6 +20,7 @@ import Table from '@/components/Table'; import Textarea from '@/components/Textarea'; import { Tooltip } from '@/components/Tip'; import useApi, { RequestError } from '@/hooks/use-api'; +import useSwrFetcher from '@/hooks/use-swr-fetcher'; import useUiLanguages from '@/hooks/use-ui-languages'; import { createEmptyUiTranslation, @@ -34,18 +35,14 @@ const emptyUiTranslation = createEmptyUiTranslation(); const LanguageDetails = () => { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); - const { data: signInExperience } = useSWR('api/sign-in-exp'); - const { languages } = useUiLanguages(); - const { selectedLanguage, setIsDirty, setSelectedLanguage } = useContext(LanguageEditorContext); - const [isDeletionAlertOpen, setIsDeletionAlertOpen] = useState(false); - const isBuiltIn = isBuiltInLanguageTag(selectedLanguage); - const isDefaultLanguage = signInExperience?.languageInfo.fallbackLanguage === selectedLanguage; + const fetchApi = useApi({ hideErrorToast: true }); + const fetcher = useSwrFetcher(fetchApi); const translationEntries = useMemo( () => Object.entries((isBuiltIn ? resource[selectedLanguage] : en).translation), @@ -55,6 +52,7 @@ const LanguageDetails = () => { const { data: customPhrase, mutate } = useSWR( `api/custom-phrases/${selectedLanguage}`, { + fetcher, shouldRetryOnError: (error: unknown) => { if (error instanceof RequestError) { return error.status !== 404; @@ -98,7 +96,6 @@ const LanguageDetails = () => { ]); const { mutate: globalMutate } = useSWRConfig(); - const api = useApi(); const upsertCustomPhrase = useCallback(