From 154f44c574e1d72d7d0241807c8238fc134e91a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Wed, 21 Jan 2026 18:50:09 +0100 Subject: [PATCH 1/3] fix: replace unwraps with errors in fullstack WebSockets --- Cargo.lock | 2 + packages/fullstack-core/Cargo.toml | 2 + packages/fullstack-core/src/error.rs | 57 ++++++++++++++++++++ packages/fullstack/src/payloads/websocket.rs | 54 ++++++++++++++----- 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f354c008c3..86471c6510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5989,11 +5989,13 @@ dependencies = [ "http 1.4.0", "inventory", "parking_lot", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.17", "tokio", "tracing", + "tungstenite 0.27.0", ] [[package]] diff --git a/packages/fullstack-core/Cargo.toml b/packages/fullstack-core/Cargo.toml index 1947bacde8..90bf06ca53 100644 --- a/packages/fullstack-core/Cargo.toml +++ b/packages/fullstack-core/Cargo.toml @@ -24,6 +24,8 @@ tracing = { workspace = true } thiserror = { workspace = true } axum-core = { workspace = true } http = { workspace = true } +reqwest = { workspace = true } +tungstenite = { workspace = true } anyhow = { workspace = true } inventory = { workspace = true } serde_json = { workspace = true } diff --git a/packages/fullstack-core/src/error.rs b/packages/fullstack-core/src/error.rs index cb95da6ce7..963532e570 100644 --- a/packages/fullstack-core/src/error.rs +++ b/packages/fullstack-core/src/error.rs @@ -312,3 +312,60 @@ impl RequestError { } } } + +impl From for RequestError { + fn from(value: reqwest::Error) -> Self { + const DEFAULT_STATUS_CODE: u16 = 0; + let string = value.to_string(); + if value.is_builder() { + Self::Builder(string) + } else if value.is_redirect() { + Self::Redirect(string) + } else if value.is_status() { + Self::Status( + string, + value + .status() + .as_ref() + .map(StatusCode::as_u16) + .unwrap_or(DEFAULT_STATUS_CODE), + ) + } else if value.is_body() { + Self::Body(string) + } else if value.is_decode() { + Self::Decode(string) + } else if value.is_upgrade() { + Self::Connect(string) + } else { + Self::Request(string) + } + } +} + +impl From for RequestError { + fn from(value: tungstenite::Error) -> Self { + match value { + tungstenite::Error::ConnectionClosed => { + Self::Connect("websocket connection closed".to_owned()) + } + tungstenite::Error::AlreadyClosed => { + Self::Connect("websocket already closed".to_owned()) + } + tungstenite::Error::Io(error) => Self::Connect(error.to_string()), + tungstenite::Error::Tls(error) => Self::Connect(error.to_string()), + tungstenite::Error::Capacity(error) => Self::Body(error.to_string()), + tungstenite::Error::Protocol(error) => Self::Request(error.to_string()), + tungstenite::Error::WriteBufferFull(message) => Self::Body(message.to_string()), + tungstenite::Error::Utf8(error) => Self::Decode(error), + tungstenite::Error::AttackAttempt => { + Self::Request("Tungstenite attack attempt detected".to_owned()) + } + tungstenite::Error::Url(error) => Self::Builder(error.to_string()), + tungstenite::Error::Http(response) => { + let status_code = response.status(); + Self::Status(format!("HTTP error: {status_code}"), status_code.as_u16()) + } + tungstenite::Error::HttpFormat(error) => Self::Builder(error.to_string()), + } + } +} diff --git a/packages/fullstack/src/payloads/websocket.rs b/packages/fullstack/src/payloads/websocket.rs index 71be0dc420..76a64bc42b 100644 --- a/packages/fullstack/src/payloads/websocket.rs +++ b/packages/fullstack/src/payloads/websocket.rs @@ -607,7 +607,7 @@ impl IntoRequest for WebSocketOptions { .map(String::as_str) .collect::>(), ) - .unwrap(); + .map_err(|error| RequestError::Connect(error.to_string()))?; return Ok(UpgradingWebsocket { protocol: Some(socket.protocol()), @@ -619,14 +619,11 @@ impl IntoRequest for WebSocketOptions { #[cfg(not(target_arch = "wasm32"))] { - let response = native::send_request(request, &self.protocols) - .await - .unwrap(); + let response = native::send_request(request, &self.protocols).await?; let (inner, protocol) = response .into_stream_and_protocol(self.protocols, None) - .await - .unwrap(); + .await?; return Ok(UpgradingWebsocket { protocol, @@ -843,14 +840,6 @@ pub enum WebsocketError { /// Error during serialization/deserialization. #[error("error during serialization/deserialization")] Serialization(Box), - - /// Error during serialization/deserialization. - #[error("serde_json error")] - Json(#[from] serde_json::Error), - - /// Error during serialization/deserialization. - #[error("ciborium error")] - Cbor(#[from] ciborium::de::Error), } #[cfg(feature = "web")] @@ -886,6 +875,25 @@ impl WebsocketError { } } +impl From for RequestError { + fn from(value: WebsocketError) -> Self { + match value { + WebsocketError::ConnectionClosed { code, description } => { + Self::Connect(format!("connection closed ({code}): {description}")) + } + WebsocketError::AlreadyClosed => Self::Connect(value.to_string()), + WebsocketError::Capacity => Self::Body(value.to_string()), + WebsocketError::Unexpected => Self::Request(value.to_string()), + WebsocketError::Uninitialized => Self::Builder(value.to_string()), + WebsocketError::Handshake(error) => error.into(), + WebsocketError::Reqwest(error) => error.into(), + WebsocketError::Tungstenite(error) => error.into(), + WebsocketError::Serialization(error) => Self::Serialization(error.to_string()), + WebsocketError::Deserialization(error) => Self::Decode(error.to_string()), + } + } +} + #[cfg(feature = "web")] struct WebsysSocket { sender: Mutex>, @@ -1139,6 +1147,7 @@ mod native { use crate::ClientRequest; use super::{CloseCode, Message, WebsocketError}; + use dioxus_fullstack_core::RequestError; use reqwest::{ header::{HeaderName, HeaderValue}, Response, StatusCode, Version, @@ -1261,6 +1270,23 @@ mod native { UnexpectedStatusCode(StatusCode), } + impl From for RequestError { + fn from(value: HandshakeError) -> Self { + let string = value.to_string(); + match value { + HandshakeError::UnexpectedStatusCode(status) => { + Self::Status(string, status.as_u16()) + } + HandshakeError::UnsupportedHttpVersion(_) + | HandshakeError::MissingHeader { .. } + | HandshakeError::UnexpectedHeaderValue { .. } + | HandshakeError::ExpectedAProtocol + | HandshakeError::UnexpectedProtocol { .. } + | HandshakeError::ServerRespondedWithDifferentVersion => Self::Connect(string), + } + } + } + pub struct WebSocketResponse { pub response: Response, pub version: Version, From 5a2299c54a8ee452ba04d10fbeecb021837c0d34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Wed, 21 Jan 2026 20:57:55 +0100 Subject: [PATCH 2/3] refactor: pull the conversions to fullstack `RequestError` up from the core crate --- Cargo.lock | 2 - packages/fullstack-core/Cargo.toml | 2 - packages/fullstack-core/src/error.rs | 57 -------- packages/fullstack/src/payloads/websocket.rs | 134 ++++++++++++++----- 4 files changed, 98 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 86471c6510..f354c008c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5989,13 +5989,11 @@ dependencies = [ "http 1.4.0", "inventory", "parking_lot", - "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.17", "tokio", "tracing", - "tungstenite 0.27.0", ] [[package]] diff --git a/packages/fullstack-core/Cargo.toml b/packages/fullstack-core/Cargo.toml index 90bf06ca53..1947bacde8 100644 --- a/packages/fullstack-core/Cargo.toml +++ b/packages/fullstack-core/Cargo.toml @@ -24,8 +24,6 @@ tracing = { workspace = true } thiserror = { workspace = true } axum-core = { workspace = true } http = { workspace = true } -reqwest = { workspace = true } -tungstenite = { workspace = true } anyhow = { workspace = true } inventory = { workspace = true } serde_json = { workspace = true } diff --git a/packages/fullstack-core/src/error.rs b/packages/fullstack-core/src/error.rs index 963532e570..cb95da6ce7 100644 --- a/packages/fullstack-core/src/error.rs +++ b/packages/fullstack-core/src/error.rs @@ -312,60 +312,3 @@ impl RequestError { } } } - -impl From for RequestError { - fn from(value: reqwest::Error) -> Self { - const DEFAULT_STATUS_CODE: u16 = 0; - let string = value.to_string(); - if value.is_builder() { - Self::Builder(string) - } else if value.is_redirect() { - Self::Redirect(string) - } else if value.is_status() { - Self::Status( - string, - value - .status() - .as_ref() - .map(StatusCode::as_u16) - .unwrap_or(DEFAULT_STATUS_CODE), - ) - } else if value.is_body() { - Self::Body(string) - } else if value.is_decode() { - Self::Decode(string) - } else if value.is_upgrade() { - Self::Connect(string) - } else { - Self::Request(string) - } - } -} - -impl From for RequestError { - fn from(value: tungstenite::Error) -> Self { - match value { - tungstenite::Error::ConnectionClosed => { - Self::Connect("websocket connection closed".to_owned()) - } - tungstenite::Error::AlreadyClosed => { - Self::Connect("websocket already closed".to_owned()) - } - tungstenite::Error::Io(error) => Self::Connect(error.to_string()), - tungstenite::Error::Tls(error) => Self::Connect(error.to_string()), - tungstenite::Error::Capacity(error) => Self::Body(error.to_string()), - tungstenite::Error::Protocol(error) => Self::Request(error.to_string()), - tungstenite::Error::WriteBufferFull(message) => Self::Body(message.to_string()), - tungstenite::Error::Utf8(error) => Self::Decode(error), - tungstenite::Error::AttackAttempt => { - Self::Request("Tungstenite attack attempt detected".to_owned()) - } - tungstenite::Error::Url(error) => Self::Builder(error.to_string()), - tungstenite::Error::Http(response) => { - let status_code = response.status(); - Self::Status(format!("HTTP error: {status_code}"), status_code.as_u16()) - } - tungstenite::Error::HttpFormat(error) => Self::Builder(error.to_string()), - } - } -} diff --git a/packages/fullstack/src/payloads/websocket.rs b/packages/fullstack/src/payloads/websocket.rs index 76a64bc42b..636de75601 100644 --- a/packages/fullstack/src/payloads/websocket.rs +++ b/packages/fullstack/src/payloads/websocket.rs @@ -875,25 +875,6 @@ impl WebsocketError { } } -impl From for RequestError { - fn from(value: WebsocketError) -> Self { - match value { - WebsocketError::ConnectionClosed { code, description } => { - Self::Connect(format!("connection closed ({code}): {description}")) - } - WebsocketError::AlreadyClosed => Self::Connect(value.to_string()), - WebsocketError::Capacity => Self::Body(value.to_string()), - WebsocketError::Unexpected => Self::Request(value.to_string()), - WebsocketError::Uninitialized => Self::Builder(value.to_string()), - WebsocketError::Handshake(error) => error.into(), - WebsocketError::Reqwest(error) => error.into(), - WebsocketError::Tungstenite(error) => error.into(), - WebsocketError::Serialization(error) => Self::Serialization(error.to_string()), - WebsocketError::Deserialization(error) => Self::Decode(error.to_string()), - } - } -} - #[cfg(feature = "web")] struct WebsysSocket { sender: Mutex>, @@ -1270,23 +1251,6 @@ mod native { UnexpectedStatusCode(StatusCode), } - impl From for RequestError { - fn from(value: HandshakeError) -> Self { - let string = value.to_string(); - match value { - HandshakeError::UnexpectedStatusCode(status) => { - Self::Status(string, status.as_u16()) - } - HandshakeError::UnsupportedHttpVersion(_) - | HandshakeError::MissingHeader { .. } - | HandshakeError::UnexpectedHeaderValue { .. } - | HandshakeError::ExpectedAProtocol - | HandshakeError::UnexpectedProtocol { .. } - | HandshakeError::ServerRespondedWithDifferentVersion => Self::Connect(string), - } - } - } - pub struct WebSocketResponse { pub response: Response, pub version: Version, @@ -1492,4 +1456,102 @@ mod native { u16::from(value).into() } } + + impl From for RequestError { + fn from(value: HandshakeError) -> Self { + let string = value.to_string(); + match value { + HandshakeError::UnexpectedStatusCode(status) => { + Self::Status(string, status.as_u16()) + } + HandshakeError::UnsupportedHttpVersion(_) + | HandshakeError::MissingHeader { .. } + | HandshakeError::UnexpectedHeaderValue { .. } + | HandshakeError::ExpectedAProtocol + | HandshakeError::UnexpectedProtocol { .. } + | HandshakeError::ServerRespondedWithDifferentVersion => Self::Connect(string), + } + } + } + + trait IntoRequestError { + fn into_request_error(self) -> RequestError; + } + + impl IntoRequestError for reqwest::Error { + fn into_request_error(self) -> RequestError { + const DEFAULT_STATUS_CODE: u16 = 0; + let string = self.to_string(); + if self.is_builder() { + RequestError::Builder(string) + } else if self.is_redirect() { + RequestError::Redirect(string) + } else if self.is_status() { + RequestError::Status( + string, + self.status() + .as_ref() + .map(StatusCode::as_u16) + .unwrap_or(DEFAULT_STATUS_CODE), + ) + } else if self.is_body() { + RequestError::Body(string) + } else if self.is_decode() { + RequestError::Decode(string) + } else if self.is_upgrade() { + RequestError::Connect(string) + } else { + RequestError::Request(string) + } + } + } + + impl IntoRequestError for tungstenite::Error { + fn into_request_error(self) -> RequestError { + match self { + tungstenite::Error::ConnectionClosed => { + RequestError::Connect("websocket connection closed".to_owned()) + } + tungstenite::Error::AlreadyClosed => { + RequestError::Connect("websocket already closed".to_owned()) + } + tungstenite::Error::Io(error) => RequestError::Connect(error.to_string()), + tungstenite::Error::Tls(error) => RequestError::Connect(error.to_string()), + tungstenite::Error::Capacity(error) => RequestError::Body(error.to_string()), + tungstenite::Error::Protocol(error) => RequestError::Request(error.to_string()), + tungstenite::Error::WriteBufferFull(message) => { + RequestError::Body(message.to_string()) + } + tungstenite::Error::Utf8(error) => RequestError::Decode(error), + tungstenite::Error::AttackAttempt => { + RequestError::Request("Tungstenite attack attempt detected".to_owned()) + } + tungstenite::Error::Url(error) => RequestError::Builder(error.to_string()), + tungstenite::Error::Http(response) => { + let status_code = response.status(); + RequestError::Status(format!("HTTP error: {status_code}"), status_code.as_u16()) + } + tungstenite::Error::HttpFormat(error) => RequestError::Builder(error.to_string()), + } + } + } + + impl From for RequestError { + fn from(value: WebsocketError) -> Self { + match value { + WebsocketError::ConnectionClosed { code, description } => { + Self::Connect(format!("connection closed ({code}): {description}")) + } + WebsocketError::AlreadyClosed => Self::Connect(value.to_string()), + WebsocketError::Capacity => Self::Body(value.to_string()), + WebsocketError::Unexpected => Self::Request(value.to_string()), + WebsocketError::Uninitialized => Self::Builder(value.to_string()), + WebsocketError::Handshake(error) => error.into(), + WebsocketError::Reqwest(error) => error.into_request_error(), + WebsocketError::Tungstenite(error) => error.into_request_error(), + WebsocketError::Serialization(error) => Self::Serialization(error.to_string()), + WebsocketError::Deserialization(error) => Self::Decode(error.to_string()), + } + } + } } From a33aadfe0213baadc47c81052e7583ba0f0471f2 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 23 Jan 2026 13:32:55 -0800 Subject: [PATCH 3/3] no breaking changes --- packages/fullstack/src/payloads/websocket.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/fullstack/src/payloads/websocket.rs b/packages/fullstack/src/payloads/websocket.rs index 636de75601..6a4c452106 100644 --- a/packages/fullstack/src/payloads/websocket.rs +++ b/packages/fullstack/src/payloads/websocket.rs @@ -840,6 +840,14 @@ pub enum WebsocketError { /// Error during serialization/deserialization. #[error("error during serialization/deserialization")] Serialization(Box), + + /// Error during serialization/deserialization. + #[error("serde_json error")] + Json(#[from] serde_json::Error), + + /// Error during serialization/deserialization. + #[error("ciborium error")] + Cbor(#[from] ciborium::de::Error), } #[cfg(feature = "web")] @@ -1551,6 +1559,8 @@ mod native { WebsocketError::Tungstenite(error) => error.into_request_error(), WebsocketError::Serialization(error) => Self::Serialization(error.to_string()), WebsocketError::Deserialization(error) => Self::Decode(error.to_string()), + WebsocketError::Json(error) => Self::Decode(error.to_string()), + WebsocketError::Cbor(error) => Self::Decode(error.to_string()), } } }