mirror of
https://github.com/grafana/grafana.git
synced 2025-09-18 09:33:09 +08:00
Alerting: Improve API error payload handling (#109956)
* Handle error.data being set to null * Tidy up code
This commit is contained in:
@ -196,4 +196,71 @@ describe('stringifyErrorLike', () => {
|
|||||||
|
|
||||||
expect(stringifyErrorLike(error)).toBe('POST /my/url failed with 404: not found');
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -288,6 +288,23 @@ export function isErrorLike(error: unknown): error is Error {
|
|||||||
return Boolean(error && typeof error === 'object' && 'message' in 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 {
|
export function getErrorCode(error: unknown): string | undefined {
|
||||||
if (isApiMachineryError(error) && error.data.details) {
|
if (isApiMachineryError(error) && error.data.details) {
|
||||||
return error.data.details.uid;
|
return error.data.details.uid;
|
||||||
@ -310,8 +327,7 @@ export function isErrorMatchingCode(error: Error | undefined, code: KnownErrorCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function stringifyErrorLike(error: unknown): string {
|
export function stringifyErrorLike(error: unknown): string {
|
||||||
const fetchError = isFetchError(error);
|
if (isFetchError(error)) {
|
||||||
if (fetchError) {
|
|
||||||
if (isApiMachineryError(error)) {
|
if (isApiMachineryError(error)) {
|
||||||
const message = getErrorMessageFromApiMachineryErrorResponse(error);
|
const message = getErrorMessageFromApiMachineryErrorResponse(error);
|
||||||
if (message) {
|
if (message) {
|
||||||
@ -323,7 +339,8 @@ export function stringifyErrorLike(error: unknown): string {
|
|||||||
return error.message;
|
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 status = getStatusFromError(error);
|
||||||
const message = getMessageFromError(error);
|
const message = getMessageFromError(error);
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user