Skip to content

Commit 2911124

Browse files
feat(web): expose granular RDCleanPath error details (#1117)
Add RDCleanPathDetails struct to provide detailed error information for RDCleanPath errors, including HTTP status codes, WSA error codes, and TLS alert codes. Allows the web client to distinguish between different types of network errors (say, WSAEACCES/10013) instead of showing a generic RDCleanpath error message.
1 parent d0874d6 commit 2911124

File tree

6 files changed

+142
-5
lines changed

6 files changed

+142
-5
lines changed

crates/iron-remote-desktop/src/error.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1+
//! Error handling types and traits for iron-remote-desktop.
2+
//!
3+
//! # Example: Handling RDCleanPath errors
4+
//!
5+
//! ```no_run
6+
//! # use iron_remote_desktop::*;
7+
//! # fn handle_error(error: impl IronError) {
8+
//! match error.kind() {
9+
//! IronErrorKind::RDCleanPath => {
10+
//! if let Some(details) = error.rdcleanpath_details() {
11+
//! // Check for specific HTTP errors
12+
//! if details.http_status_code() == Some(403) {
13+
//! // Handle forbidden/VNET deleted case
14+
//! }
15+
//! // Check for WSA errors
16+
//! if details.wsa_error_code() == Some(10013) {
17+
//! // Handle permission denied
18+
//! }
19+
//! }
20+
//! }
21+
//! _ => {}
22+
//! }
23+
//! # }
24+
//! ```
25+
126
use wasm_bindgen::prelude::*;
227

328
pub trait IronError {
429
fn backtrace(&self) -> String;
530

631
fn kind(&self) -> IronErrorKind;
32+
33+
fn rdcleanpath_details(&self) -> Option<RDCleanPathDetails>;
734
}
835

936
#[derive(Clone, Copy)]
@@ -19,8 +46,83 @@ pub enum IronErrorKind {
1946
AccessDenied,
2047
/// Something wrong happened when sending or receiving the RDCleanPath message
2148
RDCleanPath,
22-
/// Couldnt connect to proxy
49+
/// Couldn't connect to proxy
2350
ProxyConnect,
2451
/// Protocol negotiation failed
2552
NegotiationFailure,
2653
}
54+
55+
/// Detailed error information for RDCleanPath errors.
56+
///
57+
/// When an RDCleanPath error occurs, this structure provides granular details
58+
/// about the underlying cause, including HTTP status codes, Windows Socket errors,
59+
/// and TLS alert codes.
60+
#[derive(Clone, Copy, Debug)]
61+
#[wasm_bindgen]
62+
pub struct RDCleanPathDetails {
63+
http_status_code: Option<u16>,
64+
wsa_error_code: Option<u16>,
65+
tls_alert_code: Option<u8>,
66+
}
67+
68+
// NOTE: multiple impl blocks required because wasm-bindgen doesn't support
69+
// non-exported constructors in #[wasm_bindgen] impl blocks
70+
#[wasm_bindgen]
71+
impl RDCleanPathDetails {
72+
/// HTTP status code if the error originated from an HTTP response.
73+
///
74+
/// Common values:
75+
/// - 403: Forbidden (e.g., deleted VNET, insufficient permissions)
76+
/// - 404: Not Found
77+
/// - 500: Internal Server Error
78+
/// - 502: Bad Gateway
79+
/// - 503: Service Unavailable
80+
#[wasm_bindgen(getter, js_name = httpStatusCode)]
81+
pub fn http_status_code(&self) -> Option<u16> {
82+
self.http_status_code
83+
}
84+
85+
/// Windows Socket API (WSA) error code.
86+
///
87+
/// Common values:
88+
/// - 10013: Permission denied (WSAEACCES) - often indicates deleted/invalid VNET
89+
/// - 10060: Connection timed out (WSAETIMEDOUT)
90+
/// - 10061: Connection refused (WSAECONNREFUSED)
91+
/// - 10051: Network is unreachable (WSAENETUNREACH)
92+
/// - 10065: No route to host (WSAEHOSTUNREACH)
93+
#[wasm_bindgen(getter, js_name = wsaErrorCode)]
94+
pub fn wsa_error_code(&self) -> Option<u16> {
95+
self.wsa_error_code
96+
}
97+
98+
/// TLS alert code if the error occurred during TLS handshake.
99+
///
100+
/// Common values:
101+
/// - 40: Handshake failure
102+
/// - 42: Bad certificate
103+
/// - 45: Certificate expired
104+
/// - 48: Unknown CA
105+
/// - 112: Unrecognized name
106+
#[wasm_bindgen(getter, js_name = tlsAlertCode)]
107+
pub fn tls_alert_code(&self) -> Option<u8> {
108+
self.tls_alert_code
109+
}
110+
}
111+
112+
#[expect(
113+
clippy::allow_attributes,
114+
reason = "Unfortunately, expect attribute doesn't work with clippy::multiple_inherent_impl lint"
115+
)]
116+
#[allow(
117+
clippy::multiple_inherent_impl,
118+
reason = "We don't want to expose the constructor to JS"
119+
)]
120+
impl RDCleanPathDetails {
121+
pub fn new(http_status_code: Option<u16>, wsa_error_code: Option<u16>, tls_alert_code: Option<u8>) -> Self {
122+
Self {
123+
http_status_code,
124+
wsa_error_code,
125+
tls_alert_code,
126+
}
127+
}
128+
}

