diff --git a/src/components/CriticalActionDialog/CriticalActionDialog.tsx b/src/components/CriticalActionDialog/CriticalActionDialog.tsx
index 4e80453de8..d8ea3eadc2 100644
--- a/src/components/CriticalActionDialog/CriticalActionDialog.tsx
+++ b/src/components/CriticalActionDialog/CriticalActionDialog.tsx
@@ -6,6 +6,7 @@ import {Checkbox, Dialog, Icon} from '@gravity-ui/uikit';
import {ResultIssues} from '../../containers/Tenant/Query/Issues/Issues';
import type {IResponseError} from '../../types/api/error';
import {cn} from '../../utils/cn';
+import {isResponseError, isResponseErrorWithIssues} from '../../utils/response';
import {criticalActionDialogKeyset} from './i18n';
@@ -13,15 +14,20 @@ import './CriticalActionDialog.scss';
const b = cn('ydb-critical-dialog');
-const parseError = (error: IResponseError) => {
- if (error.data && 'issues' in error.data && error.data.issues) {
- return ;
- }
- if (error.status === 403) {
- return criticalActionDialogKeyset('no-rights-error');
- }
- if (error.statusText) {
- return error.statusText;
+const parseError = (error: unknown) => {
+ if (isResponseError(error)) {
+ if (error.status === 403) {
+ return criticalActionDialogKeyset('no-rights-error');
+ }
+ if (typeof error.data === 'string') {
+ return error.data;
+ }
+ if (isResponseErrorWithIssues(error) && error.data) {
+ return ;
+ }
+ if (error.statusText) {
+ return error.statusText;
+ }
}
return criticalActionDialogKeyset('default-error');
diff --git a/src/containers/Tenant/Query/utils/isQueryCancelledError.ts b/src/containers/Tenant/Query/utils/isQueryCancelledError.ts
index 5a4afef5c8..816dfed694 100644
--- a/src/containers/Tenant/Query/utils/isQueryCancelledError.ts
+++ b/src/containers/Tenant/Query/utils/isQueryCancelledError.ts
@@ -1,5 +1,5 @@
-import type {IResponseError} from '../../../../types/api/error';
import {isQueryErrorResponse, parseQueryError} from '../../../../utils/query';
+import {isResponseError} from '../../../../utils/response';
function isAbortError(error: unknown): error is {name: string} {
return (
@@ -10,10 +10,6 @@ function isAbortError(error: unknown): error is {name: string} {
);
}
-function isResponseError(error: unknown): error is IResponseError {
- return typeof error === 'object' && error !== null && 'isCancelled' in error;
-}
-
export function isQueryCancelledError(error: unknown): boolean {
if (isAbortError(error)) {
return true;
diff --git a/src/types/api/error.ts b/src/types/api/error.ts
index ffc48c7f4b..e4c099dea7 100644
--- a/src/types/api/error.ts
+++ b/src/types/api/error.ts
@@ -1,9 +1,4 @@
-import type {TIssueMessage} from './operations';
-
-// TODO: extend with other error types
-type ResponseErrorData = TIssueMessage;
-
-export interface IResponseError {
+export interface IResponseError {
data?: T;
status?: number;
statusText?: string;
diff --git a/src/utils/__test__/response.test.ts b/src/utils/__test__/response.test.ts
new file mode 100644
index 0000000000..cb26bfc336
--- /dev/null
+++ b/src/utils/__test__/response.test.ts
@@ -0,0 +1,59 @@
+import {isResponseError, isResponseErrorWithIssues} from '../response';
+
+describe('isResponseError', () => {
+ test('should return false on incorrect data', () => {
+ const incorrectValues = [{}, [], 'hello', 123, null, undefined];
+
+ incorrectValues.forEach((value) => {
+ expect(isResponseError(value)).toBe(false);
+ });
+ });
+ test('should return true if it is object with status or status text', () => {
+ expect(isResponseError({status: 403})).toBe(true);
+ expect(isResponseError({statusText: 'Gateway timeout'})).toBe(true);
+ });
+ test('should return true if it is cancelled', () => {
+ expect(isResponseError({isCancelled: true})).toBe(true);
+ });
+ test('should return true if it has data', () => {
+ expect(isResponseError({data: 'Everything is broken'})).toBe(true);
+ });
+});
+
+describe('isResponseErrorWithIssues', () => {
+ test('should return false on incorrect data', () => {
+ const incorrectValues = [{}, [], 'hello', 123, null, undefined];
+
+ incorrectValues.forEach((value) => {
+ expect(isResponseErrorWithIssues({data: value})).toBe(false);
+ });
+ });
+ test('should return false on empty issues', () => {
+ expect(
+ isResponseErrorWithIssues({
+ data: {issues: []},
+ }),
+ ).toBe(false);
+ });
+ test('should return false on incorrect issues value', () => {
+ const incorrectValues = [{}, [], 'hello', 123, null, undefined];
+
+ incorrectValues.forEach((value) => {
+ expect(isResponseErrorWithIssues({data: {issues: value}})).toBe(false);
+ });
+ });
+ test('should return false on incorrect issue inside issues', () => {
+ const incorrectValues = [{}, [], 'hello', 123, null, undefined];
+
+ incorrectValues.forEach((value) => {
+ expect(isResponseErrorWithIssues({data: {issues: [value]}})).toBe(false);
+ });
+ });
+ test('should return true if it is an array of issues', () => {
+ expect(
+ isResponseErrorWithIssues({
+ data: {issues: [{message: 'Some error'}]},
+ }),
+ ).toBe(true);
+ });
+});
diff --git a/src/utils/errors/index.ts b/src/utils/errors/index.ts
index e63fa7b8d6..a209fe0c8f 100644
--- a/src/utils/errors/index.ts
+++ b/src/utils/errors/index.ts
@@ -1,5 +1,4 @@
-import type {IResponseError} from '../../types/api/error';
-import {isNetworkError} from '../response';
+import {isNetworkError, isResponseError} from '../response';
import i18n from './i18n';
@@ -24,12 +23,16 @@ export function prepareCommonErrorMessage(err: unknown): string {
return err.message;
}
- if (typeof err === 'object' && 'data' in err) {
- const responseError = err as IResponseError;
- if (responseError.data?.message) {
- return responseError.data.message;
- } else if (typeof responseError.data === 'string') {
- return responseError.data;
+ if (isResponseError(err)) {
+ if (
+ err.data &&
+ typeof err.data === 'object' &&
+ 'message' in err.data &&
+ typeof err.data.message === 'string'
+ ) {
+ return err.data.message;
+ } else if (typeof err.data === 'string') {
+ return err.data;
}
}
diff --git a/src/utils/response.ts b/src/utils/response.ts
index 1e2a5ec413..6ddbe7b220 100644
--- a/src/utils/response.ts
+++ b/src/utils/response.ts
@@ -1,6 +1,20 @@
import type {AxiosError, AxiosResponse} from 'axios';
-import type {NetworkError} from '../types/api/error';
+import type {IResponseError, NetworkError} from '../types/api/error';
+import type {TIssueMessage} from '../types/api/operations';
+import type {IssueMessage} from '../types/api/query';
+
+export function isResponseError(error: unknown): error is IResponseError {
+ if (!error || typeof error !== 'object') {
+ return false;
+ }
+ const hasData = 'data' in error;
+ const hasStatus = 'status' in error && typeof error.status === 'number';
+ const hasStatusText = 'statusText' in error && typeof error.statusText === 'string';
+ const isCancelled = 'isCancelled' in error && typeof error.isCancelled === 'boolean';
+
+ return hasData || hasStatus || hasStatusText || isCancelled;
+}
export const isNetworkError = (error: unknown): error is NetworkError => {
return Boolean(
@@ -26,20 +40,14 @@ export function isAxiosError(error: unknown): error is AxiosErrorObject {
);
}
-export function isAccessError(error: unknown): error is {status: number} {
- return Boolean(
- error &&
- typeof error === 'object' &&
- 'status' in error &&
- (error.status === 403 || error.status === 401),
- );
+export function isAccessError(error: unknown): error is IResponseError {
+ return Boolean(isResponseError(error) && (error.status === 403 || error.status === 401));
}
export function isRedirectToAuth(error: unknown): error is {status: 401; data: {authUrl: string}} {
return Boolean(
isAccessError(error) &&
error.status === 401 &&
- 'data' in error &&
error.data &&
typeof error.data === 'object' &&
'authUrl' in error.data &&
@@ -47,3 +55,27 @@ export function isRedirectToAuth(error: unknown): error is {status: 401; data: {
typeof error.data.authUrl === 'string',
);
}
+
+type Issue = TIssueMessage | IssueMessage;
+
+export function isResponseErrorWithIssues(
+ error: unknown,
+): error is IResponseError<{issues: Issue[]}> {
+ return Boolean(
+ isResponseError(error) &&
+ error.data &&
+ typeof error.data === 'object' &&
+ 'issues' in error.data &&
+ isIssuesArray(error.data.issues),
+ );
+}
+
+export function isIssuesArray(arr: unknown): arr is Issue[] {
+ return Boolean(Array.isArray(arr) && arr.length && arr.every(isIssue));
+}
+
+export function isIssue(obj: unknown): obj is Issue {
+ return Boolean(
+ obj && typeof obj === 'object' && 'message' in obj && typeof obj.message === 'string',
+ );
+}