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) {