Skip to content

Ensure all errors thrown by TdpClient are instances of Error class#54877

Merged
gzdunek merged 10 commits intomasterfrom
gzdunek/ensure-error
May 22, 2025
Merged

Ensure all errors thrown by TdpClient are instances of Error class#54877
gzdunek merged 10 commits intomasterfrom
gzdunek/ensure-error

Conversation

@gzdunek
Copy link
Copy Markdown
Contributor

@gzdunek gzdunek commented May 16, 2025

The error listener in TdpClient specifies its error type as Error, but this assumption may not always be true at runtime. In JS, it's possible to throw any value: instances of Error, but also strings, numbers, etc.

To improve this, I changed the following:

  1. Added a ensureError that can convert any value to Error instance.
    • getErrorMessage uses this function under the hood now.
  2. Added types to EventEmitter in TdpClient to make it more difficult to emit wrong value for a given even type.

@gzdunek gzdunek added no-changelog Indicates that a PR does not require a changelog entry backport/branch/v16 backport/branch/v17 labels May 16, 2025
@github-actions github-actions Bot requested review from kiosion and ryanclark May 16, 2025 12:33
@gzdunek gzdunek requested review from kimlisa and ravicious and removed request for kiosion and ryanclark May 16, 2025 12:33
@gzdunek gzdunek force-pushed the gzdunek/ensure-error branch from b802d36 to 3897283 Compare May 16, 2025 12:35
@gzdunek gzdunek force-pushed the gzdunek/ensure-error branch from 3897283 to fabcd1a Compare May 16, 2025 12:42
Comment thread web/packages/shared/utils/error.ts Outdated
Comment thread web/packages/shared/utils/error.ts Outdated
* Converts any unknown input into an Error instance.
* Preserves message and name if available.
*/
export function ensureError(err: unknown): Error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you maybe have any articles on consequences of using this approach? By "this approach" I really mean trying to paper over the fact that error handling in JS is kinda bad.

I think it's fine if this function is meant to fix an issue in TdpClient, but if we were to use it in every catch statement, I'd definitely want to maybe see if there's someone else who did this and if they ran into any unforeseen problems.

But it also makes me think, is ensureError necessary in TdpClient in the first place? Does TdpClient or its users need to do anything specific with the error? If so, would it be enough if it used unknown instead of Error and the callsites first checked instanceof instead? I wonder why it's important for values in TdpClient to always be of Error class.

Often when an API is bad on a language level, I feel like it's better to pass that awfulness further rather than trying to paper over it. Let the callsites deal with something other than an Error being thrown. Trying to paper over it leads to some tricky edge cases like returning new Error() in response to null or undefined being thrown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just found an article with the same approach: https://medium.com/with-orus/the-5-commandments-of-clean-error-handling-in-typescript-93a9cbdf1af5

MDN mentions error normalization https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError#normalizing_caught_errors

But it also makes me think, is ensureError necessary in TdpClient in the first place? Does TdpClient or its users need to do anything specific with the error? If so, would it be enough if it used unknown instead of Error and the callsites first checked instanceof instead? I wonder why it's important for values in TdpClient to always be of Error class.

Yeah, ensureError it's not strictly needed, I even initially planned to use unknown.
But then I thought that maybe it's better to return something more meaningful than unknown, so callsites don’t have to guess how to handle it.

I took a look at how we handle errors across different parts of the codebase, and it seems we often assume that any thrown value is an instance of Error (I know that useUnknownInCatchVariables would help with it). But still normalizing the error could help reduce edge cases. As mentioned in the first article, handling the basic case (err instanceof Error) is straightforward but it's often not clear how to deal with the rest.

Some examples where normalizing the error first would make things easier:

Anyway, I'm fine with removing ensureError but maybe there's still some value in it :)

Copy link
Copy Markdown
Member

@ravicious ravicious May 16, 2025

Choose a reason for hiding this comment

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

