Skip to main content

rustlavel_http/
sse.rs

1//! Server-Sent Events: a response that stays open and delivers events as they
2//! happen.
3//!
4//! Lighter than a WebSocket for the case it fits — the server talks, the
5//! browser listens — and the browser's `EventSource` reconnects on its own,
6//! sending `Last-Event-ID` so a handler can resume where the client left off.
7//! Progress of a background job is the textbook case: one direction, a few
8//! events a second at most, and a client that must not miss the last one.
9//!
10//! ```ignore
11//! r.get("/jobs/{id}/events", |req: Request| async move {
12//!     let (tx, rx) = sse::channel(16);
13//!     tokio::spawn(async move {
14//!         for step in 0..=100 {
15//!             if tx.send(Event::json("progress", Json::object([("percent", step.into())]))).await.is_err() {
16//!                 break; // the client went away
17//!             }
18//!         }
19//!     });
20//!     Response::events(rx)
21//! });
22//! ```
23//!
24//! # What the wire looks like
25//!
26//! One event is a few `field: value` lines and a blank line. `data:` may
27//! repeat, one line each — a newline inside the data would otherwise end the
28//! event early, so it is split for you. A comment line (`: …`) is sent every
29//! fifteen seconds while nothing else is: a proxy that sees no bytes for a
30//! minute closes the connection, and the client would then reconnect for no
31//! reason.
32
33use crate::response::Response;
34use crate::upgrade::Upgraded;
35use rustlavel_core::Json;
36use std::time::Duration;
37use tokio::io::AsyncWriteExt;
38use tokio::sync::mpsc;
39
40/// How often a comment is written to keep an idle connection open.
41pub const KEEPALIVE: Duration = Duration::from_secs(15);
42
43/// One event.
44#[derive(Debug, Clone, PartialEq)]
45pub struct Event {
46    /// Sent as `id:`. The browser remembers the last one and presents it as
47    /// `Last-Event-ID` when it reconnects.
48    pub id: Option<String>,
49    /// Sent as `event:`. `EventSource` dispatches it by this name; without
50    /// one it is a plain `message`.
51    pub event: Option<String>,
52    pub data: String,
53    /// Sent as `retry:`, in milliseconds — how long the browser waits before
54    /// reconnecting after this connection ends.
55    pub retry: Option<Duration>,
56}
57
58impl Event {
59    /// A plain `message` event.
60    pub fn data(data: impl Into<String>) -> Event {
61        Event { id: None, event: None, data: data.into(), retry: None }
62    }
63
64    /// A named event.
65    pub fn named(event: impl Into<String>, data: impl Into<String>) -> Event {
66        Event { id: None, event: Some(event.into()), data: data.into(), retry: None }
67    }
68
69    /// A named event carrying JSON, which is what a UI usually wants.
70    pub fn json(event: impl Into<String>, data: Json) -> Event {
71        Event::named(event, data.to_string())
72    }
73
74    pub fn id(mut self, id: impl Into<String>) -> Event {
75        self.id = Some(id.into());
76        self
77    }
78
79    pub fn retry(mut self, after: Duration) -> Event {
80        self.retry = Some(after);
81        self
82    }
83
84    /// The bytes on the wire.
85    pub fn to_bytes(&self) -> Vec<u8> {
86        let mut out = String::new();
87        if let Some(id) = &self.id {
88            out.push_str(&format!("id: {}\n", strip_newlines(id)));
89        }
90        if let Some(event) = &self.event {
91            out.push_str(&format!("event: {}\n", strip_newlines(event)));
92        }
93        if let Some(retry) = self.retry {
94            out.push_str(&format!("retry: {}\n", retry.as_millis()));
95        }
96        // One `data:` line per line of data. A newline left inside would end
97        // the event where the sender did not mean it to.
98        for line in self.data.split('\n') {
99            out.push_str("data: ");
100            out.push_str(line.trim_end_matches('\r'));
101            out.push('\n');
102        }
103        out.push('\n');
104        out.into_bytes()
105    }
106}
107
108/// A field value may not contain a line break: it would start a new field.
109fn strip_newlines(text: &str) -> String {
110    text.replace(['\r', '\n'], " ")
111}
112
113/// The sending half of an event stream, and the receiver `Response::events`
114/// takes. `capacity` is how many events may wait for a slow client before the
115/// sender is made to wait too.
116pub fn channel(capacity: usize) -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) {
117    mpsc::channel(capacity.max(1))
118}
119
120impl Response {
121    /// A `200` that stays open and writes each event from `events` as it
122    /// arrives, until the sender is dropped or the client goes away.
123    ///
124    /// Drop the sender to end the stream cleanly. A send that fails means the
125    /// client has gone; stop producing.
126    pub fn events(events: mpsc::Receiver<Event>) -> Response {
127        let events = std::sync::Mutex::new(Some(events));
128        Response::ok()
129            .with_header("content-type", "text/event-stream")
130            .with_header("cache-control", "no-cache")
131            // The body ends when the connection does. Said so, rather than
132            // letting a client wait for a length that is never coming.
133            .with_header("connection", "close")
134            // Nginx buffers responses by default, which turns a live stream
135            // into one delivered when it ends. This header asks it not to.
136            .with_header("x-accel-buffering", "no")
137            .streaming(move |connection: Upgraded| {
138                let events = events.lock().ok().and_then(|mut held| held.take());
139                async move {
140                    if let Some(events) = events {
141                        pump(connection, events).await;
142                    }
143                }
144            })
145    }
146}
147
148/// Write events until the source ends or the client leaves.
149async fn pump(mut connection: Upgraded, mut events: mpsc::Receiver<Event>) {
150    let mut keepalive = tokio::time::interval(KEEPALIVE);
151    keepalive.tick().await; // the first tick is immediate; skip it
152
153    loop {
154        let bytes = tokio::select! {
155            event = events.recv() => match event {
156                Some(event) => event.to_bytes(),
157                // Every sender dropped: the stream is over.
158                None => break,
159            },
160            _ = keepalive.tick() => b": keepalive\n\n".to_vec(),
161        };
162
163        // A write that fails is the client gone. Not an error to log — a
164        // browser tab closing is the ordinary end of an event stream.
165        if connection.writer.write_all(&bytes).await.is_err() {
166            break;
167        }
168        if connection.writer.flush().await.is_err() {
169            break;
170        }
171    }
172    let _ = connection.writer.shutdown().await;
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn an_event_is_written_in_the_shape_event_source_reads() {
181        let bytes = Event::named("progress", "42").id("7").retry(Duration::from_secs(2)).to_bytes();
182        assert_eq!(
183            String::from_utf8(bytes).unwrap(),
184            "id: 7\nevent: progress\nretry: 2000\ndata: 42\n\n"
185        );
186        assert_eq!(String::from_utf8(Event::data("hello").to_bytes()).unwrap(), "data: hello\n\n");
187    }
188
189    /// A newline inside the data would end the event where the sender did not
190    /// mean it to, and everything after it would be read as the next event.
191    #[test]
192    fn multi_line_data_is_split_across_data_lines() {
193        let text = String::from_utf8(Event::data("line one\nline two\r\nline three").to_bytes()).unwrap();
194        assert_eq!(text, "data: line one\ndata: line two\ndata: line three\n\n");
195    }
196
197    /// A newline in an id or a name would start a new field.
198    #[test]
199    fn a_line_break_cannot_be_smuggled_into_a_field() {
200        let text = String::from_utf8(Event::named("a\nevent: b", "x").id("1\n2").to_bytes()).unwrap();
201        // One `event:` line and one `id:` line, whatever the values held.
202        assert_eq!(text.lines().filter(|l| l.starts_with("event:")).count(), 1, "{text}");
203        assert_eq!(text.lines().filter(|l| l.starts_with("id:")).count(), 1, "{text}");
204        assert!(text.starts_with("id: 1 2\nevent: a event: b\n"), "{text}");
205    }
206
207    #[test]
208    fn json_events_carry_the_document_on_one_line() {
209        let text = String::from_utf8(
210            Event::json("progress", Json::object([("percent", Json::from(50))])).to_bytes(),
211        )
212        .unwrap();
213        assert_eq!(text, "event: progress\ndata: {\"percent\":50}\n\n");
214    }
215
216    /// Over a real socket: the client must see each event as it is sent, not
217    /// when the stream ends, and the connection must close when the sender is
218    /// dropped. A stream that buffered until the end would pass every test
219    /// above and be useless for the one thing it is for.
220    #[tokio::test]
221    async fn events_arrive_as_they_are_sent_and_the_stream_ends_with_the_sender() {
222        use crate::{Request, Router, Server};
223        use rustlavel_core::Context;
224        use tokio::io::AsyncReadExt;
225        use tokio::net::{TcpListener, TcpStream};
226
227        // A handler that hands the sender to a channel the test controls, so
228        // the test decides when each event goes out.
229        let (hand_over, mut take) = mpsc::channel::<mpsc::Sender<Event>>(1);
230        let hand_over = std::sync::Arc::new(hand_over);
231        let mut router = Router::new();
232        router.get("/events", move |_req: Request| {
233            let hand_over = std::sync::Arc::clone(&hand_over);
234            async move {
235                let (tx, rx) = channel(8);
236                hand_over.try_send(tx).expect("the test is waiting for the sender");
237                Response::events(rx)
238            }
239        });
240        let server = std::sync::Arc::new(Server::new(router, Context::default()));
241
242        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
243        let addr = listener.local_addr().unwrap();
244        tokio::spawn(async move {
245            let (stream, peer) = listener.accept().await.unwrap();
246            let _ = server.serve_connection(stream, peer).await;
247        });
248
249        let mut client = TcpStream::connect(addr).await.unwrap();
250        client.write_all(b"GET /events HTTP/1.1\r\nHost: t\r\n\r\n").await.unwrap();
251
252        // The head arrives before any event exists.
253        let mut head = vec![0u8; 512];
254        let n = client.read(&mut head).await.unwrap();
255        let head = String::from_utf8_lossy(&head[..n]).to_string();
256        assert!(head.starts_with("HTTP/1.1 200 OK"), "{head}");
257        assert!(head.contains("text/event-stream"), "{head}");
258        assert!(!head.to_ascii_lowercase().contains("content-length"), "{head}");
259
260        let tx = take.recv().await.expect("the handler ran");
261
262        // Each event is readable on its own, before the next is sent.
263        for step in [10, 50, 100] {
264            tx.send(Event::json("progress", Json::object([("percent", Json::from(step))]))).await.unwrap();
265            let mut chunk = vec![0u8; 256];
266            let n = tokio::time::timeout(Duration::from_secs(2), client.read(&mut chunk))
267                .await
268                .expect("an event did not arrive within two seconds — the stream is buffering")
269                .unwrap();
270            let text = String::from_utf8_lossy(&chunk[..n]).to_string();
271            assert!(text.contains(&format!("\"percent\":{step}")), "step {step}: {text}");
272        }
273
274        // Dropping the sender ends the stream: the client reads EOF.
275        drop(tx);
276        let mut rest = Vec::new();
277        tokio::time::timeout(Duration::from_secs(2), client.read_to_end(&mut rest))
278            .await
279            .expect("the connection stayed open after the sender was dropped")
280            .unwrap();
281    }
282
283    /// The headers are what make a browser treat it as a stream, and the one
284    /// that must be absent is a content length.
285    #[test]
286    fn the_response_is_a_stream_with_no_length() {
287        let (_tx, rx) = channel(4);
288        let response = Response::events(rx);
289        let head = String::from_utf8(response.to_bytes(false)).unwrap();
290
291        assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "{head}");
292        assert!(head.contains("content-type: text/event-stream\r\n"), "{head}");
293        assert!(head.contains("cache-control: no-cache\r\n"), "{head}");
294        assert!(!head.to_ascii_lowercase().contains("content-length"), "a length would end the stream: {head}");
295        assert!(response.upgrades(), "the socket is not handed over");
296    }
297}