Skip to content

Commit 9eb1395

Browse files
committed
♿️(frontend) redirect unmanaged 5xx to dedicated /500 page
Add /500 with coffee illustration; replace inline TextErrors for API 5xx
1 parent 98ed6a5 commit 9eb1395

11 files changed

Lines changed: 227 additions & 170 deletions

File tree

src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,48 @@ test.describe('Doc Routing', () => {
3333
await expect(page).toHaveURL(/\/docs\/$/);
3434
});
3535

36+
test('checks 500 refresh retries original document request', async ({
37+
page,
38+
browserName,
39+
}) => {
40+
const [docTitle] = await createDoc(page, 'doc-routing-500', browserName, 1);
41+
await verifyDocName(page, docTitle);
42+
43+
const docId = page.url().split('/docs/')[1]?.split('/')[0];
44+
// While true, every doc GET fails (including React Query retries) so we
45+
// reliably land on /500. Set to false before refresh so the doc loads again.
46+
let failDocumentGet = true;
47+
48+
await page.route(/\**\/documents\/\**/, async (route) => {
49+
const request = route.request();
50+
if (
51+
failDocumentGet &&
52+
request.method().includes('GET') &&
53+
docId &&
54+
request.url().includes(`/documents/${docId}/`)
55+
) {
56+
await route.fulfill({
57+
status: 500,
58+
json: { detail: 'Internal Server Error' },
59+
});
60+
} else {
61+
await route.continue();
62+
}
63+
});
64+
65+
await page.reload();
66+
67+
await expect(page).toHaveURL(/\/500\/?\?from=/, { timeout: 15000 });
68+
69+
const refreshButton = page.getByRole('button', { name: 'Refresh page' });
70+
await expect(refreshButton).toBeVisible();
71+
72+
failDocumentGet = false;
73+
await refreshButton.click();
74+
75+
await verifyDocName(page, docTitle);
76+
});
77+
3678
test('checks 404 on docs/[id] page', async ({ page }) => {
3779
await page.waitForTimeout(300);
3880

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Button } from '@gouvfr-lasuite/cunningham-react';
2+
import Head from 'next/head';
3+
import Image, { StaticImageData } from 'next/image';
4+
import { useTranslation } from 'react-i18next';
5+
import styled from 'styled-components';
6+
7+
import { Box, Icon, StyledLink, Text } from '@/components';
8+
9+
const StyledButton = styled(Button)`
10+
width: fit-content;
11+
`;
12+
13+
interface ErrorPageProps {
14+
image: StaticImageData;
15+
description: string;
16+
refreshTarget?: string;
17+
showReload?: boolean;
18+
}
19+
20+
const getSafeRefreshUrl = (target?: string): string | undefined => {
21+
if (!target) {
22+
return undefined;
23+
}
24+
25+
try {
26+
const url = new URL(target, window.location.origin);
27+
if (url.origin !== window.location.origin) {
28+
return undefined;
29+
}
30+
return url.pathname;
31+
} catch {
32+
return undefined;
33+
}
34+
};
35+
36+
export const ErrorPage = ({
37+
image,
38+
description,
39+
refreshTarget,
40+
showReload,
41+
}: ErrorPageProps) => {
42+
const { t } = useTranslation();
43+
44+
const errorTitle = t('An unexpected error occurred.');
45+
const safeTarget = getSafeRefreshUrl(refreshTarget);
46+
47+
return (
48+
<>
49+
<Head>
50+
<title>
51+
{errorTitle} - {t('Docs')}
52+
</title>
53+
<meta
54+
property="og:title"
55+
content={`${errorTitle} - ${t('Docs')}`}
56+
key="title"
57+
/>
58+
</Head>
59+
<Box
60+
$align="center"
61+
$margin="auto"
62+
$gap="md"
63+
$padding={{ bottom: '2rem' }}
64+
>
65+
<Text as="h1" $textAlign="center" className="sr-only">
66+
{errorTitle} - {t('Docs')}
67+
</Text>
68+
<Image
69+
src={image}
70+
alt=""
71+
width={300}
72+
style={{
73+
maxWidth: '100%',
74+
height: 'auto',
75+
}}
76+
/>
77+
78+
<Text
79+
as="p"
80+
$textAlign="center"
81+
$maxWidth="350px"
82+
$theme="neutral"
83+
$margin="0"
84+
>
85+
{description}
86+
</Text>
87+
88+
<Box $direction="row" $gap="sm">
89+
<StyledLink href="/">
90+
<StyledButton
91+
color="neutral"
92+
icon={
93+
<Icon
94+
iconName="house"
95+
variant="symbols-outlined"
96+
$withThemeInherited
97+
/>
98+
}
99+
>
100+
{t('Home')}
101+
</StyledButton>
102+
</StyledLink>
103+
104+
{(safeTarget || showReload) && (
105+
<StyledButton
106+
color="neutral"
107+
variant="bordered"
108+
icon={
109+
<Icon
110+
iconName="refresh"
111+
variant="symbols-outlined"
112+
$withThemeInherited
113+
/>
114+
}
115+
onClick={() =>
116+
safeTarget
117+
? window.location.assign(safeTarget)
118+
: window.location.reload()
119+
}
120+
>
121+
{t('Refresh page')}
122+
</StyledButton>
123+
)}
124+
</Box>
125+
</Box>
126+
</>
127+
);
128+
};

src/frontend/apps/impress/src/components/TextErrors.tsx

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
44
import styled from 'styled-components';
55

66
import { Box, Text, TextType } from '@/components';
7-
import { useHttpErrorMessages } from '@/hooks';
87

98
const AlertStyled = styled(Alert)`
109
& .c__button--tertiary:hover {
@@ -17,15 +16,13 @@ interface TextErrorsProps extends TextType {
1716
defaultMessage?: string;
1817
icon?: ReactNode;
1918
canClose?: boolean;
20-
status?: number;
2119
}
2220

2321
export const TextErrors = ({
2422
causes,
2523
defaultMessage,
2624
icon,
2725
canClose = false,
28-
status,
2926
...textProps
3027
}: TextErrorsProps) => {
3128
return (
@@ -38,7 +35,6 @@ export const TextErrors = ({
3835
<TextOnlyErrors
3936
causes={causes}
4037
defaultMessage={defaultMessage}
41-
status={status}
4238
{...textProps}
4339
/>
4440
</AlertStyled>
@@ -48,39 +44,9 @@ export const TextErrors = ({
4844
export const TextOnlyErrors = ({
4945
causes,
5046
defaultMessage,
51-
status,
5247
...textProps
5348
}: TextErrorsProps) => {
5449
const { t } = useTranslation();
55-
const httpError = useHttpErrorMessages(status);
56-
57-
if (httpError) {
58-
return (
59-
<Box $direction="column" $gap="0.2rem">
60-
<Text
61-
as="h1"
62-
$theme="error"
63-
$textAlign="center"
64-
$margin="0"
65-
$size="1rem"
66-
$weight="unset"
67-
{...textProps}
68-
>
69-
{httpError.title}
70-
</Text>
71-
<Text
72-
as="p"
73-
$theme="error"
74-
$textAlign="center"
75-
$margin="0"
76-
$size="0.875rem"
77-
{...textProps}
78-
>
79-
{httpError.detail}
80-
</Text>
81-
</Box>
82-
);
83-
}
8450

8551
return (
8652
<Box $direction="column" $gap="0.2rem">

src/frontend/apps/impress/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './Card';
44
export * from './DropButton';
55
export * from './dropdown-menu/DropdownMenu';
66
export * from './Emoji/EmojiPicker';
7+
export * from './ErrorPage';
78
export * from './quick-search';
89
export * from './Icon';
910
export * from './InfiniteScroll';

src/frontend/apps/impress/src/features/docs/doc-versioning/components/DocVersionEditor.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ export const DocVersionEditor = ({
5858
<Box $margin="large" className="--docs--doc-version-editor-error">
5959
<TextErrors
6060
causes={error.cause}
61-
status={error.status}
6261
icon={
6362
error.status === 502 ? (
6463
<Text

src/frontend/apps/impress/src/features/docs/doc-versioning/components/VersionList.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ const VersionListState = ({
6262
>
6363
<TextErrors
6464
causes={error.cause}
65-
status={error.status}
6665
icon={
6766
error.status === 502 ? (
6867
<Icon iconName="wifi_off" $theme="danger" />
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './useClipboard';
22
export * from './useCmdK';
33
export * from './useDate';
4-
export * from './useHttpErrorMessages';
54
export * from './useKeyboardAction';

src/frontend/apps/impress/src/hooks/useHttpErrorMessages.tsx

Lines changed: 0 additions & 26 deletions
This file was deleted.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useRouter } from 'next/router';
2+
import { ReactElement } from 'react';
3+
import { useTranslation } from 'react-i18next';
4+
5+
import error_img from '@/assets/icons/error-coffee.png';
6+
import { ErrorPage } from '@/components';
7+
import { PageLayout } from '@/layouts';
8+
import { NextPageWithLayout } from '@/types/next';
9+
10+
const Page: NextPageWithLayout = () => {
11+
const { t } = useTranslation();
12+
const { query } = useRouter();
13+
const from = Array.isArray(query.from) ? query.from[0] : query.from;
14+
const refreshTarget =
15+
from?.startsWith('/') && !from.startsWith('//') ? from : undefined;
16+
17+
return (
18+
<ErrorPage
19+
image={error_img}
20+
description={t(
21+
'An unexpected error occurred. Go grab a coffee or try to refresh the page.',
22+
)}
23+
refreshTarget={refreshTarget}
24+
/>
25+
);
26+
};
27+
28+
Page.getLayout = function getLayout(page: ReactElement) {
29+
return <PageLayout withFooter={false}>{page}</PageLayout>;
30+
};
31+
32+
export default Page;

0 commit comments

Comments
 (0)