Compare commits

...

1 Commits

Author SHA1 Message Date
Ashwin Bhatkal
fc6f398828 fix: show the backend error message in the error modal instead of a generic axios error (#12215)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-07-21 19:29:13 +00:00
2 changed files with 78 additions and 3 deletions

View File

@@ -0,0 +1,62 @@
import { render, screen } from '@testing-library/react';
import { useEffect } from 'react';
import APIError from 'types/api/error';
import { ErrorModalProvider, useErrorModal } from './ErrorModalProvider';
// Mock the heavy modal so the test only asserts which error it receives.
jest.mock('components/ErrorModal/ErrorModal', () => ({
__esModule: true,
default: ({ error }: { error: APIError }): JSX.Element => (
<div data-testid="error-message">{error.getErrorMessage()}</div>
),
}));
function Trigger({ error }: { error: unknown }): null {
const { showErrorModal } = useErrorModal();
useEffect(() => {
showErrorModal(error);
}, [error, showErrorModal]);
return null;
}
describe('ErrorModalProvider — showErrorModal', () => {
it('surfaces the backend error message from a raw AxiosError', () => {
const axiosError = {
isAxiosError: true,
response: {
status: 400,
data: {
error: {
code: 'tag_invalid_key',
message: 'tag key contains disallowed characters',
errors: [],
},
},
},
};
render(
<ErrorModalProvider>
<Trigger error={axiosError} />
</ErrorModalProvider>,
);
expect(screen.getByTestId('error-message')).toHaveTextContent(
'tag key contains disallowed characters',
);
});
it('passes an already-constructed APIError through unchanged', () => {
const apiError = new APIError({
httpStatusCode: 409,
error: { code: 'conflict', message: 'already exists', url: '', errors: [] },
});
render(
<ErrorModalProvider>
<Trigger error={apiError} />
</ErrorModalProvider>,
);
expect(screen.getByTestId('error-message')).toHaveTextContent(
'already exists',
);
});
});

View File

@@ -8,11 +8,17 @@ import {
useMemo,
useState,
} from 'react';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import ErrorModal from 'components/ErrorModal/ErrorModal';
import APIError from 'types/api/error';
interface ErrorModalContextType {
showErrorModal: (error: APIError) => void;
/**
* Accepts any thrown value. Generated-client calls reject with a raw
* `AxiosError`, so we normalize to `APIError` here — otherwise the modal would
* show a generic "status 400" instead of the backend's `error.message`.
*/
showErrorModal: (error: unknown) => void;
hideErrorModal: () => void;
isErrorModalVisible: boolean;
}
@@ -29,8 +35,15 @@ export function ErrorModalProvider({
const [error, setError] = useState<APIError | null>(null);
const [isVisible, setIsVisible] = useState(false);
const showErrorModal = useCallback((error: APIError): void => {
setError(error);
const showErrorModal = useCallback((rawError: unknown): void => {
const apiError =
rawError instanceof APIError
? rawError
: convertToApiError(rawError as Parameters<typeof convertToApiError>[0]);
if (!apiError) {
return;
}
setError(apiError);
setIsVisible(true);
}, []);