Alerting: Improve API error payload handling (#109956)

* Handle error.data being set to null

* Tidy up code
This commit is contained in:
Konrad Lalik
2025-08-22 08:39:25 +02:00
committed by GitHub
parent ca19dc44db
commit 27c845828b
2 changed files with 87 additions and 3 deletions

View File

@ -196,4 +196,71 @@ describe('stringifyErrorLike', () => {
expect(stringifyErrorLike(error)).toBe('POST /my/url failed with 404: not found');
});
it('should prioritize data.message over statusText when both are present', () => {
const error = {
status: 500,
data: { message: 'API error message' },
statusText: 'Internal Server Error',
config: { url: '/api/test', method: 'GET' },
} satisfies FetchError;
expect(stringifyErrorLike(error)).toBe('GET /api/test failed with 500: API error message');
});
it('should fall back to statusText when data.message is not available', () => {
const error = {
status: 404,
data: { error: 'some other field' },
statusText: 'Not Found',
config: { url: '/api/test', method: 'GET' },
} satisfies FetchError;
expect(stringifyErrorLike(error)).toBe('Not Found');
});
it('should handle null data safely without crashing', () => {
const error = {
status: 500,
data: null,
statusText: 'Internal Server Error',
config: { url: '/api/test', method: 'POST' },
} satisfies FetchError<null>;
expect(stringifyErrorLike(error)).toBe('Internal Server Error');
});
it('should handle undefined data safely without crashing', () => {
const error = {
status: 500,
data: undefined,
statusText: 'Internal Server Error',
config: { url: '/api/test', method: 'PUT' },
} satisfies FetchError<undefined>;
expect(stringifyErrorLike(error)).toBe('Internal Server Error');
});
it('should handle data without message property safely', () => {
const error = {
status: 400,
data: { error: 'validation failed', code: 400 },
statusText: 'Bad Request',
config: { url: '/api/validate', method: 'POST' },
} satisfies FetchError;
expect(stringifyErrorLike(error)).toBe('Bad Request');
});
it('should prioritize error.message over data.message', () => {
const error = {
status: 403,
message: 'Error message property',
data: { message: 'Data message property' },
statusText: 'Forbidden',
config: { url: '/api/test', method: 'POST' },
} satisfies FetchError;
expect(stringifyErrorLike(error)).toBe('Error message property');
});
});

View File

@ -288,6 +288,23 @@ export function isErrorLike(error: unknown): error is Error {
return Boolean(error && typeof error === 'object' && 'message' in error);
}
// Small composable guards to safely inspect nested shapes without broad assertions
function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null;
}
function hasData(value: unknown): value is { data: unknown } {
return isObject(value) && 'data' in value;
}
function hasMessage(value: unknown): value is { message: string } {
if (!isObject(value)) {
return false;
}
const desc = Object.getOwnPropertyDescriptor(value, 'message');
return typeof desc?.value === 'string';
}
export function getErrorCode(error: unknown): string | undefined {
if (isApiMachineryError(error) && error.data.details) {
return error.data.details.uid;
@ -310,8 +327,7 @@ export function isErrorMatchingCode(error: Error | undefined, code: KnownErrorCo
}
export function stringifyErrorLike(error: unknown): string {
const fetchError = isFetchError(error);
if (fetchError) {
if (isFetchError(error)) {
if (isApiMachineryError(error)) {
const message = getErrorMessageFromApiMachineryErrorResponse(error);
if (message) {
@ -323,7 +339,8 @@ export function stringifyErrorLike(error: unknown): string {
return error.message;
}
if ('message' in error.data && typeof error.data.message === 'string') {
// Runtime check for error.data.message without narrow typing - prioritize over statusText
if (hasData(error) && hasMessage(error.data)) {
const status = getStatusFromError(error);
const message = getMessageFromError(error);