But then I thought that maybe it's better to return something more meaningful than unknown, so callsites don’t have to guess how to handle it.

But that's the thing, most of the time they don't have to guess because they just want to be able to serialize the error somehow, unless they do something special when a specific error is thrown.

I checked how TdpClientEvent.ERROR is used. There are three callsites, one of them ignores the error altogether and the other two do error.message || error.toString().

Callsites

useListener(
client.onError,
useCallback(
() => documentsService.update(uri, { status: 'error' }),
[documentsService, uri]
)
);

const clientOnError = useCallback((error: Error) => {
setPlayerStatus(StatusEnum.ERROR);
setStatusText(error.message || error.toString());
}, []);

const handleFatalError = useCallback(
(error: Error) => {
setDirectorySharingState(defaultDirectorySharingState);
setClipboardSharingState(defaultClipboardSharingState);
setTdpConnectionStatus({
status: 'disconnected',
fromTdpError: error instanceof TdpError,
message: error.message || error.toString(),
});
initialTdpConnectionSucceeded.current = false;
},
[setClipboardSharingState, setDirectorySharingState]
);

Of course, without typing error as Error in an unsound way, the error handling code wouldn't be as straightforward.

I'm just not fully sold on using ensureError for absolutely everything, but I have nothing against giving it a try.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm just not fully sold on using ensureError for absolutely everything, but I have nothing against giving it a try.

I agree, we don't have to use it everywhere. We can give it a try in a few places, just to see how it feels in practice.

I fixed the callsites, they no longer need || error.toString().

Comment thread web/packages/shared/utils/error.ts Outdated
Comment thread web/packages/shared/utils/error.test.ts Outdated
@gzdunek gzdunek requested a review from ravicious May 21, 2025 08:09
Comment thread web/packages/shared/utils/error.ts Outdated
return input;
}

if (input === null || input === undefined) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

just curious, i've been using input == null to test for both null and undefined, should i be doing it more explicitly like this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I use the explicit version because I can never remember the coercion rules for == 😅

It’s probably just personal preference, but I find === a lot easier to reason about.

@gzdunek gzdunek enabled auto-merge May 22, 2025 08:00
@gzdunek gzdunek added this pull request to the merge queue May 22, 2025
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 22, 2025
@gzdunek gzdunek added this pull request to the merge queue May 22, 2025
Merged via the queue into master with commit 4d28408 May 22, 2025
40 checks passed
@gzdunek gzdunek deleted the gzdunek/ensure-error branch May 22, 2025 08:32
@backport-bot-workflows
Copy link
Copy Markdown
Contributor

@gzdunek See the table below for backport results.

Branch Result
branch/v16 Failed
branch/v17 Failed

gzdunek added a commit that referenced this pull request May 22, 2025
#54877)

* Add a utility function to convert any input to an Error instance

* Add types for TdpClient events

* Always throw Error from `adaptWebSocketToTdpTransport`

* Improve getting the error message in `getErrMessage`

* Move `isAbortError` to error.ts

* Remove unused `tshd/errors.ts`

* Handle `JSON.stringify` error, add `cause` to thrown errors

* Remove unnecessary `async`

* Remove `error.toString` from the callsites

* Handle `err` being undefined

(cherry picked from commit 4d28408)
gzdunek added a commit that referenced this pull request May 22, 2025
#54877)

* Add a utility function to convert any input to an Error instance

* Add types for TdpClient events

* Always throw Error from `adaptWebSocketToTdpTransport`

* Improve getting the error message in `getErrMessage`

* Move `isAbortError` to error.ts

* Remove unused `tshd/errors.ts`

* Handle `JSON.stringify` error, add `cause` to thrown errors

* Remove unnecessary `async`

* Remove `error.toString` from the callsites

* Handle `err` being undefined

