Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions src/components/CriticalActionDialog/CriticalActionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,28 @@ 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';

import './CriticalActionDialog.scss';

const b = cn('ydb-critical-dialog');

const parseError = (error: IResponseError) => {
if (error.data && 'issues' in error.data && error.data.issues) {
return <ResultIssues hideSeverity data={error.data} />;
}
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 <ResultIssues hideSeverity data={error.data} />;
}
if (error.statusText) {
return error.statusText;
}
}

return criticalActionDialogKeyset('default-error');
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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;
Expand Down
7 changes: 1 addition & 6 deletions src/types/api/error.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
import type {TIssueMessage} from './operations';

// TODO: extend with other error types
type ResponseErrorData = TIssueMessage;

export interface IResponseError<T = ResponseErrorData> {
Comment on lines -3 to -6
Copy link
Member Author

@artemmufazalov artemmufazalov Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data is unknown, but this type prevented ts errors in our code, where we used this error.

Made data unknown, added proper type guards and some tests for them

export interface IResponseError<T = unknown> {
data?: T;
status?: number;
statusText?: string;
Expand Down
59 changes: 59 additions & 0 deletions src/utils/__test__/response.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
19 changes: 11 additions & 8 deletions src/utils/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {IResponseError} from '../../types/api/error';
import {isNetworkError} from '../response';
import {isNetworkError, isResponseError} from '../response';

import i18n from './i18n';

Expand All @@ -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;
}
}

Expand Down
50 changes: 41 additions & 9 deletions src/utils/response.ts
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -26,24 +40,42 @@ 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 &&
error.data.authUrl &&
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',
);
}
Loading