crates/iron-remote-desktop/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ mod session;
1212
pub use clipboard::{ClipboardData, ClipboardItem};
1313
pub use cursor::CursorStyle;
1414
pub use desktop_size::DesktopSize;
15-
pub use error::{IronError, IronErrorKind};
15+
pub use error::{IronError, IronErrorKind, RDCleanPathDetails};
1616
pub use extension::Extension;
1717
pub use input::{DeviceEvent, InputTransaction, RotationUnit};
1818
pub use session::{Session, SessionBuilder, SessionTerminationInfo};
@@ -431,6 +431,11 @@ macro_rules! make_bridge {
431431
pub fn kind(&self) -> $crate::IronErrorKind {
432432
$crate::IronError::kind(&self.0)
433433
}
434+
435+
#[wasm_bindgen(js_name = rdcleanpathDetails)]
436+
pub fn rdcleanpath_details(&self) -> Option<$crate::RDCleanPathDetails> {
437+
$crate::IronError::rdcleanpath_details(&self.0)
438+
}
434439
}
435440
};
436441
}

crates/ironrdp-web/src/error.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
1-
use iron_remote_desktop::IronErrorKind;
1+
use iron_remote_desktop::{IronErrorKind, RDCleanPathDetails};
22
use ironrdp::connector::{self, sspi, ConnectorErrorKind};
33

44
pub(crate) struct IronError {
55
kind: IronErrorKind,
66
source: anyhow::Error,
7+
rdcleanpath_details: Option<RDCleanPathDetails>,
78
}
89

910
impl IronError {
1011
pub(crate) fn with_kind(mut self, kind: IronErrorKind) -> Self {
1112
self.kind = kind;
1213
self
1314
}
15+
16+
pub(crate) fn with_rdcleanpath_details(mut self, details: RDCleanPathDetails) -> Self {
17+
debug_assert!(
18+
matches!(self.kind, IronErrorKind::RDCleanPath),
19+
"rdcleanpath_details should only be set for RDCleanPath errors"
20+
);
21+
self.rdcleanpath_details = Some(details);
22+
self
23+
}
1424
}
1525

1626
impl iron_remote_desktop::IronError for IronError {
@@ -21,6 +31,10 @@ impl iron_remote_desktop::IronError for IronError {
2131
fn kind(&self) -> IronErrorKind {
2232
self.kind
2333
}
34+
35+
fn rdcleanpath_details(&self) -> Option<RDCleanPathDetails> {
36+
self.rdcleanpath_details
37+
}
2438
}
2539

2640
impl From<connector::ConnectorError> for IronError {
@@ -44,6 +58,7 @@ impl From<connector::ConnectorError> for IronError {
4458
Self {
4559
kind,
4660
source: anyhow::Error::new(e),
61+
rdcleanpath_details: None,
4762
}
4863
}
4964
}
@@ -53,6 +68,7 @@ impl From<ironrdp::session::SessionError> for IronError {
5368
Self {
5469
kind: IronErrorKind::General,
5570
source: anyhow::Error::new(e),
71+
rdcleanpath_details: None,
5672
}
5773
}
5874
}
@@ -62,6 +78,7 @@ impl From<anyhow::Error> for IronError {
6278
Self {
6379
kind: IronErrorKind::General,
6480
source: e,
81+
rdcleanpath_details: None,
6582
}
6683
}
6784
}

crates/ironrdp-web/src/session.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1113,9 +1113,15 @@ where
11131113
server_addr: _,
11141114
} => (x224_connection_response, server_cert_chain),
11151115
ironrdp_rdcleanpath::RDCleanPath::GeneralErr(error) => {
1116+
let details = iron_remote_desktop::RDCleanPathDetails::new(
1117+
error.http_status_code,
1118+
error.wsa_last_error,
1119+
error.tls_alert_code,
1120+
);
11161121
return Err(
11171122
IronError::from(anyhow::Error::new(error).context("received an RDCleanPath error"))
1118-
.with_kind(IronErrorKind::RDCleanPath),
1123+
.with_kind(IronErrorKind::RDCleanPath)
1124+
.with_rdcleanpath_details(details),
11191125
);
11201126
}
11211127
ironrdp_rdcleanpath::RDCleanPath::NegotiationErr {

web-client/iron-remote-desktop/src/interfaces/Error.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,14 @@
88
NegotiationFailure = 6,
99
}
1010

11+
export interface RDCleanPathDetails {
12+
readonly httpStatusCode?: number;
13+
readonly wsaErrorCode?: number;
14+
readonly tlsAlertCode?: number;
15+
}
16+
1117
export interface IronError {
1218
backtrace: () => string;
1319
kind: () => IronErrorKind;
20+
rdcleanpathDetails: () => RDCleanPathDetails | undefined;
1421
}

web-client/iron-remote-desktop/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
export * as default from './iron-remote-desktop.svelte';
22
export type { ResizeEvent } from './interfaces/ResizeEvent';
33
export type { NewSessionInfo } from './interfaces/NewSessionInfo';
4-
export type { IronError, IronErrorKind } from './interfaces/Error';
4+
export type { IronError, IronErrorKind, RDCleanPathDetails } from './interfaces/Error';
55
export type { SessionTerminationInfo } from './interfaces/SessionTerminationInfo';
66
export type { ClipboardData } from './interfaces/ClipboardData';
77
export type { ClipboardItem } from './interfaces/ClipboardItem';

0 commit comments

Comments
 (0)