Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions src/asynch/clients/websocket/_std.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::asynch::clients::SingleExecutorMutex;
use crate::models::requests::{Request, XRPLRequest};
use crate::models::results::XRPLResponse;

use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use core::marker::PhantomData;
Expand All @@ -23,6 +24,15 @@ use tokio_tungstenite::connect_async as tokio_tungstenite_connect_async;

type TokioTungsteniteMaybeTlsStream = WebSocketStream<MaybeTlsStream<TcpStream>>;

/// Render the reason carried by a WebSocket Close frame so it is preserved in
/// the surfaced error instead of being discarded.
fn close_reason(frame: Option<&tungstenite::protocol::CloseFrame<'_>>) -> String {
match frame {
Some(frame) => format!("code {}: {}", u16::from(frame.code), frame.reason),
None => "no close reason provided".to_string(),
}
}

pub struct AsyncWebSocketClient<M = SingleExecutorMutex, Status = WebSocketClosed>
where
M: RawMutex,
Expand Down Expand Up @@ -111,8 +121,14 @@ where
};
Poll::Ready(Some(Ok(response_string)))
}
tungstenite::Message::Close(_) => {
Poll::Ready(Some(Err(XRPLWebSocketException::Disconnected.into())))
tungstenite::Message::Close(frame) => {
// tokio-tungstenite queues the Close echo internally;
// preserve the peer's reason in the surfaced error
// instead of discarding it.
let reason = close_reason(frame.as_ref());
Poll::Ready(Some(Err(
XRPLWebSocketException::ConnectionClosed(reason).into()
)))
}
_ => Poll::Ready(Some(Err(
XRPLWebSocketException::UnexpectedMessageType.into()
Expand Down Expand Up @@ -255,8 +271,12 @@ where
Err(error) => return Err(error.into()),
}
}
Some(Ok(tungstenite::Message::Close(_))) => {
return Err(XRPLWebSocketException::Disconnected.into());
Some(Ok(tungstenite::Message::Close(frame))) => {
let reason = close_reason(frame.as_ref());
// Reply with a Close frame to complete the RFC 6455 closing
// handshake before surfacing the error.
let _ = websocket.send(tungstenite::Message::Close(None)).await;
return Err(XRPLWebSocketException::ConnectionClosed(reason).into());
}
Some(Ok(_)) => {
return Err(XRPLWebSocketException::UnexpectedMessageType.into());
Expand All @@ -267,3 +287,24 @@ where
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use tungstenite::protocol::frame::coding::CloseCode;
use tungstenite::protocol::CloseFrame;

#[test]
fn close_reason_preserves_peer_detail() {
let frame = CloseFrame {
code: CloseCode::Normal,
reason: "going away".into(),
};
assert_eq!(close_reason(Some(&frame)), "code 1000: going away");
}

#[test]
fn close_reason_handles_missing_frame() {
assert_eq!(close_reason(None), "no close reason provided");
}
}
2 changes: 2 additions & 0 deletions src/asynch/clients/websocket/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub enum XRPLWebSocketException {
WebSocket(embedded_websocket_embedded_io::Error),
#[error("Disconnected")]
Disconnected,
#[error("Connection closed by peer: {0}")]
ConnectionClosed(String),
#[error("Read buffer is too small (size: {0:?})")]
RxBufferTooSmall(usize),
#[error("Unexpected message type")]
Expand Down