mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Enable useUnknownInCatchVariables in TypeScript (#68996)
* Enable `useUnknownInCatchVariables` * Add `isErrnoException` * Switch from unsafe `error.message` access to `getErrorMessage`/`ensureError` * Make `serializeError` idempotent * `TypeError` -> `unknown` * Simplify `isErrnoException`, require `code` * Create `isPermissionDeniedError` utility * Make `isPermissionDeniedError` inline helper
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"strict": false,
|
"strict": false,
|
||||||
"strictBindCallApply": true,
|
"strictBindCallApply": true,
|
||||||
"noImplicitThis": true,
|
"noImplicitThis": true,
|
||||||
|
"useUnknownInCatchVariables": true,
|
||||||
"noEmitHelpers": true,
|
"noEmitHelpers": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|||||||
+24
-4
@@ -310,10 +310,7 @@ function makeReviewStateOptions(
|
|||||||
// Connect) means the reviewer can't see the eligible Access Lists, which
|
// Connect) means the reviewer can't see the eligible Access Lists, which
|
||||||
// is distinct from there being none. Detect it by status code rather than
|
// is distinct from there being none. Detect it by status code rather than
|
||||||
// message text so it survives across versions; surface other errors as-is.
|
// message text so it survives across versions; surface other errors as-is.
|
||||||
const err = fetchSuggestedAccessListsAttempt.error;
|
msg = isPermissionDeniedError(fetchSuggestedAccessListsAttempt.error)
|
||||||
const isPermissionError =
|
|
||||||
err?.response?.status === 403 || err?.code === 'PERMISSION_DENIED';
|
|
||||||
msg = isPermissionError
|
|
||||||
? "You don't have permission to view the Access Lists eligible for long-term approval of this request. You can still reject it."
|
? "You don't have permission to view the Access Lists eligible for long-term approval of this request. You can still reject it."
|
||||||
: fetchSuggestedAccessListsAttempt.statusText;
|
: fetchSuggestedAccessListsAttempt.statusText;
|
||||||
} else if (request.resources.length === 0) {
|
} else if (request.resources.length === 0) {
|
||||||
@@ -395,3 +392,26 @@ const HorizontalLine = styled.div<{ height?: number }>`
|
|||||||
function makeTraitLabel(traitKey: string, traitVals: string[]) {
|
function makeTraitLabel(traitKey: string, traitVals: string[]) {
|
||||||
return `${traitKey}: ${traitVals.sort().join(', ')}`;
|
return `${traitKey}: ${traitVals.sort().join(', ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an error represents a permission denial from either the Web
|
||||||
|
* API or a gRPC service.
|
||||||
|
*
|
||||||
|
* TODO(gzdunek): Consider passing a permission-error predicate so that Web UI and Connect
|
||||||
|
* can identify their own native error type.
|
||||||
|
*/
|
||||||
|
function isPermissionDeniedError(error: unknown): boolean {
|
||||||
|
if (typeof error !== 'object' || error === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ('code' in error && error.code === 'PERMISSION_DENIED') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'response' in error &&
|
||||||
|
typeof error.response === 'object' &&
|
||||||
|
error.response !== null &&
|
||||||
|
'status' in error.response &&
|
||||||
|
error.response.status === 403
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
+15
-5
@@ -238,12 +238,22 @@ test('disables long-term approval and explains why when no Access List is sugges
|
|||||||
|
|
||||||
// A permission failure (the reviewer can't read the eligible Access Lists) is a
|
// A permission failure (the reviewer can't read the eligible Access Lists) is a
|
||||||
// distinct state from there being none, and gets its own message.
|
// distinct state from there being none, and gets its own message.
|
||||||
test('shows a permission-specific message when the reviewer cannot view eligible Access Lists', async () => {
|
test.each([
|
||||||
|
[
|
||||||
|
'HTTP 403',
|
||||||
|
Object.assign(
|
||||||
|
new Error('access denied to perform action "read" on access list'),
|
||||||
|
{ response: { status: 403 } }
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'gRPC PERMISSION_DENIED',
|
||||||
|
Object.assign(new Error('permission denied'), {
|
||||||
|
code: 'PERMISSION_DENIED',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
])('shows a permission-specific message for %s', async (_, permissionError) => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const permissionError = Object.assign(
|
|
||||||
new Error('access denied to perform action "read" on access list'),
|
|
||||||
{ response: { status: 403 } }
|
|
||||||
);
|
|
||||||
render(
|
render(
|
||||||
<RequestView
|
<RequestView
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { Logger } from 'design/logger';
|
|||||||
import type { ToastNotificationItem } from 'shared/components/ToastNotification';
|
import type { ToastNotificationItem } from 'shared/components/ToastNotification';
|
||||||
import { Attempt } from 'shared/hooks/useAsync';
|
import { Attempt } from 'shared/hooks/useAsync';
|
||||||
import { ClientScreenSpec, ClipboardData, TdpClient } from 'shared/libs/tdp';
|
import { ClientScreenSpec, ClipboardData, TdpClient } from 'shared/libs/tdp';
|
||||||
import { isAbortError } from 'shared/utils/error';
|
import { getErrorMessage, isAbortError } from 'shared/utils/error';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@@ -206,7 +206,7 @@ export default function useDesktopSession(
|
|||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
content: {
|
content: {
|
||||||
title: 'Could not share a directory',
|
title: 'Could not share a directory',
|
||||||
description: e.message,
|
description: getErrorMessage(e),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -222,7 +222,7 @@ export default function useDesktopSession(
|
|||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
content: {
|
content: {
|
||||||
title: 'Failed to unmount the shared directory',
|
title: 'Failed to unmount the shared directory',
|
||||||
description: e.message,
|
description: getErrorMessage(e),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ import { FileTransferListeners } from './FileTransferStateless/types';
|
|||||||
|
|
||||||
export interface FileTransferEventsEmitter extends FileTransferListeners {
|
export interface FileTransferEventsEmitter extends FileTransferListeners {
|
||||||
emitProgress(progress: number): void;
|
emitProgress(progress: number): void;
|
||||||
|
emitError(error: unknown): void;
|
||||||
emitError(error: Error): void;
|
|
||||||
|
|
||||||
emitComplete(): void;
|
emitComplete(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use TanStack Query (useQuery/useMutation) instead. See RFD 197.
|
* @deprecated Use TanStack Query (useQuery/useMutation) instead. See RFD 197.
|
||||||
*
|
*
|
||||||
@@ -209,7 +211,7 @@ export type Attempt<T> =
|
|||||||
status: 'error';
|
status: 'error';
|
||||||
data: null;
|
data: null;
|
||||||
statusText: string;
|
statusText: string;
|
||||||
error: any;
|
error: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function hasFinished<T>(attempt: Attempt<T>): boolean {
|
export function hasFinished<T>(attempt: Attempt<T>): boolean {
|
||||||
@@ -240,12 +242,12 @@ export function makeProcessingAttempt<T>(): Attempt<T> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeErrorAttempt<T>(error: Error): Attempt<T> {
|
export function makeErrorAttempt<T>(error: unknown): Attempt<T> {
|
||||||
return {
|
return {
|
||||||
data: null,
|
data: null,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
error: error,
|
error: error,
|
||||||
statusText: error.message,
|
statusText: getErrorMessage(error),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import Logger from 'shared/libs/logger';
|
import Logger from 'shared/libs/logger';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
const logger = Logger.create('shared/hooks/useAttempt');
|
const logger = Logger.create('shared/hooks/useAttempt');
|
||||||
|
|
||||||
@@ -56,9 +57,13 @@ function makeActions(setState) {
|
|||||||
setState({ ...defaultState });
|
setState({ ...defaultState });
|
||||||
}
|
}
|
||||||
|
|
||||||
function error(err: Error) {
|
function error(err: unknown) {
|
||||||
logger.error('attempt', err);
|
logger.error('attempt', err);
|
||||||
setState({ ...defaultState, isFailed: true, message: err.message });
|
setState({
|
||||||
|
...defaultState,
|
||||||
|
isFailed: true,
|
||||||
|
message: getErrorMessage(err),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function run(fn: Callback) {
|
function run(fn: Callback) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import Logger from 'shared/libs/logger';
|
import Logger from 'shared/libs/logger';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
const logger = Logger.create('shared/hooks/useAttempt');
|
const logger = Logger.create('shared/hooks/useAttempt');
|
||||||
|
|
||||||
@@ -33,9 +34,9 @@ export default function useAttemptNext(status = '' as Attempt['status']) {
|
|||||||
statusText: '',
|
statusText: '',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleError = useCallback((err: Error) => {
|
const handleError = useCallback((err: unknown) => {
|
||||||
logger.error('attempt', err);
|
logger.error('attempt', err);
|
||||||
setAttempt({ status: 'failed', statusText: err.message });
|
setAttempt({ status: 'failed', statusText: getErrorMessage(err) });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const run = useCallback((fn: Callback) => {
|
const run = useCallback((fn: Callback) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
|
|
||||||
import { Attempt } from 'shared/hooks/useAttemptNext';
|
import { Attempt } from 'shared/hooks/useAttemptNext';
|
||||||
import { isAbortError } from 'shared/utils/abortError';
|
import { isAbortError } from 'shared/utils/abortError';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports -- FIXME
|
// eslint-disable-next-line no-restricted-imports -- FIXME
|
||||||
import { ResourcesResponse } from 'teleport/services/agents';
|
import { ResourcesResponse } from 'teleport/services/agents';
|
||||||
@@ -147,7 +148,11 @@ export function useKeyBasedPagination<T>({
|
|||||||
}
|
}
|
||||||
setState({
|
setState({
|
||||||
...stateRef.current,
|
...stateRef.current,
|
||||||
attempt: { status: 'failed', statusText: err.message, statusCode },
|
attempt: {
|
||||||
|
status: 'failed',
|
||||||
|
statusText: getErrorMessage(err),
|
||||||
|
statusCode,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import init, {
|
|||||||
} from 'shared/libs/ironrdp/pkg/ironrdp';
|
} from 'shared/libs/ironrdp/pkg/ironrdp';
|
||||||
// Inlines the wasm module as a static asset bundled with our app.
|
// Inlines the wasm module as a static asset bundled with our app.
|
||||||
import wasmUrl from 'shared/libs/ironrdp/pkg/ironrdp_bg.wasm?inline';
|
import wasmUrl from 'shared/libs/ironrdp/pkg/ironrdp_bg.wasm?inline';
|
||||||
import { ensureError, isAbortError } from 'shared/utils/error';
|
import { ensureError, getErrorMessage, isAbortError } from 'shared/utils/error';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
@@ -737,7 +737,7 @@ export class TdpClient extends EventEmitter<EventMap> {
|
|||||||
path: req.path,
|
path: req.path,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.handleWarning(e.message, TdpClientEvent.CLIENT_WARNING);
|
this.handleWarning(getErrorMessage(e), TdpClientEvent.CLIENT_WARNING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -762,7 +762,7 @@ export class TdpClient extends EventEmitter<EventMap> {
|
|||||||
directoryId: req.directoryId,
|
directoryId: req.directoryId,
|
||||||
errCode: SharedDirectoryErrCode.Failed,
|
errCode: SharedDirectoryErrCode.Failed,
|
||||||
});
|
});
|
||||||
this.handleWarning(e.message, TdpClientEvent.CLIENT_WARNING);
|
this.handleWarning(getErrorMessage(e), TdpClientEvent.CLIENT_WARNING);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ensureError, isAbortError } from './error';
|
import { ensureError, isAbortError, isErrnoException } from './error';
|
||||||
|
|
||||||
class CustomErrorClass extends Error {
|
class CustomErrorClass extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
@@ -115,6 +115,81 @@ describe('isAbortError', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isErrnoException', () => {
|
||||||
|
const cases: {
|
||||||
|
name: string;
|
||||||
|
input: unknown;
|
||||||
|
code: string;
|
||||||
|
expected: boolean;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: 'Error with a matching code',
|
||||||
|
input: Object.assign(new Error('failed'), { code: 'ENOENT' }),
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'AbortError with a matching code',
|
||||||
|
input: Object.assign(new Error('aborted'), {
|
||||||
|
name: 'AbortError',
|
||||||
|
code: 'ABORT_ERR',
|
||||||
|
}),
|
||||||
|
code: 'ABORT_ERR',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Error with a non-matching code',
|
||||||
|
input: Object.assign(new Error('failed'), { code: 'EPERM' }),
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Error without a code, expected code',
|
||||||
|
input: new Error('failed'),
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'serialized error with a code',
|
||||||
|
input: {
|
||||||
|
name: 'Error',
|
||||||
|
message: 'failed',
|
||||||
|
code: 'ENOENT',
|
||||||
|
},
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'object with a non-string code',
|
||||||
|
input: { code: 123 },
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'string',
|
||||||
|
input: 'ENOENT',
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'null',
|
||||||
|
input: null,
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'undefined',
|
||||||
|
input: undefined,
|
||||||
|
code: 'ENOENT',
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.each(cases)('$name', ({ input, code, expected }) => {
|
||||||
|
expect(isErrnoException(input, code)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function newDOMAbortError() {
|
function newDOMAbortError() {
|
||||||
return new DOMException('Aborted', 'AbortError');
|
return new DOMException('Aborted', 'AbortError');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,3 +83,20 @@ export class AbortError extends DOMException {
|
|||||||
super(message, 'AbortError');
|
super(message, 'AbortError');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether an error has the expected Node.js system error code (errno exception).
|
||||||
|
* Works with both Error instances and serialized errors.
|
||||||
|
*
|
||||||
|
* This uses the conventional `error.code` check rather than attempting to
|
||||||
|
* identify the error's runtime class. Callers should pass a known system error
|
||||||
|
* code such as `ENOENT`, `EPERM`, or `ESRCH`.
|
||||||
|
*/
|
||||||
|
export function isErrnoException(error: unknown, code: string): boolean {
|
||||||
|
return (
|
||||||
|
!!error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
'code' in error &&
|
||||||
|
error.code === code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useLocation, useParams } from 'react-router';
|
|||||||
import { Flex, Indicator } from 'design';
|
import { Flex, Indicator } from 'design';
|
||||||
import { AccessDenied } from 'design/CardError';
|
import { AccessDenied } from 'design/CardError';
|
||||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import AuthnDialog from 'teleport/components/AuthnDialog';
|
import AuthnDialog from 'teleport/components/AuthnDialog';
|
||||||
import { CreateAppSessionParams, UrlLauncherParams } from 'teleport/config';
|
import { CreateAppSessionParams, UrlLauncherParams } from 'teleport/config';
|
||||||
@@ -207,7 +208,7 @@ export function AppLauncher({
|
|||||||
// `fetch` returns `TypeError` when there is a network error.
|
// `fetch` returns `TypeError` when there is a network error.
|
||||||
statusText = `Unable to access "${fqdn}". This may happen if your Teleport Proxy is using an untrusted or self-signed certificate. Please ensure Teleport Proxy service uses a valid certificate or access the application domain directly (https://${fqdn}${port}) and accept the certificate exception from your browser.`;
|
statusText = `Unable to access "${fqdn}". This may happen if your Teleport Proxy is using an untrusted or self-signed certificate. Please ensure Teleport Proxy service uses a valid certificate or access the application domain directly (https://${fqdn}${port}) and accept the certificate exception from your browser.`;
|
||||||
} else if (isRedirectFlow) {
|
} else if (isRedirectFlow) {
|
||||||
statusText = `Error while authenticating a required app: ${err.message}`;
|
statusText = `Error while authenticating a required app: ${getErrorMessage(err)}`;
|
||||||
} else if (err instanceof Error) {
|
} else if (err instanceof Error) {
|
||||||
statusText = err.message;
|
statusText = err.message;
|
||||||
}
|
}
|
||||||
@@ -354,6 +355,6 @@ function getNewAuthExchangeUrl({
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function throwFailedToParseUrlError(err: TypeError) {
|
function throwFailedToParseUrlError(err: unknown) {
|
||||||
throw Error(`Failed to parse URL: ${err.message}`);
|
throw Error(`Failed to parse URL: ${getErrorMessage(err)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import Link from 'design/Link';
|
|||||||
import FieldInput from 'shared/components/FieldInput';
|
import FieldInput from 'shared/components/FieldInput';
|
||||||
import Select from 'shared/components/Select';
|
import Select from 'shared/components/Select';
|
||||||
import Validation, { Validator } from 'shared/components/Validation';
|
import Validation, { Validator } from 'shared/components/Validation';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
|
|
||||||
@@ -308,10 +309,16 @@ const EnterpriseHostError = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const InvalidHostError = ({ rule, error }: { rule: string; error: string }) => {
|
const InvalidHostError = ({
|
||||||
|
rule,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
rule: string;
|
||||||
|
error: unknown;
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
Invalid address {rule}: {error}
|
Invalid address {rule}: {getErrorMessage(error)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
|
|
||||||
import { Option } from 'shared/components/Select';
|
import { Option } from 'shared/components/Select';
|
||||||
import { Rule } from 'shared/components/Validation/rules';
|
import { Rule } from 'shared/components/Validation/rules';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { RefType } from 'teleport/services/bot/types';
|
import { RefType } from 'teleport/services/bot/types';
|
||||||
|
|
||||||
@@ -113,6 +114,6 @@ export const requireValidRepository: Rule = value => () => {
|
|||||||
|
|
||||||
return { valid: true };
|
return { valid: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { valid: false, message: e?.message };
|
return { valid: false, message: getErrorMessage(e) };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { useParams } from 'react-router';
|
|||||||
import { Flex, Indicator } from 'design';
|
import { Flex, Indicator } from 'design';
|
||||||
import { AccessDenied, BadRequest } from 'design/CardError';
|
import { AccessDenied, BadRequest } from 'design/CardError';
|
||||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import AuthnDialog from 'teleport/components/AuthnDialog';
|
import AuthnDialog from 'teleport/components/AuthnDialog';
|
||||||
import { useMfa, shouldShowMfaPrompt } from 'teleport/lib/useMfa';
|
import { useMfa, shouldShowMfaPrompt } from 'teleport/lib/useMfa';
|
||||||
@@ -74,7 +75,7 @@ export function BrowserMfa({ onRedirect = redirectTo }: BrowserMfaProps) {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
setAttempt({
|
setAttempt({
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
statusText: err.message,
|
statusText: getErrorMessage(err),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -26,6 +26,7 @@ import { IconTooltip } from 'design/Tooltip';
|
|||||||
import TextEditor from 'shared/components/TextEditor';
|
import TextEditor from 'shared/components/TextEditor';
|
||||||
import Validation, { Validator } from 'shared/components/Validation';
|
import Validation, { Validator } from 'shared/components/Validation';
|
||||||
import { useAsync } from 'shared/hooks/useAsync';
|
import { useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
|
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
@@ -79,7 +80,7 @@ export function CreateAppAccess() {
|
|||||||
resourceName: app.name,
|
resourceName: app.name,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
emitErrorEvent(err.message);
|
emitErrorEvent(getErrorMessage(err));
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -330,7 +330,7 @@ export function useCreateDatabase() {
|
|||||||
setAttempt({ status: '' });
|
setAttempt({ status: '' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRequestError(err: Error, preErrMsg = '') {
|
function handleRequestError(err: unknown, preErrMsg = '') {
|
||||||
const message = getErrMessage(err);
|
const message = getErrMessage(err);
|
||||||
setAttempt({ status: 'failed', statusText: `${preErrMsg}${message}` });
|
setAttempt({ status: 'failed', statusText: `${preErrMsg}${message}` });
|
||||||
emitErrorEvent(`${preErrMsg}${message}`);
|
emitErrorEvent(`${preErrMsg}${message}`);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
|
|||||||
import Validation, { Validator } from 'shared/components/Validation';
|
import Validation, { Validator } from 'shared/components/Validation';
|
||||||
import { Rule } from 'shared/components/Validation/rules';
|
import { Rule } from 'shared/components/Validation/rules';
|
||||||
import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { LabelsInput } from 'teleport/components/LabelsInput';
|
import { LabelsInput } from 'teleport/components/LabelsInput';
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
@@ -176,7 +177,7 @@ export function DiscoveryConfigSsm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
emitErrorEvent(err.message);
|
emitErrorEvent(getErrorMessage(err));
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -19,6 +19,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDatabaseProtocol,
|
getDatabaseProtocol,
|
||||||
@@ -107,7 +108,7 @@ export function useConnectionDiagnostic() {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err);
|
handleError(err);
|
||||||
emitErrorEvent(err.message);
|
emitErrorEvent(getErrorMessage(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mfaRequired: false };
|
return { mfaRequired: false };
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import Dialog, {
|
|||||||
} from 'design/Dialog';
|
} from 'design/Dialog';
|
||||||
import Validation, { Validator } from 'shared/components/Validation';
|
import Validation, { Validator } from 'shared/components/Validation';
|
||||||
import { Attempt, useAsync } from 'shared/hooks/useAsync';
|
import { Attempt, useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { CatchError } from 'teleport/components/CatchError';
|
import { CatchError } from 'teleport/components/CatchError';
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
@@ -417,12 +418,16 @@ const AttemptAlert = ({ attempt }: { attempt?: Attempt<unknown> }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** Renders an alert if there is an error. */
|
/** Renders an alert if there is an error. */
|
||||||
const ErrorAlert = ({ error }: { error: Error }) =>
|
const ErrorAlert = ({ error }: { error: unknown }) => {
|
||||||
error && (
|
const details = error instanceof Error ? error.cause?.toString() : '';
|
||||||
<Danger mt={3} dismissible details={error.cause?.toString()}>
|
return (
|
||||||
{error.message}
|
error && (
|
||||||
</Danger>
|
<Danger mt={3} dismissible details={details}>
|
||||||
|
{getErrorMessage(error)}
|
||||||
|
</Danger>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ShowHide = styled(Flex)<{ hidden: boolean }>`
|
const ShowHide = styled(Flex)<{ hidden: boolean }>`
|
||||||
display: ${props => (props.hidden ? 'none' : '')};
|
display: ${props => (props.hidden ? 'none' : '')};
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export class TtyPlayer extends Player<TtyEvent> {
|
|||||||
this.terminal.loadAddon(imageAddon);
|
this.terminal.loadAddon(imageAddon);
|
||||||
this.addons.push(imageAddon);
|
this.addons.push(imageAddon);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.error(`Failed to load image addon: ${e.message}`);
|
this.logger.error('Failed to load image addon', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
let webglAddon: WebglAddon | undefined;
|
let webglAddon: WebglAddon | undefined;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import styled from 'styled-components';
|
|||||||
import { Box, ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design';
|
import { Box, ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design';
|
||||||
import { ChevronDown } from 'design/Icon';
|
import { ChevronDown } from 'design/Icon';
|
||||||
import { HoverTooltip } from 'design/Tooltip';
|
import { HoverTooltip } from 'design/Tooltip';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
import { Cluster } from 'teleport/services/clusters';
|
import { Cluster } from 'teleport/services/clusters';
|
||||||
@@ -84,7 +85,7 @@ export function ClusterDropdown({
|
|||||||
try {
|
try {
|
||||||
return clusterLoader.fetchClusters(signal);
|
return clusterLoader.fetchClusters(signal);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError(err.message);
|
onError(getErrorMessage(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ export function ClusterDropdown({
|
|||||||
const res = await loadClusters(signal.signal);
|
const res = await loadClusters(signal.signal);
|
||||||
setOptions(createOptions(res));
|
setOptions(createOptions(res));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
onError(err.message);
|
onError(getErrorMessage(err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import auth from 'teleport/services/auth';
|
import auth from 'teleport/services/auth';
|
||||||
import { MfaChallengeScope } from 'teleport/services/auth/auth';
|
import { MfaChallengeScope } from 'teleport/services/auth/auth';
|
||||||
@@ -145,17 +146,18 @@ type challengeState = {
|
|||||||
deviceUsage: DeviceUsage;
|
deviceUsage: DeviceUsage;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getReAuthenticationErrorMessage(err: Error): string {
|
function getReAuthenticationErrorMessage(err: unknown): string {
|
||||||
if (err.message.includes('attempt was made to use an object that is not')) {
|
const message = getErrorMessage(err);
|
||||||
|
if (message.includes('attempt was made to use an object that is not')) {
|
||||||
// Catch a webauthn frontend error that occurs on Firefox and replace it with a more helpful error message.
|
// Catch a webauthn frontend error that occurs on Firefox and replace it with a more helpful error message.
|
||||||
return 'The two-factor device you used is not registered on this account. You must verify using a device that has already been registered.';
|
return 'The two-factor device you used is not registered on this account. You must verify using a device that has already been registered.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err.message === 'invalid totp token') {
|
if (message === 'invalid totp token') {
|
||||||
// This message relies on the status message produced by the auth server in
|
// This message relies on the status message produced by the auth server in
|
||||||
// lib/auth/Server.checkOTP function. Please keep these in sync.
|
// lib/auth/Server.checkOTP function. Please keep these in sync.
|
||||||
return 'Invalid authenticator code';
|
return 'Invalid authenticator code';
|
||||||
}
|
}
|
||||||
|
|
||||||
return err.message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default class TtyTerminal implements TerminalSearcher {
|
|||||||
this._imageAddon = new ImageAddon();
|
this._imageAddon = new ImageAddon();
|
||||||
this.term.loadAddon(this._imageAddon);
|
this.term.loadAddon(this._imageAddon);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Failed to load image addon:', e.message);
|
logger.error('Failed to load image addon', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
makeEmptyAttempt,
|
makeEmptyAttempt,
|
||||||
useAsync,
|
useAsync,
|
||||||
} from 'shared/hooks/useAsync';
|
} from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender';
|
import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender';
|
||||||
import { TermEvent } from 'teleport/lib/term/enums';
|
import { TermEvent } from 'teleport/lib/term/enums';
|
||||||
@@ -163,7 +164,7 @@ export function useMfa(props?: MfaProps): MfaState {
|
|||||||
setMfaAttempt({
|
setMfaAttempt({
|
||||||
data: null,
|
data: null,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
statusText: err.message,
|
statusText: getErrorMessage(err),
|
||||||
error: err,
|
error: err,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import 'whatwg-fetch';
|
import 'whatwg-fetch';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import auth, { MfaChallengeScope } from 'teleport/services/auth/auth';
|
import auth, { MfaChallengeScope } from 'teleport/services/auth/auth';
|
||||||
import websession from 'teleport/services/websession';
|
import websession from 'teleport/services/websession';
|
||||||
|
|
||||||
@@ -293,7 +295,7 @@ const api = {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// error reading JSON
|
// error reading JSON
|
||||||
const message = response.ok
|
const message = response.ok
|
||||||
? err.message
|
? getErrorMessage(err)
|
||||||
: `${response.status} - ${response.url}`;
|
: `${response.status} - ${response.url}`;
|
||||||
throw new ApiError({ message, response, opts: { cause: err } });
|
throw new ApiError({ message, response, opts: { cause: err } });
|
||||||
}
|
}
|
||||||
@@ -455,8 +457,8 @@ export function getHostName() {
|
|||||||
return location.hostname + (location.port ? ':' + location.port : '');
|
return location.hostname + (location.port ? ':' + location.port : '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAdminActionRequiresMfaError(err: Error) {
|
export function isAdminActionRequiresMfaError(err: unknown) {
|
||||||
return err.message.includes(
|
return getErrorMessage(err).includes(
|
||||||
'admin-level API request requires MFA verification'
|
'admin-level API request requires MFA verification'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { MutationFunction } from '@tanstack/react-query';
|
|||||||
|
|
||||||
import cfg from 'teleport/config';
|
import cfg from 'teleport/config';
|
||||||
import api from 'teleport/services/api';
|
import api from 'teleport/services/api';
|
||||||
|
import { ApiError } from 'teleport/services/api/parseError';
|
||||||
import {
|
import {
|
||||||
canUseV1Edit,
|
canUseV1Edit,
|
||||||
canUseV2Edit,
|
canUseV2Edit,
|
||||||
@@ -67,7 +68,7 @@ export async function getBot(
|
|||||||
.then(makeBot);
|
.then(makeBot);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// capture the not found error response and return null instead of throwing
|
// capture the not found error response and return null instead of throwing
|
||||||
if (err?.response?.status === 404) {
|
if (err instanceof ApiError && err.response.status === 404) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { getErrMessage } from 'shared/utils/errorType';
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { App } from 'teleport/services/apps/types';
|
import { App } from 'teleport/services/apps/types';
|
||||||
import {
|
import {
|
||||||
@@ -124,7 +124,7 @@ export const withUnsupportedOktaPluginUpdateErrorConversion = (
|
|||||||
err: unknown
|
err: unknown
|
||||||
) => {
|
) => {
|
||||||
if (err instanceof ApiError && err.response.status === 404) {
|
if (err instanceof ApiError && err.response.status === 404) {
|
||||||
const msg = getErrMessage(err);
|
const msg = getErrorMessage(err);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not update Okta plugin: ${msg}. Your proxy may be behind the minimum required version (v17.3.0) to support Okta plugin updates with this web client.`
|
`Could not update Okta plugin: ${msg}. Your proxy may be behind the minimum required version (v17.3.0) to support Okta plugin updates with this web client.`
|
||||||
);
|
);
|
||||||
@@ -136,7 +136,7 @@ export const withUnsupportedOktaPluginCreateErrorConversion = (
|
|||||||
err: unknown
|
err: unknown
|
||||||
) => {
|
) => {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
const msg = getErrMessage(err);
|
const msg = getErrorMessage(err);
|
||||||
if (msg.match(/missing okta (?:organization url|api token)/gi)) {
|
if (msg.match(/missing okta (?:organization url|api token)/gi)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not create Okta plugin: ${msg}. Your proxy may be behind the minimum required version (v17.3.0) to support Okta plugin creation with this web client.`
|
`Could not create Okta plugin: ${msg}. Your proxy may be behind the minimum required version (v17.3.0) to support Okta plugin creation with this web client.`
|
||||||
@@ -147,7 +147,7 @@ export const withUnsupportedOktaPluginCreateErrorConversion = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Base = {
|
type Base = {
|
||||||
err: Error;
|
err: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CreateJoinToken = Base & {
|
type CreateJoinToken = Base & {
|
||||||
@@ -212,7 +212,10 @@ export function useV1Fallback() {
|
|||||||
async function tryV1Fallback(props: CreateJoinToken): Promise<JoinToken>;
|
async function tryV1Fallback(props: CreateJoinToken): Promise<JoinToken>;
|
||||||
|
|
||||||
async function tryV1Fallback(props: FallbackProps) {
|
async function tryV1Fallback(props: FallbackProps) {
|
||||||
if (!props.err.message.includes(ProxyRequiresUpgrade) || hasLabels(props)) {
|
if (
|
||||||
|
!getErrorMessage(props.err).includes(ProxyRequiresUpgrade) ||
|
||||||
|
hasLabels(props)
|
||||||
|
) {
|
||||||
throw props.err;
|
throw props.err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { tmpdir } from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
|
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
|
|
||||||
const stdio = 'pipe'; // Change to 'inherit' for easier debugging.
|
const stdio = 'pipe'; // Change to 'inherit' for easier debugging.
|
||||||
|
|
||||||
let logsDir: string;
|
let logsDir: string;
|
||||||
@@ -197,7 +199,7 @@ const isRunning = (pid: number) => {
|
|||||||
try {
|
try {
|
||||||
return process.kill(pid, 0);
|
return process.kill(pid, 0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ESRCH') {
|
if (isErrnoException(error, 'ESRCH')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export type DeepLinkParseResult =
|
|||||||
// ergonomic. Unfortunately, `if (!result.ok)` doesn't narrow down the type properly with
|
// ergonomic. Unfortunately, `if (!result.ok)` doesn't narrow down the type properly with
|
||||||
// strictNullChecks off. https://github.com/microsoft/TypeScript/issues/10564
|
// strictNullChecks off. https://github.com/microsoft/TypeScript/issues/10564
|
||||||
| DeepLinkParseResultSuccess
|
| DeepLinkParseResultSuccess
|
||||||
| ParseError<'malformed-url', { error: TypeError }>
|
| ParseError<'malformed-url', { error: unknown }>
|
||||||
| ParseError<'unknown-protocol', { protocol: string }>
|
| ParseError<'unknown-protocol', { protocol: string }>
|
||||||
| ParseError<'unsupported-url'>;
|
| ParseError<'unsupported-url'>;
|
||||||
|
|
||||||
@@ -69,9 +69,11 @@ export function parseDeepLink(rawUrl: string): DeepLinkParseResult {
|
|||||||
try {
|
try {
|
||||||
parsedURL = new URL(rawUrl);
|
parsedURL = new URL(rawUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// `error instanceof TypeError` doesn't work in tests. The URL constructor shouldn't throw other
|
return {
|
||||||
// errors anyway.
|
status: 'error',
|
||||||
return { status: 'error', reason: 'malformed-url', error };
|
reason: 'malformed-url',
|
||||||
|
error,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsedURL.protocol !== `${CUSTOM_PROTOCOL}:`) {
|
if (parsedURL.protocol !== `${CUSTOM_PROTOCOL}:`) {
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ import path from 'node:path';
|
|||||||
import { app, dialog, nativeTheme } from 'electron';
|
import { app, dialog, nativeTheme } from 'electron';
|
||||||
|
|
||||||
import { CUSTOM_PROTOCOL } from 'shared/deepLinks';
|
import { CUSTOM_PROTOCOL } from 'shared/deepLinks';
|
||||||
import { ensureError } from 'shared/utils/error';
|
import {
|
||||||
|
ensureError,
|
||||||
|
getErrorMessage,
|
||||||
|
isErrnoException,
|
||||||
|
} from 'shared/utils/error';
|
||||||
|
|
||||||
import { parseDeepLink } from 'teleterm/deepLinks';
|
import { parseDeepLink } from 'teleterm/deepLinks';
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
@@ -366,7 +370,7 @@ function launchDeepLink(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'malformed-url': {
|
case 'malformed-url': {
|
||||||
reason = `malformed URL (${result.error.message})`;
|
reason = `malformed URL (${getErrorMessage(result.error)})`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -429,7 +433,7 @@ async function migrateOldTshHomeOnce(
|
|||||||
try {
|
try {
|
||||||
await fs.stat(oldTshHome);
|
await fs.stat(oldTshHome);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (isErrnoException(err, 'ENOENT')) {
|
||||||
logger.info(
|
logger.info(
|
||||||
'Old tsh directory does not exist, marking migration as processed'
|
'Old tsh directory does not exist, marking migration as processed'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { createUnzip } from 'node:zlib';
|
|||||||
|
|
||||||
import { extract } from 'tar-fs';
|
import { extract } from 'tar-fs';
|
||||||
|
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
import { compareSemVers } from 'shared/utils/semVer';
|
import { compareSemVers } from 'shared/utils/semVer';
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
@@ -149,7 +150,7 @@ async function isCorrectAgentVersionAlreadyDownloaded(
|
|||||||
return agentVersion.stdout.trim() === neededVersion;
|
return agentVersion.stdout.trim() === neededVersion;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// When the agent is being downloaded for the first time, the binary does not yet exist.
|
// When the agent is being downloaded for the first time, the binary does not yet exist.
|
||||||
if (e.code !== 'ENOENT') {
|
if (!isErrnoException(e, 'ENOENT')) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+1
-2
@@ -20,7 +20,6 @@ import {
|
|||||||
Cluster,
|
Cluster,
|
||||||
LoggedInUser,
|
LoggedInUser,
|
||||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||||
import { ensureError } from 'shared/utils/error';
|
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
import type { IAwaitableSender } from 'teleterm/mainProcess/awaitableSender';
|
import type { IAwaitableSender } from 'teleterm/mainProcess/awaitableSender';
|
||||||
@@ -381,7 +380,7 @@ export class ClusterLifecycleManager {
|
|||||||
);
|
);
|
||||||
const serialized: ProfileWatcherError = {
|
const serialized: ProfileWatcherError = {
|
||||||
reason: watcherError.reason,
|
reason: watcherError.reason,
|
||||||
error: serializeError(ensureError(watcherError.error)),
|
error: serializeError(watcherError.error),
|
||||||
};
|
};
|
||||||
this.windowsManager
|
this.windowsManager
|
||||||
.getWindow()
|
.getWindow()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import path from 'node:path';
|
|||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
import * as connectMyComputer from 'shared/connectMyComputer';
|
import * as connectMyComputer from 'shared/connectMyComputer';
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
|
|
||||||
import { RuntimeSettings } from 'teleterm/mainProcess/types';
|
import { RuntimeSettings } from 'teleterm/mainProcess/types';
|
||||||
import { RootClusterUri, routing } from 'teleterm/ui/uri';
|
import { RootClusterUri, routing } from 'teleterm/ui/uri';
|
||||||
@@ -127,7 +128,7 @@ export async function isAgentConfigFileCreated(
|
|||||||
await fs.access(configFile);
|
await fs.access(configFile);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.code === 'ENOENT') {
|
if (isErrnoException(e, 'ENOENT')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
|||||||
@@ -64,3 +64,17 @@ test('serializes and deserializes RPC error', () => {
|
|||||||
'is-resolvable-with-relogin': ['1'],
|
'is-resolvable-with-relogin': ['1'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('serializing an already serialized RPC error preserves custom fields', () => {
|
||||||
|
const err = new RpcError('Session expired', 'UNAUTHENTICATED', {
|
||||||
|
'is-resolvable-with-relogin': ['1'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const serialized = serializeError(err);
|
||||||
|
const serializedAgain = serializeError(structuredClone(serialized));
|
||||||
|
|
||||||
|
expect(serializedAgain['code']).toBe('UNAUTHENTICATED');
|
||||||
|
expect(serializedAgain['meta']).toEqual({
|
||||||
|
'is-resolvable-with-relogin': ['1'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -16,16 +16,34 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ensureError } from 'shared/utils/error';
|
||||||
|
|
||||||
export type SerializedError = {
|
export type SerializedError = {
|
||||||
name: string;
|
name: string;
|
||||||
message: string;
|
message: string;
|
||||||
stack?: string;
|
stack?: string;
|
||||||
cause?: unknown;
|
cause?: unknown;
|
||||||
toStringResult?: string;
|
toStringResult?: string;
|
||||||
|
$serializedError?: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Serializes an Error into a plain object for transport through Electron IPC. */
|
function isSerialized(error: unknown): error is SerializedError {
|
||||||
export function serializeError(error: Error): SerializedError {
|
return (
|
||||||
|
!!error &&
|
||||||
|
typeof error === 'object' &&
|
||||||
|
error['$serializedError'] === true &&
|
||||||
|
typeof error['name'] === 'string' &&
|
||||||
|
typeof error['message'] === 'string'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serializes an error into a plain object for transport through Electron IPC. */
|
||||||
|
export function serializeError(error: unknown): SerializedError {
|
||||||
|
if (isSerialized(error)) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorInstance = ensureError(error);
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
cause,
|
cause,
|
||||||
@@ -34,7 +52,7 @@ export function serializeError(error: Error): SerializedError {
|
|||||||
// functions must be skipped, otherwise structuredClone will fail to clone the object
|
// functions must be skipped, otherwise structuredClone will fail to clone the object
|
||||||
toString,
|
toString,
|
||||||
...enumerableFields
|
...enumerableFields
|
||||||
} = error;
|
} = errorInstance;
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
message,
|
message,
|
||||||
@@ -42,14 +60,23 @@ export function serializeError(error: Error): SerializedError {
|
|||||||
stack,
|
stack,
|
||||||
// Calling the destructured function directly could result in the following error:
|
// Calling the destructured function directly could result in the following error:
|
||||||
// Method Error.prototype.toString called on incompatible receiver undefined
|
// Method Error.prototype.toString called on incompatible receiver undefined
|
||||||
toStringResult: error.toString?.(),
|
toStringResult: errorInstance.toString?.(),
|
||||||
...enumerableFields,
|
...enumerableFields,
|
||||||
|
$serializedError: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Deserializes a plain object back into an Error instance. */
|
/** Deserializes a plain object back into an Error instance. */
|
||||||
export function deserializeError(serialized: SerializedError): Error {
|
export function deserializeError(serialized: SerializedError): Error {
|
||||||
const { name, cause, stack, message, toStringResult, ...rest } = serialized;
|
const {
|
||||||
|
name,
|
||||||
|
cause,
|
||||||
|
stack,
|
||||||
|
message,
|
||||||
|
toStringResult,
|
||||||
|
$serializedError,
|
||||||
|
...rest
|
||||||
|
} = serialized;
|
||||||
const error = new Error(message);
|
const error = new Error(message);
|
||||||
error.name = name;
|
error.name = name;
|
||||||
error.cause = cause;
|
error.cause = cause;
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
const UUID_V4_REGEX =
|
const UUID_V4_REGEX =
|
||||||
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
|
/^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
|
||||||
|
|
||||||
@@ -46,7 +48,7 @@ function writeInstallationId(filePath: string): string {
|
|||||||
fs.writeFileSync(filePath, newId);
|
fs.writeFileSync(filePath, newId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Could not write installation_id to ${filePath}, ${error.message}`,
|
`Could not write installation_id to ${filePath}, ${getErrorMessage(error)}`,
|
||||||
{ cause: error }
|
{ cause: error }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,6 @@
|
|||||||
|
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
import { ensureError } from 'shared/utils/error';
|
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
import type { Message, MessageAck } from 'teleterm/mainProcess/awaitableSender';
|
import type { Message, MessageAck } from 'teleterm/mainProcess/awaitableSender';
|
||||||
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
|
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
|
||||||
@@ -325,7 +323,7 @@ function startAwaitableSenderListener<T>(
|
|||||||
try {
|
try {
|
||||||
await listener(msg.payload as T);
|
await listener(msg.payload as T);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ack.error = serializeError(ensureError(e));
|
ack.error = serializeError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
import { wait } from 'shared/utils/wait';
|
import { wait } from 'shared/utils/wait';
|
||||||
|
|
||||||
import Logger, { NullService } from 'teleterm/logger';
|
import Logger, { NullService } from 'teleterm/logger';
|
||||||
@@ -65,7 +66,7 @@ async function mockTshClient(tshDir: string, initial: { clusters: Cluster[] }) {
|
|||||||
try {
|
try {
|
||||||
paths = await fs.readdir(tshDir);
|
paths = await fs.readdir(tshDir);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.code === 'ENOENT') {
|
if (isErrnoException(err, 'ENOENT')) {
|
||||||
throw {
|
throw {
|
||||||
name: 'RpcError',
|
name: 'RpcError',
|
||||||
code: 'NOT_FOUND',
|
code: 'NOT_FOUND',
|
||||||
@@ -88,7 +89,7 @@ async function mockTshClient(tshDir: string, initial: { clusters: Cluster[] }) {
|
|||||||
// The file with the cluster disappeared between fs.readdir above and fs.readFile.
|
// The file with the cluster disappeared between fs.readdir above and fs.readFile.
|
||||||
// This is possible in tests where we call `void tshClientMock.removeCluster` without
|
// This is possible in tests where we call `void tshClientMock.removeCluster` without
|
||||||
// awaiting.
|
// awaiting.
|
||||||
if (err.code === 'ENOENT') {
|
if (isErrnoException(err, 'ENOENT')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { watch, type WatchEventType } from 'node:fs';
|
|||||||
import { access } from 'node:fs/promises';
|
import { access } from 'node:fs/promises';
|
||||||
|
|
||||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
import { debounce } from 'shared/utils/highbar';
|
import { debounce } from 'shared/utils/highbar';
|
||||||
import { wait } from 'shared/utils/wait';
|
import { wait } from 'shared/utils/wait';
|
||||||
|
|
||||||
@@ -129,7 +130,7 @@ export async function* watchProfiles({
|
|||||||
if (
|
if (
|
||||||
isTshdRpcError(error, 'NOT_FOUND') ||
|
isTshdRpcError(error, 'NOT_FOUND') ||
|
||||||
error instanceof FileSystemEventsOverflowError ||
|
error instanceof FileSystemEventsOverflowError ||
|
||||||
error?.code === 'EPERM'
|
isErrnoException(error, 'EPERM')
|
||||||
) {
|
) {
|
||||||
const ok = await pathExists(tshDirectory);
|
const ok = await pathExists(tshDirectory);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@@ -161,7 +162,7 @@ async function pathExists(dirPath: string): Promise<boolean> {
|
|||||||
await access(dirPath);
|
await access(dirPath);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 'ENOENT') {
|
if (isErrnoException(error, 'ENOENT')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ export class WindowsManager {
|
|||||||
await this.whenFrontendAppIsReady();
|
await this.whenFrontendAppIsReady();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Could not send the deep link to the frontend app: ${error.message}`
|
'Could not send the deep link to the frontend app',
|
||||||
|
error
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { NsisUpdater } from 'electron-updater';
|
|||||||
import { DownloadUpdateOptions } from 'electron-updater/out/AppUpdater';
|
import { DownloadUpdateOptions } from 'electron-updater/out/AppUpdater';
|
||||||
import { InstallOptions } from 'electron-updater/out/BaseUpdater';
|
import { InstallOptions } from 'electron-updater/out/BaseUpdater';
|
||||||
|
|
||||||
import { getErrorMessage } from 'shared/utils/error';
|
import { ensureError, getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TSH_AUTOUPDATE_ENV_VAR,
|
TSH_AUTOUPDATE_ENV_VAR,
|
||||||
@@ -100,7 +100,7 @@ export class NsisDualModeUpdater extends NsisUpdater {
|
|||||||
[TSH_AUTOUPDATE_ENV_VAR]: TSH_AUTOUPDATE_OFF,
|
[TSH_AUTOUPDATE_ENV_VAR]: TSH_AUTOUPDATE_OFF,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.dispatchError(error);
|
this.dispatchError(ensureError(error));
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
if (!errorMessage.includes('failed to ensure service is running')) {
|
if (!errorMessage.includes('failed to ensure service is running')) {
|
||||||
// If not a problem with starting the service, keep the app open and surface the error in the UI.
|
// If not a problem with starting the service, keep the app open and surface the error in the UI.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const logger = new Logger('ConfigService');
|
|||||||
|
|
||||||
export type FileLoadingError = {
|
export type FileLoadingError = {
|
||||||
source: 'file-loading';
|
source: 'file-loading';
|
||||||
error: Error;
|
error: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ValidationError = {
|
export type ValidationError = {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import fs from 'node:fs';
|
|||||||
import fsAsync from 'node:fs/promises';
|
import fsAsync from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
import { debounce } from 'shared/utils/highbar';
|
import { debounce } from 'shared/utils/highbar';
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
@@ -52,7 +53,7 @@ export interface FileStorage {
|
|||||||
getFileName(): string;
|
getFileName(): string;
|
||||||
|
|
||||||
/** Returns the error that could occur while reading and parsing the file. */
|
/** Returns the error that could occur while reading and parsing the file. */
|
||||||
getFileLoadingError(): Error | undefined;
|
getFileLoadingError(): unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,7 +81,7 @@ export function createFileStorage(opts: {
|
|||||||
|
|
||||||
const { filePath } = opts;
|
const { filePath } = opts;
|
||||||
|
|
||||||
let state: any, error: Error | undefined;
|
let state: any, error: unknown;
|
||||||
try {
|
try {
|
||||||
state = loadStateSync(filePath);
|
state = loadStateSync(filePath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -127,7 +128,7 @@ export function createFileStorage(opts: {
|
|||||||
return path.basename(opts.filePath);
|
return path.basename(opts.filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFileLoadingError(): Error | undefined {
|
function getFileLoadingError(): unknown {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ function readOrCreateFileSync(filePath: string): string {
|
|||||||
try {
|
try {
|
||||||
return fs.readFileSync(filePath, { encoding: 'utf-8' });
|
return fs.readFileSync(filePath, { encoding: 'utf-8' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code === 'ENOENT') {
|
if (isErrnoException(error, 'ENOENT')) {
|
||||||
fs.writeFileSync(filePath, defaultValue);
|
fs.writeFileSync(filePath, defaultValue);
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { watch, type Stats } from 'fs';
|
|||||||
import { readFile, rename, stat, writeFile } from 'fs/promises';
|
import { readFile, rename, stat, writeFile } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
import { isErrnoException } from 'shared/utils/error';
|
||||||
import { wait } from 'shared/utils/wait';
|
import { wait } from 'shared/utils/wait';
|
||||||
|
|
||||||
import { makeCert } from './makeCert';
|
import { makeCert } from './makeCert';
|
||||||
@@ -68,7 +69,7 @@ export async function readGrpcCert(
|
|||||||
try {
|
try {
|
||||||
stats = await stat(fullPath);
|
stats = await stat(fullPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code === 'ENOENT') {
|
if (isErrnoException(error, 'ENOENT')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ import {
|
|||||||
UnaryCall,
|
UnaryCall,
|
||||||
} from '@protobuf-ts/runtime-rpc';
|
} from '@protobuf-ts/runtime-rpc';
|
||||||
|
|
||||||
import { ensureError } from 'shared/utils/error';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
serializeError,
|
serializeError,
|
||||||
type SerializedError,
|
type SerializedError,
|
||||||
@@ -314,7 +312,7 @@ export function isRpcErrorReloginResolvable(error: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cloneError(error: unknown): SerializedError {
|
function cloneError(error: unknown): SerializedError {
|
||||||
return serializeError(ensureError(error));
|
return serializeError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneRequests<O extends object>(
|
function cloneRequests<O extends object>(
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
|
|||||||
import * as nodePTY from 'node-pty';
|
import * as nodePTY from 'node-pty';
|
||||||
import which from 'which';
|
import which from 'which';
|
||||||
|
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
import { wait } from 'shared/utils/wait';
|
import { wait } from 'shared/utils/wait';
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
@@ -257,11 +258,11 @@ export class PtyProcess extends EventEmitter implements IPtyProcess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleStartError(error: Error) {
|
private handleStartError(error: unknown) {
|
||||||
const command = `${this.options.path} ${this.options.args.join(' ')}`;
|
const command = `${this.options.path} ${this.options.args.join(' ')}`;
|
||||||
this.emit(
|
this.emit(
|
||||||
TermEventEnum.StartError,
|
TermEventEnum.StartError,
|
||||||
`Cannot execute ${command}: ${error.message}`
|
`Cannot execute ${command}: ${getErrorMessage(error)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
import { Indicator } from 'design';
|
import { Indicator } from 'design';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||||
import { useLogger } from 'teleterm/ui/hooks/useLogger';
|
import { useLogger } from 'teleterm/ui/hooks/useLogger';
|
||||||
@@ -57,10 +58,10 @@ export const AppInitializer = () => {
|
|||||||
success: true,
|
success: true,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error?.message);
|
logger.error('Failed to initialize app', error);
|
||||||
|
|
||||||
setShouldShowUi(true);
|
setShouldShowUi(true);
|
||||||
appContext?.notificationsService.notifyError(error?.message);
|
appContext?.notificationsService.notifyError(getErrorMessage(error));
|
||||||
appContext?.mainProcessClient.signalUserInterfaceReadiness({
|
appContext?.mainProcessClient.signalUserInterfaceReadiness({
|
||||||
success: false,
|
success: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import styled from 'styled-components';
|
|||||||
import { Alert, Box, ButtonPrimary, Flex, H1, Text } from 'design';
|
import { Alert, Box, ButtonPrimary, Flex, H1, Text } from 'design';
|
||||||
import * as Alerts from 'design/Alert';
|
import * as Alerts from 'design/Alert';
|
||||||
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
import { wait } from 'shared/utils/wait';
|
import { wait } from 'shared/utils/wait';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -244,7 +245,7 @@ function AgentSetup() {
|
|||||||
!isRpcErrorReloginResolvable(error)
|
!isRpcErrorReloginResolvable(error)
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot set up the role: ${error.message}. Contact your administrator for permissions to manage users and roles.`,
|
`Cannot set up the role: ${getErrorMessage(error)}. Contact your administrator for permissions to manage users and roles.`,
|
||||||
{ cause: error }
|
{ cause: error }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -431,7 +432,7 @@ function AgentSetup() {
|
|||||||
const { agentBinaryPath } = mainProcessClient.getRuntimeSettings();
|
const { agentBinaryPath } = mainProcessClient.getRuntimeSettings();
|
||||||
notificationsService.notifyError({
|
notificationsService.notifyError({
|
||||||
title: 'Could not remove the agent binary',
|
title: 'Could not remove the agent binary',
|
||||||
description: `Please try removing the binary manually to continue. The binary is at ${agentBinaryPath}. The error message was: ${error.message}`,
|
description: `Please try removing the binary manually to continue. The binary is at ${agentBinaryPath}. The error message was: ${getErrorMessage(error)}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import * as icons from 'design/Icon';
|
|||||||
import type { IconProps } from 'design/Icon/Icon';
|
import type { IconProps } from 'design/Icon/Icon';
|
||||||
import Indicator from 'design/Indicator';
|
import Indicator from 'design/Indicator';
|
||||||
import { MenuIcon } from 'shared/components/MenuAction';
|
import { MenuIcon } from 'shared/components/MenuAction';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import type * as tsh from 'teleterm/services/tshd/types';
|
import type * as tsh from 'teleterm/services/tshd/types';
|
||||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||||
@@ -112,7 +113,7 @@ export function Status(props: { closeDocument?: () => void }) {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
ctx.notificationsService.notifyError({
|
ctx.notificationsService.notifyError({
|
||||||
title: 'Failed to open agent logs directory',
|
title: 'Failed to open agent logs directory',
|
||||||
description: `${e.message}\n\nNote: the logs directory is created only after the agent process successfully spawns.`,
|
description: `${getErrorMessage(e)}\n\nNote: the logs directory is created only after the agent process successfully spawns.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
DeepURL,
|
DeepURL,
|
||||||
VnetDeepURL,
|
VnetDeepURL,
|
||||||
} from 'shared/deepLinks';
|
} from 'shared/deepLinks';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { DeepLinkParseResult } from 'teleterm/deepLinks';
|
import { DeepLinkParseResult } from 'teleterm/deepLinks';
|
||||||
import { IAppContext } from 'teleterm/ui/types';
|
import { IAppContext } from 'teleterm/ui/types';
|
||||||
@@ -59,7 +60,7 @@ export async function launchDeepLink(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'malformed-url': {
|
case 'malformed-url': {
|
||||||
reason = `The URL of the link appears to be malformed. ${result.error.message}`;
|
reason = `The URL of the link appears to be malformed. ${getErrorMessage(result.error)}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
|
|||||||
@@ -238,14 +238,15 @@ export default class TtyTerminal implements TerminalSearcher {
|
|||||||
|
|
||||||
this.term.onData(data => {
|
this.term.onData(data => {
|
||||||
this.ptyProcess.write(data).catch(error => {
|
this.ptyProcess.write(data).catch(error => {
|
||||||
this.logger.error(`Failed to write to the PTY process: ${error}`);
|
this.logger.error('Failed to write to the PTY process', error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.term.onResize(size => {
|
this.term.onResize(size => {
|
||||||
this.ptyProcess.resize(size.cols, size.rows).catch(error => {
|
this.ptyProcess.resize(size.cols, size.rows).catch(error => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
`Failed to send resize request to the PTY process: ${error}`
|
'Failed to send resize request to the PTY process',
|
||||||
|
error
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -259,7 +260,7 @@ export default class TtyTerminal implements TerminalSearcher {
|
|||||||
// The shared process version of PtyProcess knows whether it was started or not (the status
|
// The shared process version of PtyProcess knows whether it was started or not (the status
|
||||||
// field), so it's a matter of exposing this field through gRPC and reading it here.
|
// field), so it's a matter of exposing this field through gRPC and reading it here.
|
||||||
this.ptyProcess.start(this.term.cols, this.term.rows).catch(error => {
|
this.ptyProcess.start(this.term.cols, this.term.rows).catch(error => {
|
||||||
this.logger.error(`Failed to start the PTY process: ${error}`);
|
this.logger.error('Failed to start the PTY process', error);
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', this.debouncedResize);
|
window.addEventListener('resize', this.debouncedResize);
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function useDocumentTerminal(doc: types.DocumentTerminal) {
|
|||||||
return () => {
|
return () => {
|
||||||
if (attempt.status === 'success') {
|
if (attempt.status === 'success') {
|
||||||
void attempt.data.ptyProcess.dispose().catch(error => {
|
void attempt.data.ptyProcess.dispose().catch(error => {
|
||||||
logger.error(`Failed to dispose of the PTY process: ${error}`);
|
logger.error('Failed to dispose of the PTY process', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { copyToClipboard } from 'design/utils/copyToClipboard';
|
|||||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||||
import * as diag from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
import * as diag from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||||
import { CanceledError, useAsync } from 'shared/hooks/useAsync';
|
import { CanceledError, useAsync } from 'shared/hooks/useAsync';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
import { pluralize } from 'shared/utils/text';
|
import { pluralize } from 'shared/utils/text';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -122,7 +123,7 @@ export function DocumentVnetDiagReport(props: {
|
|||||||
previousSaveToFileNotificationIdRef.current =
|
previousSaveToFileNotificationIdRef.current =
|
||||||
notificationsService.notifyError({
|
notificationsService.notifyError({
|
||||||
title: 'Could not save the report to a file.',
|
title: 'Could not save the report to a file.',
|
||||||
description: error?.message,
|
description: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import { IAppContext } from 'teleterm/ui/types';
|
import { IAppContext } from 'teleterm/ui/types';
|
||||||
import { ClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri';
|
import { ClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri';
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ const commands = {
|
|||||||
error => {
|
error => {
|
||||||
ctx.notificationsService.notifyError({
|
ctx.notificationsService.notifyError({
|
||||||
title: 'Could not install tsh in PATH',
|
title: 'Could not install tsh in PATH',
|
||||||
description: `Ran into an error: ${error}`,
|
description: `Ran into an error: ${getErrorMessage(error)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -57,7 +59,7 @@ const commands = {
|
|||||||
error => {
|
error => {
|
||||||
ctx.notificationsService.notifyError({
|
ctx.notificationsService.notifyError({
|
||||||
title: 'Could not remove tsh from PATH',
|
title: 'Could not remove tsh from PATH',
|
||||||
description: `Ran into an error: ${error}`,
|
description: `Ran into an error: ${getErrorMessage(error)}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import {
|
|||||||
ReviewAccessRequestRequest,
|
ReviewAccessRequestRequest,
|
||||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb';
|
} from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb';
|
||||||
import { useStore } from 'shared/libs/stores';
|
import { useStore } from 'shared/libs/stores';
|
||||||
import { AbortError, isAbortError } from 'shared/utils/error';
|
import { AbortError, getErrorMessage, isAbortError } from 'shared/utils/error';
|
||||||
|
|
||||||
import type { State as ClustersState } from 'teleterm/mainProcess/clusterStore';
|
import type { State as ClustersState } from 'teleterm/mainProcess/clusterStore';
|
||||||
import { MainProcessClient } from 'teleterm/mainProcess/types';
|
import { MainProcessClient } from 'teleterm/mainProcess/types';
|
||||||
@@ -89,7 +89,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
|||||||
|
|
||||||
const notificationId = this.notificationsService.notifyError({
|
const notificationId = this.notificationsService.notifyError({
|
||||||
title: `Could not synchronize cluster ${clusterName}`,
|
title: `Could not synchronize cluster ${clusterName}`,
|
||||||
description: e.message,
|
description: getErrorMessage(e),
|
||||||
action: {
|
action: {
|
||||||
content: 'Retry',
|
content: 'Retry',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -112,7 +112,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
|||||||
|
|
||||||
const notificationId = this.notificationsService.notifyError({
|
const notificationId = this.notificationsService.notifyError({
|
||||||
title: `Could not start headless requests watcher for ${clusterName}`,
|
title: `Could not start headless requests watcher for ${clusterName}`,
|
||||||
description: e.message,
|
description: getErrorMessage(e),
|
||||||
action: {
|
action: {
|
||||||
content: 'Retry',
|
content: 'Retry',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -166,7 +166,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
|||||||
}
|
}
|
||||||
const notificationId = this.notificationsService.notifyError({
|
const notificationId = this.notificationsService.notifyError({
|
||||||
title: 'Could not fetch root clusters',
|
title: 'Could not fetch root clusters',
|
||||||
description: error.message,
|
description: getErrorMessage(error),
|
||||||
action: {
|
action: {
|
||||||
content: 'Retry',
|
content: 'Retry',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -193,7 +193,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const notificationId = this.notificationsService.notifyError({
|
const notificationId = this.notificationsService.notifyError({
|
||||||
title: 'Could not synchronize database connections',
|
title: 'Could not synchronize database connections',
|
||||||
description: error.message,
|
description: getErrorMessage(error),
|
||||||
action: {
|
action: {
|
||||||
content: 'Retry',
|
content: 'Retry',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -301,7 +301,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
|||||||
|
|
||||||
const notificationId = this.notificationsService.notifyError({
|
const notificationId = this.notificationsService.notifyError({
|
||||||
title,
|
title,
|
||||||
description: error.message,
|
description: getErrorMessage(error),
|
||||||
action: {
|
action: {
|
||||||
content: 'Retry',
|
content: 'Retry',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||||
import { SubmitConnectEventRequest } from 'gen-proto-ts/prehog/v1alpha/connect_pb';
|
import { SubmitConnectEventRequest } from 'gen-proto-ts/prehog/v1alpha/connect_pb';
|
||||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||||
|
import { getErrorMessage } from 'shared/utils/error';
|
||||||
|
|
||||||
import Logger from 'teleterm/logger';
|
import Logger from 'teleterm/logger';
|
||||||
import { RuntimeSettings } from 'teleterm/mainProcess/types';
|
import { RuntimeSettings } from 'teleterm/mainProcess/types';
|
||||||
@@ -304,9 +305,9 @@ export class UsageService {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.notificationsService.notifyWarning({
|
this.notificationsService.notifyWarning({
|
||||||
title: 'Failed to report usage event',
|
title: 'Failed to report usage event',
|
||||||
description: e.message,
|
description: getErrorMessage(e),
|
||||||
});
|
});
|
||||||
this.logger.warn(`Failed to report usage event`, e.message);
|
this.logger.warn('Failed to report usage event', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export async function retryWithRelogin<T>(
|
|||||||
resourceUri: ClusterOrResourceUri,
|
resourceUri: ClusterOrResourceUri,
|
||||||
actionToRetry: () => Promise<T>
|
actionToRetry: () => Promise<T>
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
let retryableErrorFromActionToRetry: Error;
|
let retryableErrorFromActionToRetry: unknown;
|
||||||
try {
|
try {
|
||||||
return await actionToRetry();
|
return await actionToRetry();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user