Skip to main content

polyoxide_ws/
bare.rs

1//! The bare tier: one connection as a `Stream` of frames (W11), with the
2//! server's close captured (W9).
3
4use std::{
5    collections::VecDeque,
6    pin::Pin,
7    task::{Context, Poll},
8};
9
10use futures_util::{SinkExt, Stream, StreamExt};
11use tokio::time::Instant;
12use tokio_tungstenite::tungstenite::Message;
13
14use crate::{request::Requests, Socket, WsError};
15
16/// Which frames a [`Connection`] yields.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Mode {
19    /// Text and Binary frames only. Pings, pongs and raw frames are read and
20    /// skipped, the server's pings still answered, and a server close ends
21    /// the stream.
22    Data,
23    /// Every frame, pings, pongs and the server's close included, for a
24    /// caller whose liveness rule counts them. A close is the last item.
25    All,
26}
27
28impl Mode {
29    /// Whether a frame other than a close is yielded in this mode.
30    fn yields(self, message: &Message) -> bool {
31        match self {
32            Self::Data => matches!(message, Message::Text(_) | Message::Binary(_)),
33            Self::All => true,
34        }
35    }
36}
37
38/// One open connection, as a `Stream` of its frames.
39///
40/// Yields `Ok` frames as [`Mode`] selects, and ends after the server's close,
41/// after the end of the stream, or after the one `Err` a failed read yields.
42/// Frames kept while a [request](Self::request) awaited its answer come out
43/// first, in the order they arrived. The server's pings are answered as the
44/// stream is read.
45///
46/// The server's close is answered: the RFC 6455 close reply is flushed before
47/// the stream yields its last item, so it goes out even when the caller never
48/// reads again (DRIFT R3). Once the stream has ended on the close,
49/// [`closed`](Self::closed) says with what code and reason, as a
50/// [`Kind::Closed`](crate::Kind::Closed) error.
51///
52/// The basis is one socket crate's bare-tier `poll_next` (`src/client.rs`)
53/// and its frame classifier, with the queue two other socket crates' former
54/// bare tiers drained first and the former close capture of one of those two
55/// (`record_close` and `closed_error`); all five socket crates read through
56/// this type since Stories 4.6 to 4.10. Two divergence rows stay with each
57/// venue: what counts as liveness (D5), which [`Mode::All`] and
58/// [`last_inbound`](Self::last_inbound) let a venue decide, and what a Binary
59/// frame means (D12): one venue decodes it as text, and the others skip it.
60pub struct Connection {
61    socket: Socket,
62    mode: Mode,
63    buffered: VecDeque<Message>,
64    last_inbound: Instant,
65    /// The server's close code and reason, once the stream ended on them.
66    close: Option<(Option<u16>, String)>,
67    /// Whether tungstenite holds a close reply not yet flushed.
68    replying: bool,
69    finished: bool,
70    pub(crate) requests: Requests,
71}
72
73impl std::fmt::Debug for Connection {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        f.debug_struct("Connection")
76            .field("mode", &self.mode)
77            .field("buffered", &self.buffered.len())
78            .field("close", &self.close)
79            .field("finished", &self.finished)
80            .finish_non_exhaustive()
81    }
82}
83
84impl Connection {
85    /// Read `socket` in `mode`.
86    pub fn new(socket: Socket, mode: Mode) -> Self {
87        Self {
88            socket,
89            mode,
90            buffered: VecDeque::new(),
91            last_inbound: Instant::now(),
92            close: None,
93            replying: false,
94            finished: false,
95            requests: Requests::default(),
96        }
97    }
98
99    /// Which frames this connection yields.
100    pub fn mode(&self) -> Mode {
101        self.mode
102    }
103
104    /// When the last frame of any kind arrived, pings, pongs and frames read
105    /// while awaiting an answer included; when the connection was made, if
106    /// none has.
107    pub fn last_inbound(&self) -> Instant {
108        self.last_inbound
109    }
110
111    /// The server's close, once the stream has ended on it: a
112    /// [`Kind::Closed`](crate::Kind::Closed) with the close frame's code and
113    /// reason, or with no code and an empty reason when the stream ended
114    /// without a close frame. A close frame without a code reads the same.
115    /// `None` while the connection is open, and after a failed read, whose
116    /// error the stream yielded.
117    pub fn closed(&self) -> Option<WsError> {
118        self.close
119            .as_ref()
120            .map(|(code, reason)| WsError::closed(*code, reason.as_str()))
121    }
122
123    /// Send one frame and flush it.
124    pub async fn send(&mut self, message: Message) -> Result<(), WsError> {
125        self.socket.send(message).await?;
126        Ok(())
127    }
128
129    /// Close the connection: send a close frame and flush it. On a
130    /// connection the server already closed, flush the reply if it has not
131    /// gone out yet; there is nothing else to send. A connection whose read
132    /// failed is already gone, so closing it does nothing.
133    pub async fn close(&mut self) -> Result<(), WsError> {
134        if self.close.is_some() {
135            self.flush_reply().await;
136            return Ok(());
137        }
138        if self.finished {
139            return Ok(());
140        }
141        // Sends the close frame and flushes it.
142        self.socket.close(None).await?;
143        Ok(())
144    }
145
146    /// Note the server's close frame, or the end of the stream without one.
147    /// tungstenite queues the reply to a close frame as it reads it, and sends
148    /// it on the next read or flush.
149    pub(crate) fn record_close(&mut self, frame: Option<&Message>) {
150        self.replying = matches!(frame, Some(Message::Close(_)));
151        self.close = Some(match frame {
152            Some(Message::Close(Some(frame))) => {
153                (Some(u16::from(frame.code)), frame.reason.to_string())
154            }
155            _ => (None, String::new()),
156        });
157        self.finished = true;
158        tracing::debug!(close = ?self.close, "the server closed the WebSocket connection");
159    }
160
161    /// Send the queued close reply, if there is one.
162    pub(crate) async fn flush_reply(&mut self) {
163        if self.replying {
164            if let Err(err) = self.socket.flush().await {
165                tracing::debug!(%err, "the close reply could not be sent");
166            }
167            self.replying = false;
168        }
169    }
170
171    /// Keep a frame for the stream to yield later, if its mode yields it.
172    pub(crate) fn keep(&mut self, message: Message) {
173        if self.mode.yields(&message) {
174            self.buffered.push_back(message);
175        }
176    }
177
178    /// The underlying socket, for the request module's reads.
179    pub(crate) fn socket(&mut self) -> &mut Socket {
180        &mut self.socket
181    }
182
183    /// Note that a frame arrived now.
184    pub(crate) fn touch(&mut self) {
185        self.last_inbound = Instant::now();
186    }
187
188    /// End the stream after a failed read.
189    pub(crate) fn fail(&mut self) {
190        self.finished = true;
191    }
192
193    /// Whether the stream has ended.
194    pub(crate) fn is_finished(&self) -> bool {
195        self.finished
196    }
197
198    /// Whether frames a request kept are waiting to be yielded.
199    pub(crate) fn has_kept(&self) -> bool {
200        !self.buffered.is_empty()
201    }
202}
203
204impl Stream for Connection {
205    type Item = Result<Message, WsError>;
206
207    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
208        let this = self.get_mut();
209        loop {
210            // RFC 6455 requires the close reply, and this stream may never be
211            // read again once it has yielded its last item.
212            if this.replying {
213                match this.socket.poll_flush_unpin(cx) {
214                    Poll::Pending => return Poll::Pending,
215                    Poll::Ready(Err(err)) => {
216                        tracing::debug!(%err, "the close reply could not be sent");
217                    }
218                    Poll::Ready(Ok(())) => {}
219                }
220                this.replying = false;
221            }
222            if let Some(kept) = this.buffered.pop_front() {
223                return Poll::Ready(Some(Ok(kept)));
224            }
225            if this.finished {
226                return Poll::Ready(None);
227            }
228            let message = match this.socket.poll_next_unpin(cx) {
229                Poll::Ready(Some(Ok(message))) => message,
230                Poll::Ready(Some(Err(err))) => {
231                    this.fail();
232                    return Poll::Ready(Some(Err(err.into())));
233                }
234                Poll::Ready(None) => {
235                    this.record_close(None);
236                    return Poll::Ready(None);
237                }
238                Poll::Pending => return Poll::Pending,
239            };
240            this.touch();
241            match message {
242                Message::Close(_) => {
243                    this.record_close(Some(&message));
244                    this.keep(message);
245                }
246                Message::Ping(_) if this.mode == Mode::All => {
247                    // tungstenite has queued the pong, and sends it on the
248                    // next read or flush; the caller may not read again soon.
249                    let _ = this.socket.poll_flush_unpin(cx);
250                    return Poll::Ready(Some(Ok(message)));
251                }
252                message if this.mode.yields(&message) => return Poll::Ready(Some(Ok(message))),
253                // Reading again is what sends the pong for a ping just read.
254                _ => continue,
255            }
256        }
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use std::time::Duration;
263
264    use tokio_tungstenite::tungstenite::protocol::{frame::coding::CloseCode, CloseFrame};
265
266    use super::*;
267    use crate::{
268        connect,
269        test_server::{Script, ScriptedServer},
270        Kind, DEFAULT_CONNECT_TIMEOUT,
271    };
272
273    const WINDOW: Duration = Duration::from_secs(3);
274
275    async fn open(server: &ScriptedServer, mode: Mode) -> Connection {
276        let socket = connect(server.url.as_str(), DEFAULT_CONNECT_TIMEOUT)
277            .await
278            .expect("connect");
279        Connection::new(socket, mode)
280    }
281
282    async fn next(connection: &mut Connection) -> Option<Result<Message, WsError>> {
283        tokio::time::timeout(WINDOW, connection.next())
284            .await
285            .expect("an item within the window")
286    }
287
288    fn frames() -> Vec<Message> {
289        vec![
290            Message::Ping(b"p".to_vec().into()),
291            Message::Pong(b"q".to_vec().into()),
292            Message::Binary(vec![1, 2, 3].into()),
293            Message::Text("data".into()),
294        ]
295    }
296
297    #[tokio::test]
298    async fn data_mode_yields_text_and_binary_and_skips_control_frames() {
299        let server = ScriptedServer::start(vec![Script::messages(frames())]).await;
300        let mut connection = open(&server, Mode::Data).await;
301        assert_eq!(
302            next(&mut connection).await.unwrap().unwrap(),
303            Message::Binary(vec![1, 2, 3].into())
304        );
305        assert_eq!(
306            next(&mut connection).await.unwrap().unwrap(),
307            Message::Text("data".into())
308        );
309    }
310
311    #[tokio::test]
312    async fn all_mode_yields_every_frame() {
313        let server = ScriptedServer::start(vec![Script::messages(frames())]).await;
314        let mut connection = open(&server, Mode::All).await;
315        let mut seen = Vec::new();
316        for _ in 0..4 {
317            seen.push(next(&mut connection).await.unwrap().unwrap());
318        }
319        assert_eq!(seen, frames());
320    }
321
322    #[tokio::test]
323    async fn a_server_ping_is_answered_in_either_mode() {
324        // Data mode answers by reading on; all mode yields the ping and still
325        // sends the pong, though the caller never reads again.
326        for mode in [Mode::Data, Mode::All] {
327            let server = ScriptedServer::start(vec![Script::messages(vec![Message::Ping(
328                b"p1".to_vec().into(),
329            )])])
330            .await;
331            let mut connection = open(&server, mode).await;
332            let reader = match mode {
333                Mode::Data => {
334                    tokio::spawn(async move { while connection.next().await.is_some() {} })
335                }
336                Mode::All => {
337                    assert_eq!(
338                        next(&mut connection).await.unwrap().unwrap(),
339                        Message::Ping(b"p1".to_vec().into())
340                    );
341                    // Held, never read again.
342                    tokio::spawn(async move {
343                        let _held = connection;
344                        std::future::pending::<()>().await;
345                    })
346                }
347            };
348            server
349                .wait_for("a pong", |s| s.pongs() == [b"p1".to_vec()])
350                .await;
351            reader.abort();
352        }
353    }
354
355    #[tokio::test]
356    async fn a_server_close_ends_the_stream_with_its_code_and_reason() {
357        for (code, reason) in [(1011, "overloaded"), (1008, "policy")] {
358            let server =
359                ScriptedServer::start(vec![Script::close_at_once().close_with(code, reason)]).await;
360            let mut connection = open(&server, Mode::Data).await;
361            assert!(connection.closed().is_none(), "open, so not closed yet");
362            assert!(next(&mut connection).await.is_none());
363            assert_eq!(
364                connection.closed().expect("closed").kind(),
365                Kind::Closed {
366                    code: Some(code),
367                    reason
368                }
369            );
370            assert!(next(&mut connection).await.is_none(), "it stays ended");
371        }
372    }
373
374    #[tokio::test]
375    async fn all_mode_yields_the_close_frame_last() {
376        let server =
377            ScriptedServer::start(vec![Script::close_at_once().close_with(1001, "bye")]).await;
378        let mut connection = open(&server, Mode::All).await;
379        assert_eq!(
380            next(&mut connection).await.unwrap().unwrap(),
381            Message::Close(Some(CloseFrame {
382                code: CloseCode::Away,
383                reason: "bye".into()
384            }))
385        );
386        assert!(next(&mut connection).await.is_none());
387        assert_eq!(
388            connection.closed().unwrap().kind(),
389            Kind::Closed {
390                code: Some(1001),
391                reason: "bye"
392            }
393        );
394    }
395
396    #[tokio::test]
397    async fn a_server_close_is_answered_though_the_stream_is_never_read_again() {
398        // RFC 6455's close reply (DRIFT R3). tungstenite queues it as it reads
399        // the close and sends it only on a later read or flush, which a
400        // stream that has ended never makes.
401        for mode in [Mode::Data, Mode::All] {
402            let server = ScriptedServer::start(vec![
403                Script::close_at_once().close_with(1011, "pong_timeout")
404            ])
405            .await;
406            let mut connection = open(&server, mode).await;
407            let last = next(&mut connection).await;
408            match mode {
409                Mode::Data => assert!(last.is_none(), "{last:?}"),
410                Mode::All => assert!(matches!(last, Some(Ok(Message::Close(_)))), "{last:?}"),
411            }
412            assert_eq!(
413                connection.closed().unwrap().kind(),
414                Kind::Closed {
415                    code: Some(1011),
416                    reason: "pong_timeout"
417                }
418            );
419            // Held open and never read again, as a caller that saw the end
420            // would leave it.
421            server
422                .wait_for("the close reply", |s| s.close_reply_count() == 1)
423                .await;
424            drop(connection);
425        }
426    }
427
428    #[tokio::test]
429    async fn close_after_the_server_closed_succeeds() {
430        let server = ScriptedServer::start(vec![Script::close_at_once()]).await;
431        let mut connection = open(&server, Mode::Data).await;
432        assert!(next(&mut connection).await.is_none());
433        connection.close().await.expect("close");
434        server
435            .wait_for("the close reply", |s| s.close_reply_count() == 1)
436            .await;
437    }
438
439    #[tokio::test]
440    async fn a_failed_read_is_yielded_once_and_ends_the_stream() {
441        // The server drops the TCP connection without a closing handshake.
442        let server = ScriptedServer::start(vec![Script::hold().then_cut()]).await;
443        let mut connection = open(&server, Mode::Data).await;
444        let err = next(&mut connection).await.unwrap().unwrap_err();
445        assert!(matches!(err.kind(), Kind::Protocol | Kind::Io), "{err:?}");
446        assert!(next(&mut connection).await.is_none());
447        assert!(connection.closed().is_none(), "an error is not a close");
448    }
449
450    #[tokio::test]
451    async fn close_after_a_failed_read_does_nothing() {
452        let server = ScriptedServer::start(vec![Script::hold().then_cut()]).await;
453        let mut connection = open(&server, Mode::Data).await;
454        assert!(next(&mut connection).await.unwrap().is_err());
455        connection
456            .close()
457            .await
458            .expect("closing a connection that is already gone succeeds");
459    }
460
461    #[tokio::test]
462    async fn close_sends_a_close_frame_the_server_receives() {
463        let server = ScriptedServer::start(vec![Script::hold()]).await;
464        let mut connection = open(&server, Mode::Data).await;
465        connection.close().await.expect("close");
466        server
467            .wait_for("a close frame", |s| s.close_count() == 1)
468            .await;
469    }
470
471    #[tokio::test]
472    async fn every_frame_refreshes_last_inbound() {
473        let server = ScriptedServer::start(vec![Script::messages(vec![Message::Pong(
474            Vec::new().into(),
475        )])])
476        .await;
477        let mut connection = open(&server, Mode::Data).await;
478        let opened = connection.last_inbound();
479        // The pong is skipped, so the read waits; it must still count.
480        let _ = tokio::time::timeout(Duration::from_millis(200), connection.next()).await;
481        assert!(connection.last_inbound() > opened);
482    }
483
484    #[test]
485    fn a_connection_can_move_between_tasks() {
486        fn assert_send_unpin<T: Send + Unpin>() {}
487        assert_send_unpin::<Connection>();
488    }
489}