mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 15:44:59 +00:00
Enhancement: Add API error handling utilities
This commit is contained in:
parent
43067bf4c2
commit
8e9f2b74ed
@ -1,10 +1,9 @@
|
||||
import React, { memo, useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import get from 'lodash/get';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import { Button } from '@strapi/design-system/Button';
|
||||
import Trash from '@strapi/icons/Trash';
|
||||
import { ConfirmDialog, useNotification } from '@strapi/helper-plugin';
|
||||
import { ConfirmDialog, useNotification, formatAPIError } from '@strapi/helper-plugin';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getTrad } from '../../../utils';
|
||||
import { connect, select } from './utils';
|
||||
@ -29,14 +28,12 @@ const DeleteLink = ({ isCreatingEntry, onDelete, onDeleteSucceeded, trackerPrope
|
||||
toggleWarningDelete();
|
||||
onDeleteSucceeded();
|
||||
} catch (err) {
|
||||
const errorMessage = get(
|
||||
err,
|
||||
'response.payload.message',
|
||||
formatMessage({ id: getTrad('error.record.delete') })
|
||||
);
|
||||
setIsModalConfirmButtonLoading(false);
|
||||
toggleWarningDelete();
|
||||
toggleNotification({ type: 'warning', message: errorMessage });
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: formatAPIError(err, { formatMessage, getTrad }),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
useRBACProvider,
|
||||
useTracking,
|
||||
Link,
|
||||
formatAPIError,
|
||||
} from '@strapi/helper-plugin';
|
||||
|
||||
import { IconButton } from '@strapi/design-system/IconButton';
|
||||
@ -147,7 +148,6 @@ function ListView({
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(err);
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: getTrad('error.model.fetch') },
|
||||
@ -170,11 +170,11 @@ function ListView({
|
||||
} catch (err) {
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: { id: getTrad('error.record.delete') },
|
||||
message: formatAPIError(err, { formatMessage, getTrad }),
|
||||
});
|
||||
}
|
||||
},
|
||||
[fetchData, params, slug, toggleNotification]
|
||||
[fetchData, params, slug, toggleNotification, formatMessage]
|
||||
);
|
||||
|
||||
const handleConfirmDeleteData = useCallback(
|
||||
@ -190,15 +190,9 @@ function ListView({
|
||||
message: { id: getTrad('success.record.delete') },
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = get(
|
||||
err,
|
||||
'response.payload.message',
|
||||
formatMessage({ id: getTrad('error.record.delete') })
|
||||
);
|
||||
|
||||
toggleNotification({
|
||||
type: 'warning',
|
||||
message: errorMessage,
|
||||
message: formatAPIError(err, { formatMessage, getTrad }),
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -101,4 +101,7 @@ export { default as wrapAxiosInstance } from './utils/wrapAxiosInstance';
|
||||
export { default as request } from './utils/request';
|
||||
export { default as getAPIInnerErrors } from './utils/getAPIInnerErrors';
|
||||
export { default as getYupInnerErrors } from './utils/getYupInnerErrors';
|
||||
export { default as formatAPIError } from './utils/formatAPIError';
|
||||
export { default as normalizeAPIError } from './utils/normalizeAPIError';
|
||||
|
||||
export { default as getFetchClient } from './utils/getFetchClient';
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import normalizeAPIError from '../normalizeAPIError';
|
||||
|
||||
export default function formatAPIError(error, { formatMessage, getTrad }) {
|
||||
if (!formatMessage) {
|
||||
throw new Error('formatMessage() is a mandatory argument.');
|
||||
}
|
||||
|
||||
const normalizedError = normalizeAPIError(error, getTrad);
|
||||
|
||||
// stringify multiple errors
|
||||
if (normalizedError?.errors) {
|
||||
return Object.values(normalizedError.errors).map(formatMessage).join('\n');
|
||||
}
|
||||
|
||||
return formatMessage(normalizedError);
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
import formatAPIError from '../index';
|
||||
|
||||
const API_VALIDATION_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ValidationError',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
path: ['field', '0', 'name'],
|
||||
message: 'Field contains errors',
|
||||
},
|
||||
|
||||
{
|
||||
path: ['field'],
|
||||
message: 'Field must be unique',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const formatMessage = jest.fn((t) => t.defaultMessage);
|
||||
|
||||
describe('formatAPIError', () => {
|
||||
test('handles ValidationError', () => {
|
||||
expect(
|
||||
formatAPIError(API_VALIDATION_ERROR_FIXTURE, {
|
||||
formatMessage,
|
||||
getTrad: (translation) => `plugin.${translation}`,
|
||||
})
|
||||
).toBe(`Field contains errors\nField must be unique`);
|
||||
});
|
||||
|
||||
test('handles ValidationError and applies a global translation prefix without getTrad', () => {
|
||||
expect(formatAPIError(API_VALIDATION_ERROR_FIXTURE, { formatMessage })).toBe(
|
||||
`Field contains errors\nField must be unique`
|
||||
);
|
||||
});
|
||||
|
||||
test('handles ApplicationError errors', () => {
|
||||
expect(
|
||||
formatAPIError(
|
||||
{
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ApplicationError',
|
||||
message: 'Error message',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ formatMessage, getTrad: (translation) => translation }
|
||||
)
|
||||
).toBe('Error message');
|
||||
});
|
||||
|
||||
test('error if formatMessage was not passed', () => {
|
||||
expect(() => formatAPIError(API_VALIDATION_ERROR_FIXTURE)).toThrow();
|
||||
});
|
||||
});
|
||||
@ -1,16 +1,11 @@
|
||||
import normalizeAPIError from '../normalizeAPIError';
|
||||
|
||||
export default function getAPIInnerErrors(error, { getTrad }) {
|
||||
const errorPayload = error.response.data.error.details.errors;
|
||||
const validationErrors = errorPayload.reduce((acc, err) => {
|
||||
acc[err.path.join('.')] = {
|
||||
id: getTrad(`apiError.${err.message}`),
|
||||
defaultMessage: err.message,
|
||||
values: {
|
||||
field: err.path[err.path.length - 1],
|
||||
},
|
||||
};
|
||||
const normalizedError = normalizeAPIError(error, getTrad);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
if (normalizedError?.errors) {
|
||||
return normalizedError.errors;
|
||||
}
|
||||
|
||||
return validationErrors;
|
||||
return normalizedError.defaultMessage;
|
||||
}
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import getAPIInnerErrors from '../index';
|
||||
|
||||
const API_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
path: ['field', '0', 'name'],
|
||||
message: 'Field contains errors',
|
||||
},
|
||||
|
||||
{
|
||||
path: ['field'],
|
||||
message: 'Field must be unique',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getAPIInnerError', () => {
|
||||
test('transforms API errors into errors, which can be rendered by the CM', () => {
|
||||
expect(getAPIInnerErrors(API_ERROR_FIXTURE)).toMatchObject({
|
||||
'field.0.name': {
|
||||
id: 'content-manager.apiError.Field contains errors',
|
||||
},
|
||||
field: {
|
||||
id: 'content-manager.apiError.Field must be unique',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
import getAPIInnerErrors from '../index';
|
||||
|
||||
const API_VALIDATION_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ValidationError',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
path: ['field', '0', 'name'],
|
||||
message: 'Field contains errors',
|
||||
},
|
||||
|
||||
{
|
||||
path: ['field'],
|
||||
message: 'Field must be unique',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const API_APPLICATION_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ApplicationError',
|
||||
message: 'Error message',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('getAPIInnerError', () => {
|
||||
test('handles ValidationError errors', () => {
|
||||
expect(
|
||||
getAPIInnerErrors(API_VALIDATION_ERROR_FIXTURE, { getTrad: (translation) => translation })
|
||||
).toMatchObject({
|
||||
'field.0.name': {
|
||||
id: 'apiError.Field contains errors',
|
||||
},
|
||||
field: {
|
||||
id: 'apiError.Field must be unique',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('handles ApplicationError errors', () => {
|
||||
expect(
|
||||
getAPIInnerErrors(API_APPLICATION_ERROR_FIXTURE, { getTrad: (translation) => translation })
|
||||
).toBe('Error message');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,48 @@
|
||||
function getPrefixedId(message, getTrad) {
|
||||
const errorPrefix = 'apiError.';
|
||||
const prefixedMessage = `${errorPrefix}${message}`;
|
||||
|
||||
if (getTrad) {
|
||||
return getTrad(prefixedMessage);
|
||||
}
|
||||
|
||||
return prefixedMessage;
|
||||
}
|
||||
|
||||
export default function normalizeAPIError(resError, getTrad) {
|
||||
const { error } = resError.response.data;
|
||||
|
||||
switch (error.name) {
|
||||
case 'ValidationError':
|
||||
const { errors } = error.details;
|
||||
|
||||
const normalizedErrors = errors.reduce((acc, err) => {
|
||||
const path = err?.path.join('.');
|
||||
const { message } = err;
|
||||
|
||||
acc[path] = {
|
||||
id: getPrefixedId(message, getTrad),
|
||||
defaultMessage: message,
|
||||
values: {
|
||||
field: err.path[err.path.length - 1],
|
||||
},
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return {
|
||||
name: error.name,
|
||||
errors: normalizedErrors,
|
||||
};
|
||||
|
||||
default:
|
||||
const { message } = error;
|
||||
|
||||
return {
|
||||
name: error.name,
|
||||
id: getPrefixedId(message, getTrad),
|
||||
defaultMessage: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
import normalizeAPIError from '../';
|
||||
|
||||
const API_VALIDATION_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ValidationError',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
path: ['field', '0', 'name'],
|
||||
message: 'Field contains errors',
|
||||
},
|
||||
|
||||
{
|
||||
path: ['field'],
|
||||
message: 'Field must be unique',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const API_APPLICATION_ERROR_FIXTURE = {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ApplicationError',
|
||||
message: 'Application crashed',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('normalizeAPIError', () => {
|
||||
test('Handle ValidationError', () => {
|
||||
expect(normalizeAPIError(API_VALIDATION_ERROR_FIXTURE)).toStrictEqual({
|
||||
name: 'ValidationError',
|
||||
errors: {
|
||||
field: {
|
||||
defaultMessage: 'Field must be unique',
|
||||
id: 'apiError.Field must be unique',
|
||||
values: {
|
||||
field: 'field',
|
||||
},
|
||||
},
|
||||
|
||||
'field.0.name': {
|
||||
defaultMessage: 'Field contains errors',
|
||||
id: 'apiError.Field contains errors',
|
||||
values: {
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Handle ValidationError with custom prefix function', () => {
|
||||
const prefixFunction = (id) => `custom.${id}`;
|
||||
|
||||
expect(normalizeAPIError(API_VALIDATION_ERROR_FIXTURE, prefixFunction)).toStrictEqual({
|
||||
name: 'ValidationError',
|
||||
errors: {
|
||||
field: {
|
||||
defaultMessage: 'Field must be unique',
|
||||
id: 'custom.apiError.Field must be unique',
|
||||
values: {
|
||||
field: 'field',
|
||||
},
|
||||
},
|
||||
|
||||
'field.0.name': {
|
||||
defaultMessage: 'Field contains errors',
|
||||
id: 'custom.apiError.Field contains errors',
|
||||
values: {
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Handle ApplicationError', () => {
|
||||
expect(normalizeAPIError(API_APPLICATION_ERROR_FIXTURE)).toStrictEqual({
|
||||
name: 'ApplicationError',
|
||||
defaultMessage: 'Application crashed',
|
||||
id: 'apiError.Application crashed',
|
||||
});
|
||||
});
|
||||
|
||||
test('Handle ApplicationError with custom prefix function', () => {
|
||||
const prefixFunction = (id) => `custom.${id}`;
|
||||
|
||||
expect(normalizeAPIError(API_APPLICATION_ERROR_FIXTURE, prefixFunction)).toStrictEqual({
|
||||
name: 'ApplicationError',
|
||||
defaultMessage: 'Application crashed',
|
||||
id: 'custom.apiError.Application crashed',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -167,11 +167,13 @@ describe('EditFolderDialog', () => {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ValidationError',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
path: ['parent'],
|
||||
message: FIXTURE_ERROR_MESSAGE,
|
||||
name: 'ValidationError',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@ -139,13 +139,13 @@ describe('BulkMoveButton', () => {
|
||||
response: {
|
||||
data: {
|
||||
error: {
|
||||
name: 'ValidationError',
|
||||
details: {
|
||||
errors: [
|
||||
{
|
||||
key: 'destination',
|
||||
path: [],
|
||||
message: FIXTURE_ERROR_MESSAGE,
|
||||
defaultMessage: FIXTURE_ERROR_MESSAGE,
|
||||
name: 'ValidationError',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user