diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx index 4259fde7fbf56..4fef60b3badc6 100644 --- a/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx +++ b/web/packages/shared/components/DesktopSession/DesktopSession.story.tsx @@ -30,6 +30,7 @@ import { TdpClient, TdpClientEvent, } from 'shared/libs/tdp'; +import { TdpError as RemoteTdpError } from 'shared/libs/tdp/client'; import { DesktopSession, DesktopSessionProps } from './DesktopSession'; @@ -86,7 +87,15 @@ export const FetchError = () => ( export const TdpError = () => { const client = fakeClient(); client.connect = async () => { - client.emit(TdpClientEvent.ERROR, new Error('some tdp error')); + client.emit( + TdpClientEvent.ERROR, + new RemoteTdpError( + 'RDP client exited with an error: Connection Timed Out.\n\n' + + 'Teleport could not connect to the host within the timeout period. This may be due to a firewall blocking the connection, an overloaded system, or network congestion.\n\n' + + 'To resolve this issue, ensure that the Teleport agent can reach the Windows host.\n\n' + + 'You can use the command "nc -vz HOST 3389" to help diagnose connectivity problems.' + ) + ); }; return ; @@ -96,10 +105,10 @@ export const Connected = () => { return ; }; -export const Disconnected = () => { +export const DisconnectedWithNoMessage = () => { const client = fakeClient(); client.connect = async () => { - client.emit(TdpClientEvent.TRANSPORT_CLOSE, 'session disconnected'); + client.emit(TdpClientEvent.TRANSPORT_CLOSE); }; return ; diff --git a/web/packages/shared/components/DesktopSession/DesktopSession.tsx b/web/packages/shared/components/DesktopSession/DesktopSession.tsx index fd24992c90f4f..278c5e38ff1d2 100644 --- a/web/packages/shared/components/DesktopSession/DesktopSession.tsx +++ b/web/packages/shared/components/DesktopSession/DesktopSession.tsx @@ -37,6 +37,7 @@ import { TdpClient, useListener, } from 'shared/libs/tdp'; +import { TdpError } from 'shared/libs/tdp/client'; import { KeyboardHandler } from './KeyboardHandler'; import TopBar from './TopBar'; @@ -130,7 +131,8 @@ export function DesktopSession({ setClipboardSharingState(defaultClipboardSharingState); setTdpConnectionStatus({ status: 'disconnected', - message: error.message || error.toString(), + fromTdpError: error instanceof TdpError, + message: error?.message || error?.toString(), }); initialTdpConnectionSucceeded.current = false; }, @@ -166,8 +168,11 @@ export function DesktopSession({ useListener( client.onTransportClose, useCallback( - statusText => { - setTdpConnectionStatus({ status: 'disconnected', message: statusText }); + error => { + setTdpConnectionStatus({ + status: 'disconnected', + message: error?.message || error?.toString(), + }); initialTdpConnectionSucceeded.current = false; }, [setTdpConnectionStatus] @@ -350,7 +355,7 @@ export function DesktopSession({ /> )} {screenState.state === 'custom' && screenState.component} - {screenState.state === 'error' && ( + {screenState.state === 'disconnected' && ( )} {screenState.state === 'processing' && } @@ -447,7 +452,7 @@ function getScreenState( if (aclAttempt.status === 'error') { return { - state: 'error', + state: 'disconnected', message: { title: 'Could not fetch session details', details: aclAttempt.statusText, @@ -456,7 +461,7 @@ function getScreenState( } if (anotherDesktopActiveAttempt.status === 'error') { return { - state: 'error', + state: 'disconnected', message: { title: 'Could not fetch session details', details: anotherDesktopActiveAttempt.statusText, @@ -465,8 +470,8 @@ function getScreenState( } if (tdpConnectionStatus.status === 'disconnected') { return { - state: 'error', - message: { title: tdpConnectionStatus.message }, + state: 'disconnected', + message: { title: tdpConnectionStatus.message || 'Session disconnected' }, }; } @@ -499,6 +504,7 @@ type TdpConnectionStatus = */ | { status: 'disconnected'; + fromTdpError?: boolean; message: string; }; @@ -508,6 +514,11 @@ type ScreenState = | { state: 'processing' } | { state: 'canvas-visible' } | { - state: 'error'; - message: { title: string; details?: string }; + state: 'disconnected'; + message: DisconnectedMessage; }; + +interface DisconnectedMessage { + title: string; + details?: string; +} diff --git a/web/packages/shared/libs/tdp/client.ts b/web/packages/shared/libs/tdp/client.ts index 122e91ffa40af..fe694939c0154 100644 --- a/web/packages/shared/libs/tdp/client.ts +++ b/web/packages/shared/libs/tdp/client.ts @@ -144,7 +144,7 @@ export class TdpClient extends EventEmitter { this.transportAbortController.signal ); } catch (error) { - this.emit(TdpClientEvent.ERROR, error.message); + this.emit(TdpClientEvent.ERROR, error); return; } @@ -183,11 +183,11 @@ export class TdpClient extends EventEmitter { // 'Processing' errors are the most important. if (processingError) { - this.emit(TdpClientEvent.ERROR, processingError.message); + this.emit(TdpClientEvent.ERROR, processingError); } else if (connectionError) { - this.emit(TdpClientEvent.TRANSPORT_CLOSE, connectionError.message); + this.emit(TdpClientEvent.TRANSPORT_CLOSE, connectionError); } else { - this.emit(TdpClientEvent.TRANSPORT_CLOSE, 'Session disconnected'); + this.emit(TdpClientEvent.TRANSPORT_CLOSE); } this.logger.info('Transport is closed'); @@ -235,7 +235,7 @@ export class TdpClient extends EventEmitter { return () => this.off(TdpClientEvent.TDP_WARNING, listener); }; - onTransportClose = (listener: (message: string) => void) => { + onTransportClose = (listener: (error?: Error) => void) => { this.on(TdpClientEvent.TRANSPORT_CLOSE, listener); return () => this.off(TdpClientEvent.TRANSPORT_CLOSE, listener); }; @@ -391,7 +391,7 @@ export class TdpClient extends EventEmitter { handleTdpNotification(buffer: ArrayBuffer) { const notification = this.codec.decodeNotification(buffer); if (notification.severity === Severity.Error) { - throw new Error(notification.message); + throw new TdpError(notification.message); } else if (notification.severity === Severity.Warning) { this.handleWarning(notification.message, TdpClientEvent.TDP_WARNING); } else { @@ -799,3 +799,11 @@ export function useListener( }; }, [emitter, listener]); } + +/** Represents an alert emitted by the TDP service with "error" severity. */ +export class TdpError extends Error { + constructor(message: string) { + super(message); + this.name = 'TdpError'; + } +} diff --git a/web/packages/teleport/src/lib/tdp/webSocketTransportAdapter.ts b/web/packages/teleport/src/lib/tdp/webSocketTransportAdapter.ts index 0eed87426a0ad..92c8c153f4aca 100644 --- a/web/packages/teleport/src/lib/tdp/webSocketTransportAdapter.ts +++ b/web/packages/teleport/src/lib/tdp/webSocketTransportAdapter.ts @@ -30,6 +30,9 @@ export async function adaptWebSocketToTdpTransport( socket: WebSocket, signal: AbortSignal ): Promise { + if (signal.aborted) { + throw new DOMException('Websocket was aborted.', 'AbortError'); + } // WebsocketCloseCode.NORMAL signal.addEventListener('abort', () => socket.close(1000)); socket.binaryType = 'arraybuffer';