Skip to content

Commit

Permalink
Merge pull request #783 from open-formulieren/feature/5033-service-un…
Browse files Browse the repository at this point in the history
…available-error-message

✨ [open-formulieren/open-forms#5033] User friendly error in case of 503
  • Loading branch information
sergei-maertens authored Jan 28, 2025
2 parents 385816d + 0217758 commit dc2a16f
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 15 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# ViteJS based
VITE_VERSION=latest
VITE_MUTE_ERROR_BOUNDARY_LOG=false
VITE_BASE_API_URL=http://localhost:8000/api/v2/
VITE_FORM_ID=93c09209-5fb9-4105-b6bb-9d9f0aa6782c
VITE_USE_HASH_ROUTING=false
32 changes: 17 additions & 15 deletions src/components/Errors/ErrorBoundary.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as Sentry from '@sentry/react';
import {getEnv} from 'env.mjs';
import PropTypes from 'prop-types';
import React from 'react';
import {FormattedMessage, useIntl} from 'react-intl';

import Body from 'components/Body';
import Card from 'components/Card';
import FormUnavailable from 'components/Errors/FormUnavailable';
import FormMaximumSubmissions from 'components/FormMaximumSubmissions';
import Link from 'components/Link';
import MaintenanceMode from 'components/MaintenanceMode';
Expand All @@ -13,7 +16,12 @@ import {DEBUG} from 'utils';
import ErrorMessage from './ErrorMessage';

const logError = (error, errorInfo) => {
if (DEBUG) console.error(error, errorInfo);
if (DEBUG) {
const muteConsole = getEnv('MUTE_ERROR_BOUNDARY_LOG');
if (!muteConsole) console.error(error, errorInfo);
} else {
Sentry.captureException(error);
}
};

class ErrorBoundary extends React.Component {
Expand All @@ -30,7 +38,6 @@ class ErrorBoundary extends React.Component {
}

componentDidCatch(error, errorInfo) {
// TODO: depending on the error type, send to sentry?
logError(error, errorInfo);
}

Expand Down Expand Up @@ -139,13 +146,9 @@ const UnprocessableEntityError = ({wrapper: Wrapper, error}) => {
UnprocessableEntityError.propTypes = GenericError.propTypes;

const ServiceUnavailableError = ({wrapper: Wrapper, error}) => {
if (!['form-maintenance', 'form-maximum-submissions'].includes(error.code)) {
return <GenericError wrapper={Wrapper} error={error} />;
}

// handle maintenance mode forms
if (error.code === 'form-maintenance') {
return (
const defaultComponent = <GenericError wrapper={Wrapper} error={error} />;
const componentMapping = {
'form-maintenance': (
<MaintenanceMode
title={
<FormattedMessage
Expand All @@ -154,13 +157,12 @@ const ServiceUnavailableError = ({wrapper: Wrapper, error}) => {
/>
}
/>
);
}
),
'form-maximum-submissions': <FormMaximumSubmissions />,
service_unavailable: <FormUnavailable wrapper={Wrapper} />,
};

// handle submission limit forms
if (error.code === 'form-maximum-submissions') {
return <FormMaximumSubmissions />;
}
return componentMapping[error.code] || defaultComponent;
};

// map the error class to the component to render it
Expand Down
103 changes: 103 additions & 0 deletions src/components/Errors/ErrorBoundary.stories.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {MemoryRouter} from 'react-router';

import {PermissionDenied, ServiceUnavailable, UnprocessableEntity} from 'errors';

import ErrorBoundary from './ErrorBoundary';

const Nested = ({error}) => {
throw error;
};

const render = ({useCard, errorType, errorCode}) => {
const error = new errorType('some error', 500, 'some error', errorCode);
return (
<ErrorBoundary useCard={useCard}>
<Nested error={error} />
</ErrorBoundary>
);
};

export default {
title: 'Private API / ErrorBoundary',
component: ErrorBoundary,
render,
argTypes: {
useCard: {control: {type: 'boolean'}},
errorType: {
table: {
options: [PermissionDenied, ServiceUnavailable, UnprocessableEntity],
control: {type: 'radio'},
},
},
},
};

export const GenericError = {
args: {
useCard: true,
errorType: Error,
errorCode: 'generic',
},
};

export const PermissionDeniedError = {
decorators: [
Story => (
<MemoryRouter initialEntries={['/']}>
<Story />
</MemoryRouter>
),
],
args: {
useCard: true,
errorType: PermissionDenied,
},
};

export const UnprocessableEntityErrorInactive = {
args: {
useCard: true,
errorType: UnprocessableEntity,
errorCode: 'form-inactive',
},
};

export const UnprocessableEntityErrorGeneric = {
args: {
useCard: true,
errorType: UnprocessableEntity,
errorCode: 'generic',
},
};

export const ServiceUnavailableErrorMaintenance = {
args: {
useCard: true,
errorType: ServiceUnavailable,
errorCode: 'form-maintenance',
},
};

export const ServiceUnavailableErrorMaxSubmissions = {
args: {
useCard: true,
errorType: ServiceUnavailable,
errorCode: 'form-maximum-submissions',
},
};

export const ServiceUnavailableError = {
args: {
useCard: true,
errorType: ServiceUnavailable,
errorCode: 'service_unavailable',
},
};

export const ServiceUnavailableErrorGeneric = {
args: {
useCard: true,
errorType: ServiceUnavailable,
errorCode: 'generic',
},
};
24 changes: 24 additions & 0 deletions src/components/Errors/FormUnavailable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {FormattedMessage, useIntl} from 'react-intl';

import ErrorMessage from 'components/Errors/ErrorMessage';

const FormUnavailable = ({wrapper: Wrapper}) => {
const intl = useIntl();
// Wrapper may be a DOM element, which can't handle <FormattedMessage />
const title = intl.formatMessage({
description: 'Open Forms service unavailable error title',
defaultMessage: 'Form unavailable',
});
return (
<Wrapper title={title}>
<ErrorMessage modifier="error">
<FormattedMessage
description="Open Forms service unavailable error message"
defaultMessage="Unfortunately, this form is currently unavailable due to an outage. Please try again later."
/>
</ErrorMessage>
</Wrapper>
);
};

export default FormUnavailable;

0 comments on commit dc2a16f

Please sign in to comment.