diff --git a/public/app/features/alerting/unified/utils/misc.test.ts b/public/app/features/alerting/unified/utils/misc.test.ts index 8916603f2ee..815d5782a1c 100644 --- a/public/app/features/alerting/unified/utils/misc.test.ts +++ b/public/app/features/alerting/unified/utils/misc.test.ts @@ -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; + + 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; + + 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'); + }); }); diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index e7db8710c32..a90ee8f1d40 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -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);