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,
|
||||
"strictBindCallApply": true,
|
||||
"noImplicitThis": true,
|
||||
"useUnknownInCatchVariables": true,
|
||||
"noEmitHelpers": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
+24
-4
@@ -310,10 +310,7 @@ function makeReviewStateOptions(
|
||||
// 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
|
||||
// message text so it survives across versions; surface other errors as-is.
|
||||
const err = fetchSuggestedAccessListsAttempt.error;
|
||||
const isPermissionError =
|
||||
err?.response?.status === 403 || err?.code === 'PERMISSION_DENIED';
|
||||
msg = isPermissionError
|
||||
msg = isPermissionDeniedError(fetchSuggestedAccessListsAttempt.error)
|
||||
? "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;
|
||||
} else if (request.resources.length === 0) {
|
||||
@@ -395,3 +392,26 @@ const HorizontalLine = styled.div<{ height?: number }>`
|
||||
function makeTraitLabel(traitKey: string, traitVals: string[]) {
|
||||
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
|
||||
// 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 permissionError = Object.assign(
|
||||
new Error('access denied to perform action "read" on access list'),
|
||||
{ response: { status: 403 } }
|
||||
);
|
||||
render(
|
||||
<RequestView
|
||||
{...props}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { Logger } from 'design/logger';
|
||||
import type { ToastNotificationItem } from 'shared/components/ToastNotification';
|
||||
import { Attempt } from 'shared/hooks/useAsync';
|
||||
import { ClientScreenSpec, ClipboardData, TdpClient } from 'shared/libs/tdp';
|
||||
import { isAbortError } from 'shared/utils/error';
|
||||
import { getErrorMessage, isAbortError } from 'shared/utils/error';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -206,7 +206,7 @@ export default function useDesktopSession(
|
||||
severity: 'warn',
|
||||
content: {
|
||||
title: 'Could not share a directory',
|
||||
description: e.message,
|
||||
description: getErrorMessage(e),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -222,7 +222,7 @@ export default function useDesktopSession(
|
||||
severity: 'warn',
|
||||
content: {
|
||||
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 {
|
||||
emitProgress(progress: number): void;
|
||||
|
||||
emitError(error: Error): void;
|
||||
|
||||
emitError(error: unknown): void;
|
||||
emitComplete(): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
/**
|
||||
* @deprecated Use TanStack Query (useQuery/useMutation) instead. See RFD 197.
|
||||
*
|
||||
@@ -209,7 +211,7 @@ export type Attempt<T> =
|
||||
status: 'error';
|
||||
data: null;
|
||||
statusText: string;
|
||||
error: any;
|
||||
error: unknown;
|
||||
};
|
||||
|
||||
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 {
|
||||
data: null,
|
||||
status: 'error',
|
||||
error: error,
|
||||
statusText: error.message,
|
||||
statusText: getErrorMessage(error),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import Logger from 'shared/libs/logger';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
const logger = Logger.create('shared/hooks/useAttempt');
|
||||
|
||||
@@ -56,9 +57,13 @@ function makeActions(setState) {
|
||||
setState({ ...defaultState });
|
||||
}
|
||||
|
||||
function error(err: Error) {
|
||||
function error(err: unknown) {
|
||||
logger.error('attempt', err);
|
||||
setState({ ...defaultState, isFailed: true, message: err.message });
|
||||
setState({
|
||||
...defaultState,
|
||||
isFailed: true,
|
||||
message: getErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
function run(fn: Callback) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import Logger from 'shared/libs/logger';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
const logger = Logger.create('shared/hooks/useAttempt');
|
||||
|
||||
@@ -33,9 +34,9 @@ export default function useAttemptNext(status = '' as Attempt['status']) {
|
||||
statusText: '',
|
||||
}));
|
||||
|
||||
const handleError = useCallback((err: Error) => {
|
||||
const handleError = useCallback((err: unknown) => {
|
||||
logger.error('attempt', err);
|
||||
setAttempt({ status: 'failed', statusText: err.message });
|
||||
setAttempt({ status: 'failed', statusText: getErrorMessage(err) });
|
||||
}, []);
|
||||
|
||||
const run = useCallback((fn: Callback) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
|
||||
import { Attempt } from 'shared/hooks/useAttemptNext';
|
||||
import { isAbortError } from 'shared/utils/abortError';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports -- FIXME
|
||||
import { ResourcesResponse } from 'teleport/services/agents';
|
||||
@@ -147,7 +148,11 @@ export function useKeyBasedPagination<T>({
|
||||
}
|
||||
setState({
|
||||
...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';
|
||||
// Inlines the wasm module as a static asset bundled with our app.
|
||||
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 {
|
||||
Alert,
|
||||
@@ -737,7 +737,7 @@ export class TdpClient extends EventEmitter<EventMap> {
|
||||
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,
|
||||
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/>.
|
||||
*/
|
||||
|
||||
import { ensureError, isAbortError } from './error';
|
||||
import { ensureError, isAbortError, isErrnoException } from './error';
|
||||
|
||||
class CustomErrorClass extends Error {
|
||||
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() {
|
||||
return new DOMException('Aborted', 'AbortError');
|
||||
}
|
||||
|
||||
@@ -83,3 +83,20 @@ export class AbortError extends DOMException {
|
||||
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 { AccessDenied } from 'design/CardError';
|
||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import AuthnDialog from 'teleport/components/AuthnDialog';
|
||||
import { CreateAppSessionParams, UrlLauncherParams } from 'teleport/config';
|
||||
@@ -207,7 +208,7 @@ export function AppLauncher({
|
||||
// `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.`;
|
||||
} 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) {
|
||||
statusText = err.message;
|
||||
}
|
||||
@@ -354,6 +355,6 @@ function getNewAuthExchangeUrl({
|
||||
return url;
|
||||
}
|
||||
|
||||
function throwFailedToParseUrlError(err: TypeError) {
|
||||
throw Error(`Failed to parse URL: ${err.message}`);
|
||||
function throwFailedToParseUrlError(err: unknown) {
|
||||
throw Error(`Failed to parse URL: ${getErrorMessage(err)}`);
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import Link from 'design/Link';
|
||||
import FieldInput from 'shared/components/FieldInput';
|
||||
import Select from 'shared/components/Select';
|
||||
import Validation, { Validator } from 'shared/components/Validation';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
|
||||
@@ -308,10 +309,16 @@ const EnterpriseHostError = () => {
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
const InvalidHostError = ({ rule, error }: { rule: string; error: string }) => {
|
||||
const InvalidHostError = ({
|
||||
rule,
|
||||
error,
|
||||
}: {
|
||||
rule: string;
|
||||
error: unknown;
|
||||
}) => {
|
||||
return (
|
||||
<Box>
|
||||
Invalid address {rule}: {error}
|
||||
Invalid address {rule}: {getErrorMessage(error)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import { Option } from 'shared/components/Select';
|
||||
import { Rule } from 'shared/components/Validation/rules';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { RefType } from 'teleport/services/bot/types';
|
||||
|
||||
@@ -113,6 +114,6 @@ export const requireValidRepository: Rule = value => () => {
|
||||
|
||||
return { valid: true };
|
||||
} 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 { AccessDenied, BadRequest } from 'design/CardError';
|
||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import AuthnDialog from 'teleport/components/AuthnDialog';
|
||||
import { useMfa, shouldShowMfaPrompt } from 'teleport/lib/useMfa';
|
||||
@@ -74,7 +75,7 @@ export function BrowserMfa({ onRedirect = redirectTo }: BrowserMfaProps) {
|
||||
} catch (err) {
|
||||
setAttempt({
|
||||
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 Validation, { Validator } from 'shared/components/Validation';
|
||||
import { useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { TextSelectCopyMulti } from 'teleport/components/TextSelectCopy';
|
||||
import cfg from 'teleport/config';
|
||||
@@ -79,7 +80,7 @@ export function CreateAppAccess() {
|
||||
resourceName: app.name,
|
||||
});
|
||||
} catch (err) {
|
||||
emitErrorEvent(err.message);
|
||||
emitErrorEvent(getErrorMessage(err));
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ export function useCreateDatabase() {
|
||||
setAttempt({ status: '' });
|
||||
}
|
||||
|
||||
function handleRequestError(err: Error, preErrMsg = '') {
|
||||
function handleRequestError(err: unknown, preErrMsg = '') {
|
||||
const message = getErrMessage(err);
|
||||
setAttempt({ status: 'failed', statusText: `${preErrMsg}${message}` });
|
||||
emitErrorEvent(`${preErrMsg}${message}`);
|
||||
|
||||
@@ -38,6 +38,7 @@ import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
|
||||
import Validation, { Validator } from 'shared/components/Validation';
|
||||
import { Rule } from 'shared/components/Validation/rules';
|
||||
import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { LabelsInput } from 'teleport/components/LabelsInput';
|
||||
import cfg from 'teleport/config';
|
||||
@@ -176,7 +177,7 @@ export function DiscoveryConfigSsm() {
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
emitErrorEvent(err.message);
|
||||
emitErrorEvent(getErrorMessage(err));
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -19,6 +19,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import useAttempt from 'shared/hooks/useAttemptNext';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import {
|
||||
getDatabaseProtocol,
|
||||
@@ -107,7 +108,7 @@ export function useConnectionDiagnostic() {
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
emitErrorEvent(err.message);
|
||||
emitErrorEvent(getErrorMessage(err));
|
||||
}
|
||||
|
||||
return { mfaRequired: false };
|
||||
|
||||
@@ -28,6 +28,7 @@ import Dialog, {
|
||||
} from 'design/Dialog';
|
||||
import Validation, { Validator } from 'shared/components/Validation';
|
||||
import { Attempt, useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { CatchError } from 'teleport/components/CatchError';
|
||||
import cfg from 'teleport/config';
|
||||
@@ -417,12 +418,16 @@ const AttemptAlert = ({ attempt }: { attempt?: Attempt<unknown> }) => {
|
||||
};
|
||||
|
||||
/** Renders an alert if there is an error. */
|
||||
const ErrorAlert = ({ error }: { error: Error }) =>
|
||||
error && (
|
||||
<Danger mt={3} dismissible details={error.cause?.toString()}>
|
||||
{error.message}
|
||||
</Danger>
|
||||
const ErrorAlert = ({ error }: { error: unknown }) => {
|
||||
const details = error instanceof Error ? error.cause?.toString() : '';
|
||||
return (
|
||||
error && (
|
||||
<Danger mt={3} dismissible details={details}>
|
||||
{getErrorMessage(error)}
|
||||
</Danger>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const ShowHide = styled(Flex)<{ hidden: boolean }>`
|
||||
display: ${props => (props.hidden ? 'none' : '')};
|
||||
|
||||
@@ -85,7 +85,7 @@ export class TtyPlayer extends Player<TtyEvent> {
|
||||
this.terminal.loadAddon(imageAddon);
|
||||
this.addons.push(imageAddon);
|
||||
} 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;
|
||||
|
||||
@@ -23,6 +23,7 @@ import styled from 'styled-components';
|
||||
import { Box, ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design';
|
||||
import { ChevronDown } from 'design/Icon';
|
||||
import { HoverTooltip } from 'design/Tooltip';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
import { Cluster } from 'teleport/services/clusters';
|
||||
@@ -84,7 +85,7 @@ export function ClusterDropdown({
|
||||
try {
|
||||
return clusterLoader.fetchClusters(signal);
|
||||
} catch (err) {
|
||||
onError(err.message);
|
||||
onError(getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ export function ClusterDropdown({
|
||||
const res = await loadClusters(signal.signal);
|
||||
setOptions(createOptions(res));
|
||||
} catch (err) {
|
||||
onError(err.message);
|
||||
onError(getErrorMessage(err));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import auth from 'teleport/services/auth';
|
||||
import { MfaChallengeScope } from 'teleport/services/auth/auth';
|
||||
@@ -145,17 +146,18 @@ type challengeState = {
|
||||
deviceUsage: DeviceUsage;
|
||||
};
|
||||
|
||||
function getReAuthenticationErrorMessage(err: Error): string {
|
||||
if (err.message.includes('attempt was made to use an object that is not')) {
|
||||
function getReAuthenticationErrorMessage(err: unknown): string {
|
||||
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.
|
||||
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
|
||||
// lib/auth/Server.checkOTP function. Please keep these in sync.
|
||||
return 'Invalid authenticator code';
|
||||
}
|
||||
|
||||
return err.message;
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ export default class TtyTerminal implements TerminalSearcher {
|
||||
this._imageAddon = new ImageAddon();
|
||||
this.term.loadAddon(this._imageAddon);
|
||||
} catch (e) {
|
||||
logger.error('Failed to load image addon:', e.message);
|
||||
logger.error('Failed to load image addon', e);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
makeEmptyAttempt,
|
||||
useAsync,
|
||||
} from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { EventEmitterMfaSender } from 'teleport/lib/EventEmitterMfaSender';
|
||||
import { TermEvent } from 'teleport/lib/term/enums';
|
||||
@@ -163,7 +164,7 @@ export function useMfa(props?: MfaProps): MfaState {
|
||||
setMfaAttempt({
|
||||
data: null,
|
||||
status: 'error',
|
||||
statusText: err.message,
|
||||
statusText: getErrorMessage(err),
|
||||
error: err,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
|
||||
import 'whatwg-fetch';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import auth, { MfaChallengeScope } from 'teleport/services/auth/auth';
|
||||
import websession from 'teleport/services/websession';
|
||||
|
||||
@@ -293,7 +295,7 @@ const api = {
|
||||
} catch (err) {
|
||||
// error reading JSON
|
||||
const message = response.ok
|
||||
? err.message
|
||||
? getErrorMessage(err)
|
||||
: `${response.status} - ${response.url}`;
|
||||
throw new ApiError({ message, response, opts: { cause: err } });
|
||||
}
|
||||
@@ -455,8 +457,8 @@ export function getHostName() {
|
||||
return location.hostname + (location.port ? ':' + location.port : '');
|
||||
}
|
||||
|
||||
export function isAdminActionRequiresMfaError(err: Error) {
|
||||
return err.message.includes(
|
||||
export function isAdminActionRequiresMfaError(err: unknown) {
|
||||
return getErrorMessage(err).includes(
|
||||
'admin-level API request requires MFA verification'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { MutationFunction } from '@tanstack/react-query';
|
||||
|
||||
import cfg from 'teleport/config';
|
||||
import api from 'teleport/services/api';
|
||||
import { ApiError } from 'teleport/services/api/parseError';
|
||||
import {
|
||||
canUseV1Edit,
|
||||
canUseV2Edit,
|
||||
@@ -67,7 +68,7 @@ export async function getBot(
|
||||
.then(makeBot);
|
||||
} catch (err) {
|
||||
// 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;
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* 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 {
|
||||
@@ -124,7 +124,7 @@ export const withUnsupportedOktaPluginUpdateErrorConversion = (
|
||||
err: unknown
|
||||
) => {
|
||||
if (err instanceof ApiError && err.response.status === 404) {
|
||||
const msg = getErrMessage(err);
|
||||
const msg = getErrorMessage(err);
|
||||
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.`
|
||||
);
|
||||
@@ -136,7 +136,7 @@ export const withUnsupportedOktaPluginCreateErrorConversion = (
|
||||
err: unknown
|
||||
) => {
|
||||
if (err instanceof ApiError) {
|
||||
const msg = getErrMessage(err);
|
||||
const msg = getErrorMessage(err);
|
||||
if (msg.match(/missing okta (?:organization url|api token)/gi)) {
|
||||
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.`
|
||||
@@ -147,7 +147,7 @@ export const withUnsupportedOktaPluginCreateErrorConversion = (
|
||||
};
|
||||
|
||||
type Base = {
|
||||
err: Error;
|
||||
err: unknown;
|
||||
};
|
||||
|
||||
type CreateJoinToken = Base & {
|
||||
@@ -212,7 +212,10 @@ export function useV1Fallback() {
|
||||
async function tryV1Fallback(props: CreateJoinToken): Promise<JoinToken>;
|
||||
|
||||
async function tryV1Fallback(props: FallbackProps) {
|
||||
if (!props.err.message.includes(ProxyRequiresUpgrade) || hasLabels(props)) {
|
||||
if (
|
||||
!getErrorMessage(props.err).includes(ProxyRequiresUpgrade) ||
|
||||
hasLabels(props)
|
||||
) {
|
||||
throw props.err;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
|
||||
import { isErrnoException } from 'shared/utils/error';
|
||||
|
||||
const stdio = 'pipe'; // Change to 'inherit' for easier debugging.
|
||||
|
||||
let logsDir: string;
|
||||
@@ -197,7 +199,7 @@ const isRunning = (pid: number) => {
|
||||
try {
|
||||
return process.kill(pid, 0);
|
||||
} catch (error) {
|
||||
if (error.code === 'ESRCH') {
|
||||
if (isErrnoException(error, 'ESRCH')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ export type DeepLinkParseResult =
|
||||
// ergonomic. Unfortunately, `if (!result.ok)` doesn't narrow down the type properly with
|
||||
// strictNullChecks off. https://github.com/microsoft/TypeScript/issues/10564
|
||||
| DeepLinkParseResultSuccess
|
||||
| ParseError<'malformed-url', { error: TypeError }>
|
||||
| ParseError<'malformed-url', { error: unknown }>
|
||||
| ParseError<'unknown-protocol', { protocol: string }>
|
||||
| ParseError<'unsupported-url'>;
|
||||
|
||||
@@ -69,9 +69,11 @@ export function parseDeepLink(rawUrl: string): DeepLinkParseResult {
|
||||
try {
|
||||
parsedURL = new URL(rawUrl);
|
||||
} catch (error) {
|
||||
// `error instanceof TypeError` doesn't work in tests. The URL constructor shouldn't throw other
|
||||
// errors anyway.
|
||||
return { status: 'error', reason: 'malformed-url', error };
|
||||
return {
|
||||
status: 'error',
|
||||
reason: 'malformed-url',
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
if (parsedURL.protocol !== `${CUSTOM_PROTOCOL}:`) {
|
||||
|
||||
@@ -23,7 +23,11 @@ import path from 'node:path';
|
||||
import { app, dialog, nativeTheme } from 'electron';
|
||||
|
||||
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 Logger from 'teleterm/logger';
|
||||
@@ -366,7 +370,7 @@ function launchDeepLink(
|
||||
break;
|
||||
}
|
||||
case 'malformed-url': {
|
||||
reason = `malformed URL (${result.error.message})`;
|
||||
reason = `malformed URL (${getErrorMessage(result.error)})`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -429,7 +433,7 @@ async function migrateOldTshHomeOnce(
|
||||
try {
|
||||
await fs.stat(oldTshHome);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (isErrnoException(err, 'ENOENT')) {
|
||||
logger.info(
|
||||
'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 { isErrnoException } from 'shared/utils/error';
|
||||
import { compareSemVers } from 'shared/utils/semVer';
|
||||
|
||||
import Logger from 'teleterm/logger';
|
||||
@@ -149,7 +150,7 @@ async function isCorrectAgentVersionAlreadyDownloaded(
|
||||
return agentVersion.stdout.trim() === neededVersion;
|
||||
} catch (e) {
|
||||
// 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;
|
||||
}
|
||||
return false;
|
||||
|
||||
+1
-2
@@ -20,7 +20,6 @@ import {
|
||||
Cluster,
|
||||
LoggedInUser,
|
||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||
import { ensureError } from 'shared/utils/error';
|
||||
|
||||
import Logger from 'teleterm/logger';
|
||||
import type { IAwaitableSender } from 'teleterm/mainProcess/awaitableSender';
|
||||
@@ -381,7 +380,7 @@ export class ClusterLifecycleManager {
|
||||
);
|
||||
const serialized: ProfileWatcherError = {
|
||||
reason: watcherError.reason,
|
||||
error: serializeError(ensureError(watcherError.error)),
|
||||
error: serializeError(watcherError.error),
|
||||
};
|
||||
this.windowsManager
|
||||
.getWindow()
|
||||
|
||||
@@ -22,6 +22,7 @@ import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import * as connectMyComputer from 'shared/connectMyComputer';
|
||||
import { isErrnoException } from 'shared/utils/error';
|
||||
|
||||
import { RuntimeSettings } from 'teleterm/mainProcess/types';
|
||||
import { RootClusterUri, routing } from 'teleterm/ui/uri';
|
||||
@@ -127,7 +128,7 @@ export async function isAgentConfigFileCreated(
|
||||
await fs.access(configFile);
|
||||
return true;
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
if (isErrnoException(e, 'ENOENT')) {
|
||||
return false;
|
||||
}
|
||||
throw e;
|
||||
|
||||
@@ -64,3 +64,17 @@ test('serializes and deserializes RPC error', () => {
|
||||
'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/>.
|
||||
*/
|
||||
|
||||
import { ensureError } from 'shared/utils/error';
|
||||
|
||||
export type SerializedError = {
|
||||
name: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
cause?: unknown;
|
||||
toStringResult?: string;
|
||||
$serializedError?: true;
|
||||
};
|
||||
|
||||
/** Serializes an Error into a plain object for transport through Electron IPC. */
|
||||
export function serializeError(error: Error): SerializedError {
|
||||
function isSerialized(error: unknown): error is 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 {
|
||||
name,
|
||||
cause,
|
||||
@@ -34,7 +52,7 @@ export function serializeError(error: Error): SerializedError {
|
||||
// functions must be skipped, otherwise structuredClone will fail to clone the object
|
||||
toString,
|
||||
...enumerableFields
|
||||
} = error;
|
||||
} = errorInstance;
|
||||
return {
|
||||
name,
|
||||
message,
|
||||
@@ -42,14 +60,23 @@ export function serializeError(error: Error): SerializedError {
|
||||
stack,
|
||||
// Calling the destructured function directly could result in the following error:
|
||||
// Method Error.prototype.toString called on incompatible receiver undefined
|
||||
toStringResult: error.toString?.(),
|
||||
toStringResult: errorInstance.toString?.(),
|
||||
...enumerableFields,
|
||||
$serializedError: true,
|
||||
};
|
||||
}
|
||||
|
||||
/** Deserializes a plain object back into an Error instance. */
|
||||
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);
|
||||
error.name = name;
|
||||
error.cause = cause;
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
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;
|
||||
|
||||
@@ -46,7 +48,7 @@ function writeInstallationId(filePath: string): string {
|
||||
fs.writeFileSync(filePath, newId);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not write installation_id to ${filePath}, ${error.message}`,
|
||||
`Could not write installation_id to ${filePath}, ${getErrorMessage(error)}`,
|
||||
{ cause: error }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
import { ensureError } from 'shared/utils/error';
|
||||
|
||||
import Logger from 'teleterm/logger';
|
||||
import type { Message, MessageAck } from 'teleterm/mainProcess/awaitableSender';
|
||||
import { CreateAgentConfigFileArgs } from 'teleterm/mainProcess/createAgentConfigFile';
|
||||
@@ -325,7 +323,7 @@ function startAwaitableSenderListener<T>(
|
||||
try {
|
||||
await listener(msg.payload as T);
|
||||
} catch (e) {
|
||||
ack.error = serializeError(ensureError(e));
|
||||
ack.error = serializeError(e);
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -23,6 +23,7 @@ import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
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 Logger, { NullService } from 'teleterm/logger';
|
||||
@@ -65,7 +66,7 @@ async function mockTshClient(tshDir: string, initial: { clusters: Cluster[] }) {
|
||||
try {
|
||||
paths = await fs.readdir(tshDir);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
if (isErrnoException(err, 'ENOENT')) {
|
||||
throw {
|
||||
name: 'RpcError',
|
||||
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.
|
||||
// This is possible in tests where we call `void tshClientMock.removeCluster` without
|
||||
// awaiting.
|
||||
if (err.code === 'ENOENT') {
|
||||
if (isErrnoException(err, 'ENOENT')) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { watch, type WatchEventType } from 'node:fs';
|
||||
import { access } from 'node:fs/promises';
|
||||
|
||||
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 { wait } from 'shared/utils/wait';
|
||||
|
||||
@@ -129,7 +130,7 @@ export async function* watchProfiles({
|
||||
if (
|
||||
isTshdRpcError(error, 'NOT_FOUND') ||
|
||||
error instanceof FileSystemEventsOverflowError ||
|
||||
error?.code === 'EPERM'
|
||||
isErrnoException(error, 'EPERM')
|
||||
) {
|
||||
const ok = await pathExists(tshDirectory);
|
||||
if (!ok) {
|
||||
@@ -161,7 +162,7 @@ async function pathExists(dirPath: string): Promise<boolean> {
|
||||
await access(dirPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
if (isErrnoException(error, 'ENOENT')) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -246,7 +246,8 @@ export class WindowsManager {
|
||||
await this.whenFrontendAppIsReady();
|
||||
} catch (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;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { NsisUpdater } from 'electron-updater';
|
||||
import { DownloadUpdateOptions } from 'electron-updater/out/AppUpdater';
|
||||
import { InstallOptions } from 'electron-updater/out/BaseUpdater';
|
||||
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
import { ensureError, getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import {
|
||||
TSH_AUTOUPDATE_ENV_VAR,
|
||||
@@ -100,7 +100,7 @@ export class NsisDualModeUpdater extends NsisUpdater {
|
||||
[TSH_AUTOUPDATE_ENV_VAR]: TSH_AUTOUPDATE_OFF,
|
||||
});
|
||||
} catch (error) {
|
||||
this.dispatchError(error);
|
||||
this.dispatchError(ensureError(error));
|
||||
const errorMessage = getErrorMessage(error);
|
||||
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.
|
||||
|
||||
@@ -33,7 +33,7 @@ const logger = new Logger('ConfigService');
|
||||
|
||||
export type FileLoadingError = {
|
||||
source: 'file-loading';
|
||||
error: Error;
|
||||
error: unknown;
|
||||
};
|
||||
|
||||
export type ValidationError = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import fs from 'node:fs';
|
||||
import fsAsync from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { isErrnoException } from 'shared/utils/error';
|
||||
import { debounce } from 'shared/utils/highbar';
|
||||
|
||||
import Logger from 'teleterm/logger';
|
||||
@@ -52,7 +53,7 @@ export interface FileStorage {
|
||||
getFileName(): string;
|
||||
|
||||
/** 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;
|
||||
|
||||
let state: any, error: Error | undefined;
|
||||
let state: any, error: unknown;
|
||||
try {
|
||||
state = loadStateSync(filePath);
|
||||
} catch (e) {
|
||||
@@ -127,7 +128,7 @@ export function createFileStorage(opts: {
|
||||
return path.basename(opts.filePath);
|
||||
}
|
||||
|
||||
function getFileLoadingError(): Error | undefined {
|
||||
function getFileLoadingError(): unknown {
|
||||
return error;
|
||||
}
|
||||
|
||||
@@ -161,7 +162,7 @@ function readOrCreateFileSync(filePath: string): string {
|
||||
try {
|
||||
return fs.readFileSync(filePath, { encoding: 'utf-8' });
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
if (isErrnoException(error, 'ENOENT')) {
|
||||
fs.writeFileSync(filePath, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { watch, type Stats } from 'fs';
|
||||
import { readFile, rename, stat, writeFile } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { isErrnoException } from 'shared/utils/error';
|
||||
import { wait } from 'shared/utils/wait';
|
||||
|
||||
import { makeCert } from './makeCert';
|
||||
@@ -68,7 +69,7 @@ export async function readGrpcCert(
|
||||
try {
|
||||
stats = await stat(fullPath);
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
if (isErrnoException(error, 'ENOENT')) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
|
||||
@@ -28,8 +28,6 @@ import {
|
||||
UnaryCall,
|
||||
} from '@protobuf-ts/runtime-rpc';
|
||||
|
||||
import { ensureError } from 'shared/utils/error';
|
||||
|
||||
import {
|
||||
serializeError,
|
||||
type SerializedError,
|
||||
@@ -314,7 +312,7 @@ export function isRpcErrorReloginResolvable(error: unknown): boolean {
|
||||
}
|
||||
|
||||
function cloneError(error: unknown): SerializedError {
|
||||
return serializeError(ensureError(error));
|
||||
return serializeError(error);
|
||||
}
|
||||
|
||||
function cloneRequests<O extends object>(
|
||||
|
||||
@@ -24,6 +24,7 @@ import { promisify } from 'node:util';
|
||||
import * as nodePTY from 'node-pty';
|
||||
import which from 'which';
|
||||
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
import { wait } from 'shared/utils/wait';
|
||||
|
||||
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(' ')}`;
|
||||
this.emit(
|
||||
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 { Indicator } from 'design';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||
import { useLogger } from 'teleterm/ui/hooks/useLogger';
|
||||
@@ -57,10 +58,10 @@ export const AppInitializer = () => {
|
||||
success: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(error?.message);
|
||||
logger.error('Failed to initialize app', error);
|
||||
|
||||
setShouldShowUi(true);
|
||||
appContext?.notificationsService.notifyError(error?.message);
|
||||
appContext?.notificationsService.notifyError(getErrorMessage(error));
|
||||
appContext?.mainProcessClient.signalUserInterfaceReadiness({
|
||||
success: false,
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import styled from 'styled-components';
|
||||
import { Alert, Box, ButtonPrimary, Flex, H1, Text } from 'design';
|
||||
import * as Alerts from 'design/Alert';
|
||||
import { Attempt, makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
import { wait } from 'shared/utils/wait';
|
||||
|
||||
import {
|
||||
@@ -244,7 +245,7 @@ function AgentSetup() {
|
||||
!isRpcErrorReloginResolvable(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 }
|
||||
);
|
||||
}
|
||||
@@ -431,7 +432,7 @@ function AgentSetup() {
|
||||
const { agentBinaryPath } = mainProcessClient.getRuntimeSettings();
|
||||
notificationsService.notifyError({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import * as icons from 'design/Icon';
|
||||
import type { IconProps } from 'design/Icon/Icon';
|
||||
import Indicator from 'design/Indicator';
|
||||
import { MenuIcon } from 'shared/components/MenuAction';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import type * as tsh from 'teleterm/services/tshd/types';
|
||||
import { useAppContext } from 'teleterm/ui/appContextProvider';
|
||||
@@ -112,7 +113,7 @@ export function Status(props: { closeDocument?: () => void }) {
|
||||
} catch (e) {
|
||||
ctx.notificationsService.notifyError({
|
||||
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,
|
||||
VnetDeepURL,
|
||||
} from 'shared/deepLinks';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
|
||||
import { DeepLinkParseResult } from 'teleterm/deepLinks';
|
||||
import { IAppContext } from 'teleterm/ui/types';
|
||||
@@ -59,7 +60,7 @@ export async function launchDeepLink(
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
default: {
|
||||
|
||||
@@ -238,14 +238,15 @@ export default class TtyTerminal implements TerminalSearcher {
|
||||
|
||||
this.term.onData(data => {
|
||||
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.ptyProcess.resize(size.cols, size.rows).catch(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
|
||||
// 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.logger.error(`Failed to start the PTY process: ${error}`);
|
||||
this.logger.error('Failed to start the PTY process', error);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', this.debouncedResize);
|
||||
|
||||
@@ -89,7 +89,7 @@ export function useDocumentTerminal(doc: types.DocumentTerminal) {
|
||||
return () => {
|
||||
if (attempt.status === 'success') {
|
||||
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 * as diag from 'gen-proto-ts/teleport/lib/vnet/diag/v1/diag_pb';
|
||||
import { CanceledError, useAsync } from 'shared/hooks/useAsync';
|
||||
import { getErrorMessage } from 'shared/utils/error';
|
||||
import { pluralize } from 'shared/utils/text';
|
||||
|
||||
import {
|
||||
@@ -122,7 +123,7 @@ export function DocumentVnetDiagReport(props: {
|
||||
previousSaveToFileNotificationIdRef.current =
|
||||
notificationsService.notifyError({
|
||||
title: 'Could not save the report to a file.',
|
||||
description: error?.message,
|
||||
description: getErrorMessage(error),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
* 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 { ClusterUri, RootClusterUri, routing } from 'teleterm/ui/uri';
|
||||
|
||||
@@ -35,7 +37,7 @@ const commands = {
|
||||
error => {
|
||||
ctx.notificationsService.notifyError({
|
||||
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 => {
|
||||
ctx.notificationsService.notifyError({
|
||||
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,
|
||||
} from 'gen-proto-ts/teleport/lib/teleterm/v1/service_pb';
|
||||
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 { MainProcessClient } from 'teleterm/mainProcess/types';
|
||||
@@ -89,7 +89,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
||||
|
||||
const notificationId = this.notificationsService.notifyError({
|
||||
title: `Could not synchronize cluster ${clusterName}`,
|
||||
description: e.message,
|
||||
description: getErrorMessage(e),
|
||||
action: {
|
||||
content: 'Retry',
|
||||
onClick: () => {
|
||||
@@ -112,7 +112,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
||||
|
||||
const notificationId = this.notificationsService.notifyError({
|
||||
title: `Could not start headless requests watcher for ${clusterName}`,
|
||||
description: e.message,
|
||||
description: getErrorMessage(e),
|
||||
action: {
|
||||
content: 'Retry',
|
||||
onClick: () => {
|
||||
@@ -166,7 +166,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
||||
}
|
||||
const notificationId = this.notificationsService.notifyError({
|
||||
title: 'Could not fetch root clusters',
|
||||
description: error.message,
|
||||
description: getErrorMessage(error),
|
||||
action: {
|
||||
content: 'Retry',
|
||||
onClick: () => {
|
||||
@@ -193,7 +193,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
||||
} catch (error) {
|
||||
const notificationId = this.notificationsService.notifyError({
|
||||
title: 'Could not synchronize database connections',
|
||||
description: error.message,
|
||||
description: getErrorMessage(error),
|
||||
action: {
|
||||
content: 'Retry',
|
||||
onClick: () => {
|
||||
@@ -301,7 +301,7 @@ export class ClustersService extends ImmutableStore<ClustersServiceState> {
|
||||
|
||||
const notificationId = this.notificationsService.notifyError({
|
||||
title,
|
||||
description: error.message,
|
||||
description: getErrorMessage(error),
|
||||
action: {
|
||||
content: 'Retry',
|
||||
onClick: () => {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import { SubmitConnectEventRequest } from 'gen-proto-ts/prehog/v1alpha/connect_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 { RuntimeSettings } from 'teleterm/mainProcess/types';
|
||||
@@ -304,9 +305,9 @@ export class UsageService {
|
||||
} catch (e) {
|
||||
this.notificationsService.notifyWarning({
|
||||
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,
|
||||
actionToRetry: () => Promise<T>
|
||||
): Promise<T> {
|
||||
let retryableErrorFromActionToRetry: Error;
|
||||
let retryableErrorFromActionToRetry: unknown;
|
||||
try {
|
||||
return await actionToRetry();
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user