From 9a5d81f983cb1206e718dc7fb46c5bd73b75ab71 Mon Sep 17 00:00:00 2001 From: Cyril Date: Tue, 7 Apr 2026 10:49:01 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BF=EF=B8=8F(frontend)=20redirect=20unman?= =?UTF-8?q?aged=205xx=20to=20dedicated=20/500=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add /500 with coffee illustration; replace inline TextErrors for API 5xx --- .../__tests__/app-impress/doc-routing.spec.ts | 42 ++++++ .../apps/impress/src/components/ErrorPage.tsx | 134 ++++++++++++++++++ .../impress/src/components/TextErrors.tsx | 34 ----- .../apps/impress/src/components/index.ts | 1 + .../components/DocVersionEditor.tsx | 1 - .../doc-versioning/components/VersionList.tsx | 1 - src/frontend/apps/impress/src/hooks/index.ts | 1 - .../src/hooks/useHttpErrorMessages.tsx | 26 ---- src/frontend/apps/impress/src/pages/500.tsx | 32 +++++ .../apps/impress/src/pages/_error.tsx | 90 +----------- .../impress/src/pages/docs/[id]/index.tsx | 43 +++--- 11 files changed, 234 insertions(+), 171 deletions(-) create mode 100644 src/frontend/apps/impress/src/components/ErrorPage.tsx delete mode 100644 src/frontend/apps/impress/src/hooks/useHttpErrorMessages.tsx create mode 100644 src/frontend/apps/impress/src/pages/500.tsx diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts index 2702b22e..ba80e0a8 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts @@ -33,6 +33,48 @@ test.describe('Doc Routing', () => { await expect(page).toHaveURL(/\/docs\/$/); }); + test('checks 500 refresh retries original document request', async ({ + page, + browserName, + }) => { + const [docTitle] = await createDoc(page, 'doc-routing-500', browserName, 1); + await verifyDocName(page, docTitle); + + const docId = page.url().split('/docs/')[1]?.split('/')[0]; + // While true, every doc GET fails (including React Query retries) so we + // reliably land on /500. Set to false before refresh so the doc loads again. + let failDocumentGet = true; + + await page.route(/\**\/documents\/\**/, async (route) => { + const request = route.request(); + if ( + failDocumentGet && + request.method().includes('GET') && + docId && + request.url().includes(`/documents/${docId}/`) + ) { + await route.fulfill({ + status: 500, + json: { detail: 'Internal Server Error' }, + }); + } else { + await route.continue(); + } + }); + + await page.reload(); + + await expect(page).toHaveURL(/\/500\/?\?from=/, { timeout: 15000 }); + + const refreshButton = page.getByRole('button', { name: 'Refresh page' }); + await expect(refreshButton).toBeVisible(); + + failDocumentGet = false; + await refreshButton.click(); + + await verifyDocName(page, docTitle); + }); + test('checks 404 on docs/[id] page', async ({ page }) => { await page.waitForTimeout(300); diff --git a/src/frontend/apps/impress/src/components/ErrorPage.tsx b/src/frontend/apps/impress/src/components/ErrorPage.tsx new file mode 100644 index 00000000..baf38034 --- /dev/null +++ b/src/frontend/apps/impress/src/components/ErrorPage.tsx @@ -0,0 +1,134 @@ +import { Button } from '@gouvfr-lasuite/cunningham-react'; +import Head from 'next/head'; +import Image, { StaticImageData } from 'next/image'; +import { useTranslation } from 'react-i18next'; +import styled from 'styled-components'; + +import { Box, Icon, StyledLink, Text } from '@/components'; + +const StyledButton = styled(Button)` + width: fit-content; +`; + +interface ErrorPageProps { + image: StaticImageData; + description: string; + refreshTarget?: string; + showReload?: boolean; +} + +const getSafeRefreshUrl = (target?: string): string | undefined => { + if (!target) { + return undefined; + } + + if (typeof window === 'undefined') { + return target.startsWith('/') && !target.startsWith('//') + ? target + : undefined; + } + + try { + const url = new URL(target, window.location.origin); + if (url.origin !== window.location.origin) { + return undefined; + } + return url.pathname + url.search + url.hash; + } catch { + return undefined; + } +}; + +export const ErrorPage = ({ + image, + description, + refreshTarget, + showReload, +}: ErrorPageProps) => { + const { t } = useTranslation(); + + const errorTitle = t('An unexpected error occurred.'); + const safeTarget = getSafeRefreshUrl(refreshTarget); + + return ( + <> + + + {errorTitle} - {t('Docs')} + + + + + + {errorTitle} - {t('Docs')} + + + + + {description} + + + + + + } + > + {t('Home')} + + + + {(safeTarget || showReload) && ( + + } + onClick={() => + safeTarget + ? window.location.assign(safeTarget) + : window.location.reload() + } + > + {t('Refresh page')} + + )} + + + + ); +}; diff --git a/src/frontend/apps/impress/src/components/TextErrors.tsx b/src/frontend/apps/impress/src/components/TextErrors.tsx index 996cf0f6..d5c635e2 100644 --- a/src/frontend/apps/impress/src/components/TextErrors.tsx +++ b/src/frontend/apps/impress/src/components/TextErrors.tsx @@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Box, Text, TextType } from '@/components'; -import { useHttpErrorMessages } from '@/hooks'; const AlertStyled = styled(Alert)` & .c__button--tertiary:hover { @@ -17,7 +16,6 @@ interface TextErrorsProps extends TextType { defaultMessage?: string; icon?: ReactNode; canClose?: boolean; - status?: number; } export const TextErrors = ({ @@ -25,7 +23,6 @@ export const TextErrors = ({ defaultMessage, icon, canClose = false, - status, ...textProps }: TextErrorsProps) => { return ( @@ -38,7 +35,6 @@ export const TextErrors = ({ @@ -48,39 +44,9 @@ export const TextErrors = ({ export const TextOnlyErrors = ({ causes, defaultMessage, - status, ...textProps }: TextErrorsProps) => { const { t } = useTranslation(); - const httpError = useHttpErrorMessages(status); - - if (httpError) { - return ( - - - {httpError.title} - - - {httpError.detail} - - - ); - } return ( diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts index 986796be..9075311a 100644 --- a/src/frontend/apps/impress/src/components/index.ts +++ b/src/frontend/apps/impress/src/components/index.ts @@ -4,6 +4,7 @@ export * from './Card'; export * from './DropButton'; export * from './dropdown-menu/DropdownMenu'; export * from './Emoji/EmojiPicker'; +export * from './ErrorPage'; export * from './quick-search'; export * from './Icon'; export * from './InfiniteScroll'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx index 50c79eb0..f903e70e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx @@ -58,7 +58,6 @@ export const DocVersionEditor = ({ diff --git a/src/frontend/apps/impress/src/hooks/index.ts b/src/frontend/apps/impress/src/hooks/index.ts index cd243993..340eaa5d 100644 --- a/src/frontend/apps/impress/src/hooks/index.ts +++ b/src/frontend/apps/impress/src/hooks/index.ts @@ -1,5 +1,4 @@ export * from './useClipboard'; export * from './useCmdK'; export * from './useDate'; -export * from './useHttpErrorMessages'; export * from './useKeyboardAction'; diff --git a/src/frontend/apps/impress/src/hooks/useHttpErrorMessages.tsx b/src/frontend/apps/impress/src/hooks/useHttpErrorMessages.tsx deleted file mode 100644 index 4d488a90..00000000 --- a/src/frontend/apps/impress/src/hooks/useHttpErrorMessages.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useTranslation } from 'react-i18next'; - -export const useHttpErrorMessages = (status?: number) => { - const { t } = useTranslation(); - - const messages: Record = { - 500: { - title: t('500 - Internal Server Error'), - detail: t('The server met an unexpected condition.'), - }, - 502: { - title: t('502 - Bad Gateway'), - detail: t( - 'The server received an invalid response. Please check your connection and try again.', - ), - }, - 503: { - title: t('503 - Service Unavailable'), - detail: t( - 'The service is temporarily unavailable. Please try again later.', - ), - }, - }; - - return status ? messages[status] : undefined; -}; diff --git a/src/frontend/apps/impress/src/pages/500.tsx b/src/frontend/apps/impress/src/pages/500.tsx new file mode 100644 index 00000000..f69f2ede --- /dev/null +++ b/src/frontend/apps/impress/src/pages/500.tsx @@ -0,0 +1,32 @@ +import { useRouter } from 'next/router'; +import { ReactElement } from 'react'; +import { useTranslation } from 'react-i18next'; + +import error_img from '@/assets/icons/error-coffee.png'; +import { ErrorPage } from '@/components'; +import { PageLayout } from '@/layouts'; +import { NextPageWithLayout } from '@/types/next'; + +const Page: NextPageWithLayout = () => { + const { t } = useTranslation(); + const { query } = useRouter(); + const from = Array.isArray(query.from) ? query.from[0] : query.from; + const refreshTarget = + from?.startsWith('/') && !from.startsWith('//') ? from : undefined; + + return ( + + ); +}; + +Page.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default Page; diff --git a/src/frontend/apps/impress/src/pages/_error.tsx b/src/frontend/apps/impress/src/pages/_error.tsx index 703d6df9..55c2c9df 100644 --- a/src/frontend/apps/impress/src/pages/_error.tsx +++ b/src/frontend/apps/impress/src/pages/_error.tsx @@ -1,100 +1,22 @@ -import { Button } from '@gouvfr-lasuite/cunningham-react'; import * as Sentry from '@sentry/nextjs'; import { NextPageContext } from 'next'; import NextError from 'next/error'; -import Head from 'next/head'; -import Image from 'next/image'; import { ReactElement } from 'react'; import { useTranslation } from 'react-i18next'; -import styled from 'styled-components'; import error_img from '@/assets/icons/error-planetes.png'; -import { Box, Icon, StyledLink, Text } from '@/components'; +import { ErrorPage } from '@/components'; import { PageLayout } from '@/layouts'; -const StyledButton = styled(Button)` - width: fit-content; -`; - const Error = () => { const { t } = useTranslation(); - const errorTitle = t('An unexpected error occurred.'); - return ( - <> - - - {errorTitle} - {t('Docs')} - - - - - - {errorTitle} - {t('Docs')} - - - - - {errorTitle} - - - - - - } - > - {t('Home')} - - - - - } - onClick={() => window.location.reload()} - > - {t('Refresh page')} - - - - + ); }; diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx index 9a81fc3b..3cb4ffb9 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Box, Icon, Loading, TextErrors } from '@/components'; +import { Loading } from '@/components'; import { DEFAULT_QUERY_RETRY } from '@/core'; import { Doc, @@ -105,7 +105,7 @@ const DocPage = ({ id }: DocProps) => { const { setCurrentDoc } = useDocStore(); const { addTask } = useBroadcastStore(); const queryClient = useQueryClient(); - const { replace } = useRouter(); + const { replace, asPath } = useRouter(); useCollaboration(doc?.id, doc?.content); const { t } = useTranslation(); const { authenticated } = useAuth(); @@ -191,44 +191,39 @@ const DocPage = ({ id }: DocProps) => { }, [addTask, doc?.id, queryClient]); useEffect(() => { - if (!isError || !error?.status || ![404, 401].includes(error.status)) { + if (!isError || !error?.status || [403].includes(error.status)) { return; } - let replacePath = `/${error.status}`; - if (error.status === 401) { if (authenticated) { queryClient.setQueryData([KEY_AUTH], null); } setAuthUrl(); + void replace('/401'); + return; } - void replace(replacePath); - }, [isError, error?.status, replace, authenticated, queryClient]); + if (error.status === 404) { + void replace('/404'); + return; + } + + if (error.status === 502) { + void replace('/offline'); + return; + } + + const fromPath = encodeURIComponent(asPath); + void replace(`/500?from=${fromPath}`); + }, [isError, error?.status, replace, authenticated, queryClient, asPath]); if (isError && error?.status) { - if ([404, 401].includes(error.status)) { - return ; - } - if (error.status === 403) { return ; } - return ( - - - ) : undefined - } - /> - - ); + return ; } if (!doc) {