(cherry picked from commit 4d28408)
github-merge-queue Bot pushed a commit that referenced this pull request May 22, 2025
…` class (#55043)

* Ensure all errors thrown by `TdpClient` are instances of `Error` class (#54877)

* Add a utility function to convert any input to an Error instance

* Add types for TdpClient events

* Always throw Error from `adaptWebSocketToTdpTransport`

* Improve getting the error message in `getErrMessage`

* Move `isAbortError` to error.ts

* Remove unused `tshd/errors.ts`

* Handle `JSON.stringify` error, add `cause` to thrown errors

* Remove unnecessary `async`

* Remove `error.toString` from the callsites

* Handle `err` being undefined

(cherry picked from commit 4d28408)

* Remove `isUnimplementedError` usage
github-merge-queue Bot pushed a commit that referenced this pull request Jun 2, 2025
…54926)

* Show Windows desktops in Connect (#53955)

* Show Windows desktops in Connect

* Use `net.SplitHostPort`

* Use underscore in URIs

* Omit the default RDP port before displaying item in the UI

(cherry picked from commit 81af813)

* Add desktop session-related ACLs to Connect (#54031)

* Add desktop session-related ACLs to Connect

* Add new fields to `makeAcl`

(cherry picked from commit aba0de0)

* Add latency detector for desktop sessions (#52827)

* wip

* add ping message and latency from desktop side

* version

* Fix backward compatibility

* godocs

* formatting

* fix imports

* formatting

* formatting

* log and gci

* lint

* Apply tooltip on the icon directly, instead of on the Menu component

This fixes a problem where onMouseLeave in HoverTooltip wasn't called and the tooltip didn't disappear.

* Use consistent spacing between top bar elements

* updates from origin

* e

* e

* lint

* rework UI after merge

* Rename fields in backend

* prettier

* Update web/packages/shared/components/DesktopSession/TopBar.tsx

Co-authored-by: Grzegorz Zdunek <gzdunek@users.noreply.github.com>

* review comments

* fix ui

* add env var to disable windows desktop "ping"

* review comment

* review comment

* Update lib/web/desktop.go

Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com>

* Refactor latency monitoring

* fix spelling

* remove monitorSessionLatency

* gci

* review comment

* review comment

* Update RFD and version

* fix gaps

---------

Co-authored-by: Zac Bergquist <zac.bergquist@goteleport.com>
Co-authored-by: Grzegorz Zdunek <grzegorz.zdunek@goteleport.com>
Co-authored-by: Grzegorz Zdunek <gzdunek@users.noreply.github.com>

(cherry picked from commit 1aba870)

* Abstract directory sharing file system (#54545)

* Extract an interface to interact with a shared directory file system

Some methods were renamed to more closely follow common file system naming conventions.

* Use the new interface in the TDP client

* Provide a browser file system as the shared directory access implementation

(cherry picked from commit d3fbeeb)

* Support desktop access in Connect  (#54373)

* Extract reusable function for establishing connections to Windows Desktop Service

* Add `ProxyWindowsDesktopSession` proto

* Implement `ProxyWindowsDesktopSession`

* Enable fetching desktops and desktop services in remote proxy cache

* Implement dialing windows desktop

* Implement client

* Support Windows desktop certs in tsh

* Fix incorrect `windowsDesktop` URI

* Add proto for `ConnectToDesktop`

* Implement `ConnectToDesktop`

* Do not log requests/responses for `ConnectToDesktop` RPC

* Add boilerplate for `DocumentDesktopSession`

* Open a desktop connection

* Relax ArrayBuffer type passed to encode methods, ignore tshd abort errors

In tshd stream, the buffer is of type `ArrayBufferLike` (which is `ArrayBuffer` & `SharedArrayBuffer`). To allow assigning it to the type in our TDP code, we make it more general.

* Ensure WASM IronRDP code is initialized only once

* Use `utils.ShuffleVisit`

* Improve stream cancellation handling

* Leave a TODO about ListWindowsDesktops

* Do not return empty data slice

* Provide non-nil src and dest addresses to `streamutils.NewConn()`

* Do not emit an empty message to indicate a successful connection

* Fix test

* Simplify code

* Add missing `WindowsDesktopTLSCredentials` initialization

* Require that the first message is only a dial request and the subsequent ones are only data

* Add explicit `stop()` check

* Hold cluster name and desktop name in a struct for the map key

* Do not return early on non-connection problem errors

* Handle io.EOF error specifically in BidiStreamingClient.Send instead of in `tlsConn.HandshakeContext`

* Extract a common function to proxy TDP connections

* Improve proto comments and connection setup

* Add comments and logs

* Lint

* Explain why there's a special handling for abort error

* Bring back the original `proxyWebsocketConn` behavior when it comes to error handling

* Post merge fixes

* Adjust proxying TDP connection to changes from master

* Lint

* Channels improvements

* Post merge fixes

(cherry picked from commit 980ce61)

* Show desktops in connection tracker (#54668)

* Make supporting non-Windows desktops easier in the future

* Add connection boilerplate for desktop connections

* Show connected/disconnected status in the connection tracker

* Make sure that ACLs were fetched before reading from them

ACLs are fetched asynchronously.

* Do not crash when reading unsupported connection from app_state.json

* `gwDoc` -> `doc`

* `windowsDesktops` -> `windows_desktops`

(cherry picked from commit 9f49376)

* Implement shared directory file system in tsh daemon  (#54662)

* Implement shared directory file system in tshd

* Remove `Open` method

* Do not use `os.IsNotExist`

* Read bytes into `[]byte` parameter

* Correctly use `t.Helper()`

* Add missing `trace.Wrap`

* Rename receivers

* Handle errors from `file.Close()`

* Use named returns to return close error

* Improve error handling

(cherry picked from commit b3e9199)

* Support directory sharing in Connect  (#54663)

* Register directory access when starting a desktop session

* Add RPC to attach a directory to desktop session

* Do not allow `attachDirectoryToDesktopSession` to be called from the renderer process

* Open the directory picker and send the selected path to tshd

* Intercept file system events coming from the server and handle them

* Disallow file system messages to be sent from the renderer

* Refactor dir sharing

* `AttachDirectoryToDesktopSession` -> `SetSharedDirectoryForDesktopSession`

* Improve comments

* Small fixes

* Add missing defer for `s.dirAccessMu.RUnlock()`

* `TestOpenSharedDirectory` -> `TestNewDirectoryAccess`

* Add a comment for JS file system handlers

* `make grpc`

---------

Co-authored-by: Rafał Cieślak <rafal.cieslak@goteleport.com>

(cherry picked from commit 45428c0)

* Simplify code around latency detection for desktop (#54795)

* Simplify code around latency detection for desktop

* cleanup

* Use tdp.Conn for client and server

* Add check for context cancellation

* Cleanup

* remove context check

* remove context check

* remove unused field

* use sync.OnceFunc, error on unexpected ping

* err message

(cherry picked from commit 28f78c3)

* Show `desktop access requires Teleport Proxy 17.5.0 or higher` when Proxy returns NotImplementedError

* Add `wasm-unsafe-eval` to Connect CSP (#54916)

(cherry picked from commit ee8be4d)

* Ensure all errors thrown by `TdpClient` are instances of `Error` class (#54877)

* Add a utility function to convert any input to an Error instance

* Add types for TdpClient events

* Always throw Error from `adaptWebSocketToTdpTransport`

* Improve getting the error message in `getErrMessage`

* Move `isAbortError` to error.ts

* Remove unused `tshd/errors.ts`

* Handle `JSON.stringify` error, add `cause` to thrown errors

* Remove unnecessary `async`

* Remove `error.toString` from the callsites

* Handle `err` being undefined

(cherry picked from commit 4d28408)

---------

Co-authored-by: Przemko Robakowski <przemko.robakowski@goteleport.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport/branch/v17 no-changelog Indicates that a PR does not require a changelog entry size/md ui

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants