From 3d82a6efe4a0c9eca0ff514041832c327a6728b1 Mon Sep 17 00:00:00 2001 From: Grzegorz Zdunek Date: Fri, 31 Jul 2026 11:01:36 +0200 Subject: [PATCH] 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 --- tsconfig.base.json | 1 + .../RequestReview/RequestReview.tsx | 28 ++++++- .../RequestView/RequestView.test.tsx | 20 +++-- .../DesktopSession/useDesktopSession.tsx | 6 +- .../createFileTransferEventsEmitter.ts | 4 +- web/packages/shared/hooks/useAsync.ts | 8 +- web/packages/shared/hooks/useAttempt.ts | 9 ++- web/packages/shared/hooks/useAttemptNext.ts | 5 +- .../useKeyBasedPagination.ts | 7 +- web/packages/shared/libs/tdp/client.ts | 6 +- web/packages/shared/utils/error.test.ts | 77 ++++++++++++++++++- web/packages/shared/utils/error.ts | 17 ++++ .../teleport/src/AppLauncher/AppLauncher.tsx | 7 +- .../Add/GitHubActionsSsh/ConnectGitHub.tsx | 11 ++- .../teleport/src/Bots/Add/Shared/github.ts | 3 +- .../teleport/src/BrowserMFA/BrowserMFA.tsx | 3 +- .../CreateAppAccess/CreateAppAccess.tsx | 3 +- .../CreateDatabase/useCreateDatabase.ts | 2 +- .../DiscoveryConfigSsm/DiscoveryConfigSsm.tsx | 3 +- .../useConnectionDiagnostic.ts | 3 +- .../src/Roles/RoleEditor/RoleEditor.tsx | 15 ++-- .../view/player/tty/TtyPlayer.ts | 2 +- .../ClusterDropdown/ClusterDropdown.tsx | 5 +- .../ReAuthenticate/useReAuthenticate.ts | 10 ++- .../teleport/src/lib/term/terminal.ts | 2 +- web/packages/teleport/src/lib/useMfa.ts | 3 +- web/packages/teleport/src/services/api/api.ts | 8 +- web/packages/teleport/src/services/bot/bot.ts | 3 +- .../src/services/version/unsupported.ts | 13 ++-- .../agentCleanupDaemon.test.ts | 4 +- web/packages/teleterm/src/deepLinks.ts | 10 ++- web/packages/teleterm/src/main.ts | 10 ++- .../agentDownloader/agentDownloader.ts | 3 +- .../clusterLifecycleManager.ts | 3 +- .../src/mainProcess/createAgentConfigFile.ts | 3 +- .../src/mainProcess/ipcSerializer.test.ts | 14 ++++ .../teleterm/src/mainProcess/ipcSerializer.ts | 37 +++++++-- .../loadInstallationId/loadInstallationId.ts | 4 +- .../src/mainProcess/mainProcessClient.ts | 4 +- .../profileWatcher/profileWatcher.test.ts | 5 +- .../profileWatcher/profileWatcher.ts | 5 +- .../src/mainProcess/windowsManager.ts | 3 +- .../appUpdater/nsisDualModeUpdater.ts | 4 +- .../src/services/config/configService.ts | 2 +- .../src/services/fileStorage/fileStorage.ts | 9 ++- .../src/services/grpcCredentials/files.ts | 3 +- .../src/services/tshd/cloneableClient.ts | 4 +- .../src/sharedProcess/ptyHost/ptyProcess.ts | 5 +- .../src/ui/AppInitializer/AppInitializer.tsx | 5 +- .../DocumentConnectMyComputer/Setup.tsx | 5 +- .../DocumentConnectMyComputer/Status.tsx | 3 +- .../src/ui/DeepLinks/launchDeepLink.ts | 3 +- .../src/ui/DocumentTerminal/Terminal/ctrl.ts | 7 +- .../DocumentTerminal/useDocumentTerminal.ts | 2 +- .../src/ui/Vnet/DocumentVnetDiagReport.tsx | 3 +- .../teleterm/src/ui/commandLauncher.ts | 6 +- .../ui/services/clusters/clustersService.ts | 12 +-- .../src/ui/services/usage/usageService.ts | 5 +- .../teleterm/src/ui/utils/retryWithRelogin.ts | 2 +- 59 files changed, 347 insertions(+), 122 deletions(-) diff --git a/tsconfig.base.json b/tsconfig.base.json index d0c636d02ec..9980edc99f8 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,6 +15,7 @@ "strict": false, "strictBindCallApply": true, "noImplicitThis": true, + "useUnknownInCatchVariables": true, "noEmitHelpers": true, "resolveJsonModule": true, "skipLibCheck": true, diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx index 714d2d69c5e..94a8a1be8e2 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestReview/RequestReview.tsx @@ -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 + ); +} diff --git a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.test.tsx b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.test.tsx index 22009a19d54..9089adab211 100644 --- a/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.test.tsx +++ b/web/packages/shared/components/AccessRequests/ReviewRequests/RequestView/RequestView.test.tsx @@ -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( = status: 'error'; data: null; statusText: string; - error: any; + error: unknown; }; export function hasFinished(attempt: Attempt): boolean { @@ -240,12 +242,12 @@ export function makeProcessingAttempt(): Attempt { }; } -export function makeErrorAttempt(error: Error): Attempt { +export function makeErrorAttempt(error: unknown): Attempt { return { data: null, status: 'error', error: error, - statusText: error.message, + statusText: getErrorMessage(error), }; } diff --git a/web/packages/shared/hooks/useAttempt.ts b/web/packages/shared/hooks/useAttempt.ts index 3ef8ff27155..27e14b46bff 100644 --- a/web/packages/shared/hooks/useAttempt.ts +++ b/web/packages/shared/hooks/useAttempt.ts @@ -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) { diff --git a/web/packages/shared/hooks/useAttemptNext.ts b/web/packages/shared/hooks/useAttemptNext.ts index f20851497a1..9b7e0ecdca8 100644 --- a/web/packages/shared/hooks/useAttemptNext.ts +++ b/web/packages/shared/hooks/useAttemptNext.ts @@ -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) => { diff --git a/web/packages/shared/hooks/useInfiniteScroll/useKeyBasedPagination.ts b/web/packages/shared/hooks/useInfiniteScroll/useKeyBasedPagination.ts index 89424c68e19..08064cf5abf 100644 --- a/web/packages/shared/hooks/useInfiniteScroll/useKeyBasedPagination.ts +++ b/web/packages/shared/hooks/useInfiniteScroll/useKeyBasedPagination.ts @@ -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({ } setState({ ...stateRef.current, - attempt: { status: 'failed', statusText: err.message, statusCode }, + attempt: { + status: 'failed', + statusText: getErrorMessage(err), + statusCode, + }, }); } }, diff --git a/web/packages/shared/libs/tdp/client.ts b/web/packages/shared/libs/tdp/client.ts index 8be66c0e73a..ba16bfdf5e6 100644 --- a/web/packages/shared/libs/tdp/client.ts +++ b/web/packages/shared/libs/tdp/client.ts @@ -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 { 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 { directoryId: req.directoryId, errCode: SharedDirectoryErrCode.Failed, }); - this.handleWarning(e.message, TdpClientEvent.CLIENT_WARNING); + this.handleWarning(getErrorMessage(e), TdpClientEvent.CLIENT_WARNING); } } diff --git a/web/packages/shared/utils/error.test.ts b/web/packages/shared/utils/error.test.ts index 71bbc6f363f..382218207bd 100644 --- a/web/packages/shared/utils/error.test.ts +++ b/web/packages/shared/utils/error.test.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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'); } diff --git a/web/packages/shared/utils/error.ts b/web/packages/shared/utils/error.ts index fa28612a649..121eb0d9ea9 100644 --- a/web/packages/shared/utils/error.ts +++ b/web/packages/shared/utils/error.ts @@ -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 + ); +} diff --git a/web/packages/teleport/src/AppLauncher/AppLauncher.tsx b/web/packages/teleport/src/AppLauncher/AppLauncher.tsx index 5f352d461eb..81fc1ee36ef 100644 --- a/web/packages/teleport/src/AppLauncher/AppLauncher.tsx +++ b/web/packages/teleport/src/AppLauncher/AppLauncher.tsx @@ -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)}`); } diff --git a/web/packages/teleport/src/Bots/Add/GitHubActionsSsh/ConnectGitHub.tsx b/web/packages/teleport/src/Bots/Add/GitHubActionsSsh/ConnectGitHub.tsx index bdc51348e2c..c9dba8abc11 100644 --- a/web/packages/teleport/src/Bots/Add/GitHubActionsSsh/ConnectGitHub.tsx +++ b/web/packages/teleport/src/Bots/Add/GitHubActionsSsh/ConnectGitHub.tsx @@ -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 = () => { ); }; -const InvalidHostError = ({ rule, error }: { rule: string; error: string }) => { +const InvalidHostError = ({ + rule, + error, +}: { + rule: string; + error: unknown; +}) => { return ( - Invalid address {rule}: {error} + Invalid address {rule}: {getErrorMessage(error)} ); }; diff --git a/web/packages/teleport/src/Bots/Add/Shared/github.ts b/web/packages/teleport/src/Bots/Add/Shared/github.ts index 1912a19bd37..612d33625e7 100644 --- a/web/packages/teleport/src/Bots/Add/Shared/github.ts +++ b/web/packages/teleport/src/Bots/Add/Shared/github.ts @@ -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) }; } }; diff --git a/web/packages/teleport/src/BrowserMFA/BrowserMFA.tsx b/web/packages/teleport/src/BrowserMFA/BrowserMFA.tsx index 07df330f9dd..b1f46a18ede 100644 --- a/web/packages/teleport/src/BrowserMFA/BrowserMFA.tsx +++ b/web/packages/teleport/src/BrowserMFA/BrowserMFA.tsx @@ -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), }); } } diff --git a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx index 14ad8555dbf..a7cbc8f26ae 100644 --- a/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx +++ b/web/packages/teleport/src/Discover/AwsMangementConsole/CreateAppAccess/CreateAppAccess.tsx @@ -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; } }); diff --git a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts index aa896a41d7d..f029a795049 100644 --- a/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts +++ b/web/packages/teleport/src/Discover/Database/CreateDatabase/useCreateDatabase.ts @@ -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}`); diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx index d48821fc224..8189576a910 100644 --- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx +++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx @@ -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; } } diff --git a/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts b/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts index ba3dca5e5e5..8642c9f2a5a 100644 --- a/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts +++ b/web/packages/teleport/src/Discover/Shared/ConnectionDiagnostic/useConnectionDiagnostic.ts @@ -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 }; diff --git a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx index 0f53efc5743..6a5e3943c83 100644 --- a/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx +++ b/web/packages/teleport/src/Roles/RoleEditor/RoleEditor.tsx @@ -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 }) => { }; /** Renders an alert if there is an error. */ -const ErrorAlert = ({ error }: { error: Error }) => - error && ( - - {error.message} - +const ErrorAlert = ({ error }: { error: unknown }) => { + const details = error instanceof Error ? error.cause?.toString() : ''; + return ( + error && ( + + {getErrorMessage(error)} + + ) ); +}; const ShowHide = styled(Flex)<{ hidden: boolean }>` display: ${props => (props.hidden ? 'none' : '')}; diff --git a/web/packages/teleport/src/SessionRecordings/view/player/tty/TtyPlayer.ts b/web/packages/teleport/src/SessionRecordings/view/player/tty/TtyPlayer.ts index 88f3921b2dd..c946a442e28 100644 --- a/web/packages/teleport/src/SessionRecordings/view/player/tty/TtyPlayer.ts +++ b/web/packages/teleport/src/SessionRecordings/view/player/tty/TtyPlayer.ts @@ -85,7 +85,7 @@ export class TtyPlayer extends Player { 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; diff --git a/web/packages/teleport/src/components/ClusterDropdown/ClusterDropdown.tsx b/web/packages/teleport/src/components/ClusterDropdown/ClusterDropdown.tsx index 7ac06410201..d210e5f2ce6 100644 --- a/web/packages/teleport/src/components/ClusterDropdown/ClusterDropdown.tsx +++ b/web/packages/teleport/src/components/ClusterDropdown/ClusterDropdown.tsx @@ -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)); } } diff --git a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts index 09e44e1b745..807c0e074ed 100644 --- a/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts +++ b/web/packages/teleport/src/components/ReAuthenticate/useReAuthenticate.ts @@ -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; } diff --git a/web/packages/teleport/src/lib/term/terminal.ts b/web/packages/teleport/src/lib/term/terminal.ts index b62c604f4d0..c5927d6558e 100644 --- a/web/packages/teleport/src/lib/term/terminal.ts +++ b/web/packages/teleport/src/lib/term/terminal.ts @@ -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 { diff --git a/web/packages/teleport/src/lib/useMfa.ts b/web/packages/teleport/src/lib/useMfa.ts index 82484ae7319..ded4f5b44ec 100644 --- a/web/packages/teleport/src/lib/useMfa.ts +++ b/web/packages/teleport/src/lib/useMfa.ts @@ -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, }); } diff --git a/web/packages/teleport/src/services/api/api.ts b/web/packages/teleport/src/services/api/api.ts index 2c939bf5bf9..01b7c8bd7a6 100644 --- a/web/packages/teleport/src/services/api/api.ts +++ b/web/packages/teleport/src/services/api/api.ts @@ -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' ); } diff --git a/web/packages/teleport/src/services/bot/bot.ts b/web/packages/teleport/src/services/bot/bot.ts index 1fc26133aeb..7aecab21857 100644 --- a/web/packages/teleport/src/services/bot/bot.ts +++ b/web/packages/teleport/src/services/bot/bot.ts @@ -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; diff --git a/web/packages/teleport/src/services/version/unsupported.ts b/web/packages/teleport/src/services/version/unsupported.ts index f62b8a2df21..fed16038443 100644 --- a/web/packages/teleport/src/services/version/unsupported.ts +++ b/web/packages/teleport/src/services/version/unsupported.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -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; async function tryV1Fallback(props: FallbackProps) { - if (!props.err.message.includes(ProxyRequiresUpgrade) || hasLabels(props)) { + if ( + !getErrorMessage(props.err).includes(ProxyRequiresUpgrade) || + hasLabels(props) + ) { throw props.err; } diff --git a/web/packages/teleterm/src/agentCleanupDaemon/agentCleanupDaemon.test.ts b/web/packages/teleterm/src/agentCleanupDaemon/agentCleanupDaemon.test.ts index d62d6e602e6..9e0288bfb35 100644 --- a/web/packages/teleterm/src/agentCleanupDaemon/agentCleanupDaemon.test.ts +++ b/web/packages/teleterm/src/agentCleanupDaemon/agentCleanupDaemon.test.ts @@ -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; } diff --git a/web/packages/teleterm/src/deepLinks.ts b/web/packages/teleterm/src/deepLinks.ts index e88c72683bc..f4254f305fc 100644 --- a/web/packages/teleterm/src/deepLinks.ts +++ b/web/packages/teleterm/src/deepLinks.ts @@ -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}:`) { diff --git a/web/packages/teleterm/src/main.ts b/web/packages/teleterm/src/main.ts index 1bba5945e4b..8572b55fb95 100644 --- a/web/packages/teleterm/src/main.ts +++ b/web/packages/teleterm/src/main.ts @@ -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' ); diff --git a/web/packages/teleterm/src/mainProcess/agentDownloader/agentDownloader.ts b/web/packages/teleterm/src/mainProcess/agentDownloader/agentDownloader.ts index 23d989669c0..9d39d9014eb 100644 --- a/web/packages/teleterm/src/mainProcess/agentDownloader/agentDownloader.ts +++ b/web/packages/teleterm/src/mainProcess/agentDownloader/agentDownloader.ts @@ -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; diff --git a/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts index 29c39b4fd6b..5f044c100a4 100644 --- a/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts +++ b/web/packages/teleterm/src/mainProcess/clusterLifecycleManager/clusterLifecycleManager.ts @@ -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() diff --git a/web/packages/teleterm/src/mainProcess/createAgentConfigFile.ts b/web/packages/teleterm/src/mainProcess/createAgentConfigFile.ts index 92e010ae81d..598d94ecc9c 100644 --- a/web/packages/teleterm/src/mainProcess/createAgentConfigFile.ts +++ b/web/packages/teleterm/src/mainProcess/createAgentConfigFile.ts @@ -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; diff --git a/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts b/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts index 83127d503bf..3df28479e70 100644 --- a/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts +++ b/web/packages/teleterm/src/mainProcess/ipcSerializer.test.ts @@ -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'], + }); +}); diff --git a/web/packages/teleterm/src/mainProcess/ipcSerializer.ts b/web/packages/teleterm/src/mainProcess/ipcSerializer.ts index f73fde1147a..0b0914f0fb7 100644 --- a/web/packages/teleterm/src/mainProcess/ipcSerializer.ts +++ b/web/packages/teleterm/src/mainProcess/ipcSerializer.ts @@ -16,16 +16,34 @@ * along with this program. If not, see . */ +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; diff --git a/web/packages/teleterm/src/mainProcess/loadInstallationId/loadInstallationId.ts b/web/packages/teleterm/src/mainProcess/loadInstallationId/loadInstallationId.ts index 4f3b00bd3a8..e292693b1f5 100644 --- a/web/packages/teleterm/src/mainProcess/loadInstallationId/loadInstallationId.ts +++ b/web/packages/teleterm/src/mainProcess/loadInstallationId/loadInstallationId.ts @@ -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 } ); } diff --git a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts index b01e7136d8f..2d24673fd19 100644 --- a/web/packages/teleterm/src/mainProcess/mainProcessClient.ts +++ b/web/packages/teleterm/src/mainProcess/mainProcessClient.ts @@ -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( try { await listener(msg.payload as T); } catch (e) { - ack.error = serializeError(ensureError(e)); + ack.error = serializeError(e); } try { diff --git a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts index be5937a2980..8e1936b43fa 100644 --- a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts +++ b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.test.ts @@ -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; diff --git a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts index fed294d2bb0..254842dea9f 100644 --- a/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts +++ b/web/packages/teleterm/src/mainProcess/profileWatcher/profileWatcher.ts @@ -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 { await access(dirPath); return true; } catch (error) { - if (error.code === 'ENOENT') { + if (isErrnoException(error, 'ENOENT')) { return false; } throw error; diff --git a/web/packages/teleterm/src/mainProcess/windowsManager.ts b/web/packages/teleterm/src/mainProcess/windowsManager.ts index 5e11d119117..6d6577a8c21 100644 --- a/web/packages/teleterm/src/mainProcess/windowsManager.ts +++ b/web/packages/teleterm/src/mainProcess/windowsManager.ts @@ -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; } diff --git a/web/packages/teleterm/src/services/appUpdater/nsisDualModeUpdater.ts b/web/packages/teleterm/src/services/appUpdater/nsisDualModeUpdater.ts index ae9c4774e3a..af0ce46a568 100644 --- a/web/packages/teleterm/src/services/appUpdater/nsisDualModeUpdater.ts +++ b/web/packages/teleterm/src/services/appUpdater/nsisDualModeUpdater.ts @@ -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. diff --git a/web/packages/teleterm/src/services/config/configService.ts b/web/packages/teleterm/src/services/config/configService.ts index ea097e88247..8bc5701e374 100644 --- a/web/packages/teleterm/src/services/config/configService.ts +++ b/web/packages/teleterm/src/services/config/configService.ts @@ -33,7 +33,7 @@ const logger = new Logger('ConfigService'); export type FileLoadingError = { source: 'file-loading'; - error: Error; + error: unknown; }; export type ValidationError = { diff --git a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts index 0332f29abd6..52297d93707 100644 --- a/web/packages/teleterm/src/services/fileStorage/fileStorage.ts +++ b/web/packages/teleterm/src/services/fileStorage/fileStorage.ts @@ -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; } diff --git a/web/packages/teleterm/src/services/grpcCredentials/files.ts b/web/packages/teleterm/src/services/grpcCredentials/files.ts index f7d22041745..d0501e11d98 100644 --- a/web/packages/teleterm/src/services/grpcCredentials/files.ts +++ b/web/packages/teleterm/src/services/grpcCredentials/files.ts @@ -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; diff --git a/web/packages/teleterm/src/services/tshd/cloneableClient.ts b/web/packages/teleterm/src/services/tshd/cloneableClient.ts index 69a694eab7c..e6fce75179d 100644 --- a/web/packages/teleterm/src/services/tshd/cloneableClient.ts +++ b/web/packages/teleterm/src/services/tshd/cloneableClient.ts @@ -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( diff --git a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts index 98cfc613792..f89d2eaa5cf 100644 --- a/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts +++ b/web/packages/teleterm/src/sharedProcess/ptyHost/ptyProcess.ts @@ -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)}` ); } diff --git a/web/packages/teleterm/src/ui/AppInitializer/AppInitializer.tsx b/web/packages/teleterm/src/ui/AppInitializer/AppInitializer.tsx index 7e4143b1c83..809c8e6e784 100644 --- a/web/packages/teleterm/src/ui/AppInitializer/AppInitializer.tsx +++ b/web/packages/teleterm/src/ui/AppInitializer/AppInitializer.tsx @@ -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, }); diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.tsx b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.tsx index 8704e07e0dc..cf8a89e2a8b 100644 --- a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.tsx +++ b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Setup.tsx @@ -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; } diff --git a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Status.tsx b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Status.tsx index 1a6e38ddd6c..fe2af7eb930 100644 --- a/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Status.tsx +++ b/web/packages/teleterm/src/ui/ConnectMyComputer/DocumentConnectMyComputer/Status.tsx @@ -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.`, }); } } diff --git a/web/packages/teleterm/src/ui/DeepLinks/launchDeepLink.ts b/web/packages/teleterm/src/ui/DeepLinks/launchDeepLink.ts index b38eaf94c68..1dfdc45976f 100644 --- a/web/packages/teleterm/src/ui/DeepLinks/launchDeepLink.ts +++ b/web/packages/teleterm/src/ui/DeepLinks/launchDeepLink.ts @@ -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: { diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts b/web/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts index a021b04b19d..a735a54272b 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/Terminal/ctrl.ts @@ -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); diff --git a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts index 08173969c61..44c9433e05e 100644 --- a/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts +++ b/web/packages/teleterm/src/ui/DocumentTerminal/useDocumentTerminal.ts @@ -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); }); } }; diff --git a/web/packages/teleterm/src/ui/Vnet/DocumentVnetDiagReport.tsx b/web/packages/teleterm/src/ui/Vnet/DocumentVnetDiagReport.tsx index 5c01209252e..ab41c6b5bcf 100644 --- a/web/packages/teleterm/src/ui/Vnet/DocumentVnetDiagReport.tsx +++ b/web/packages/teleterm/src/ui/Vnet/DocumentVnetDiagReport.tsx @@ -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; } diff --git a/web/packages/teleterm/src/ui/commandLauncher.ts b/web/packages/teleterm/src/ui/commandLauncher.ts index 6a2a117cc5e..b36df7a2e62 100644 --- a/web/packages/teleterm/src/ui/commandLauncher.ts +++ b/web/packages/teleterm/src/ui/commandLauncher.ts @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +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)}`, }); } ); diff --git a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts index d53c2151cfe..818445023e2 100644 --- a/web/packages/teleterm/src/ui/services/clusters/clustersService.ts +++ b/web/packages/teleterm/src/ui/services/clusters/clustersService.ts @@ -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 { 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 { 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 { } 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 { } 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 { const notificationId = this.notificationsService.notifyError({ title, - description: error.message, + description: getErrorMessage(error), action: { content: 'Retry', onClick: () => { diff --git a/web/packages/teleterm/src/ui/services/usage/usageService.ts b/web/packages/teleterm/src/ui/services/usage/usageService.ts index 339a25e915b..69035ae092d 100644 --- a/web/packages/teleterm/src/ui/services/usage/usageService.ts +++ b/web/packages/teleterm/src/ui/services/usage/usageService.ts @@ -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); } } diff --git a/web/packages/teleterm/src/ui/utils/retryWithRelogin.ts b/web/packages/teleterm/src/ui/utils/retryWithRelogin.ts index 15469a62935..ec385e84526 100644 --- a/web/packages/teleterm/src/ui/utils/retryWithRelogin.ts +++ b/web/packages/teleterm/src/ui/utils/retryWithRelogin.ts @@ -54,7 +54,7 @@ export async function retryWithRelogin( resourceUri: ClusterOrResourceUri, actionToRetry: () => Promise ): Promise { - let retryableErrorFromActionToRetry: Error; + let retryableErrorFromActionToRetry: unknown; try { return await actionToRetry(); } catch (error) {