Skip to main content

ullun/
sse.rs

1//! Server-Sent Events
2//!
3//! ```
4//! use ullun::prelude::*;
5//! use ullun::sse::{sse, Event};
6//!
7//! async fn ticker(_req: Request) -> Result<Response> {
8//!     let (tx, res) = sse();
9//!     tokio::spawn(async move {
10//!         for n in 1..=3 {
11//!             let event = Event::data(format!("tick {n}")).event("tick").id(n.to_string());
12//!             if tx.send(event).await.is_err() {
13//!                 return; // browser closed the tab
14//!             }
15//!             tokio::time::sleep(std::time::Duration::from_secs(1)).await;
16//!         }
17//!     });
18//!     Ok(res)
19//! }
20//! ```
21//!
22//! Browser side: `new EventSource("/ticker").addEventListener("tick", e => ...)`.
23//! Newlines in field values cannot inject extra fields: `event` and `id` have
24//! them removed, `data` is split into one `data:` line per line.
25
26use crate::error::{Error, Result};
27use crate::response::Response;
28use crate::stream::{BodySender, Disconnected};
29use serde::Serialize;
30
31/// One server-sent event
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct Event {
34    event: Option<String>,
35    id: Option<String>,
36    retry_ms: Option<u64>,
37    data: Option<String>,
38    comment: Option<String>,
39}
40
41impl Event {
42    /// An event carrying `data`
43    pub fn data(data: impl Into<String>) -> Self {
44        Self {
45            data: Some(data.into()),
46            ..Self::default()
47        }
48    }
49
50    /// An event whose data is `value` serialized as JSON
51    pub fn json<T: Serialize>(value: &T) -> Result<Self> {
52        let data = serde_json::to_string(value)
53            .map_err(|e| Error::internal(format!("SSE JSON: {}", e)))?;
54        Ok(Self::data(data))
55    }
56
57    /// A comment line, ignored by `EventSource`; useful as a keep-alive
58    pub fn comment(text: impl Into<String>) -> Self {
59        Self {
60            comment: Some(text.into()),
61            ..Self::default()
62        }
63    }
64
65    /// Event name, delivered to `addEventListener(name, ...)`
66    pub fn event(mut self, name: impl Into<String>) -> Self {
67        self.event = Some(name.into());
68        self
69    }
70
71    /// Event id; the browser sends the last one back as `Last-Event-ID` on reconnect
72    pub fn id(mut self, id: impl Into<String>) -> Self {
73        self.id = Some(id.into());
74        self
75    }
76
77    /// Reconnect delay hint in milliseconds
78    pub fn retry(mut self, millis: u64) -> Self {
79        self.retry_ms = Some(millis);
80        self
81    }
82
83    /// Wire format, terminated by a blank line
84    pub fn encode(&self) -> String {
85        fn single_line(s: &str) -> String {
86            s.chars().filter(|c| *c != '\n' && *c != '\r').collect()
87        }
88        let mut out = String::new();
89        if let Some(c) = &self.comment {
90            for line in c.lines() {
91                out.push_str(": ");
92                out.push_str(line);
93                out.push('\n');
94            }
95        }
96        if let Some(e) = &self.event {
97            out.push_str(&format!("event: {}\n", single_line(e)));
98        }
99        if let Some(id) = &self.id {
100            out.push_str(&format!("id: {}\n", single_line(id)));
101        }
102        if let Some(ms) = self.retry_ms {
103            out.push_str(&format!("retry: {}\n", ms));
104        }
105        if let Some(d) = &self.data {
106            // `lines()` splits on \n and \r\n; an empty payload is one empty line
107            let mut any = false;
108            for line in d.split('\n') {
109                any = true;
110                out.push_str("data: ");
111                out.push_str(line.trim_end_matches('\r'));
112                out.push('\n');
113            }
114            debug_assert!(any);
115        }
116        out.push('\n');
117        out
118    }
119}
120
121/// Sends events of an SSE response
122#[derive(Clone)]
123pub struct SseSender {
124    inner: BodySender,
125}
126
127impl SseSender {
128    pub async fn send(&self, event: Event) -> std::result::Result<(), Disconnected> {
129        self.inner.send(event.encode()).await
130    }
131
132    pub fn is_closed(&self) -> bool {
133        self.inner.is_closed()
134    }
135
136    /// Completes when the browser has disconnected
137    pub async fn closed(&self) {
138        self.inner.closed().await
139    }
140}
141
142/// An SSE response with the right headers (`text/event-stream`, no caching,
143/// no proxy buffering) and the sender for its events
144pub fn sse() -> (SseSender, Response) {
145    let (tx, res) = Response::stream();
146    let res = res
147        .with_header("content-type", "text/event-stream; charset=utf-8")
148        .with_header("cache-control", "no-cache")
149        .with_header("x-accel-buffering", "no");
150    (SseSender { inner: tx }, res)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn encodes_fields_in_order_and_ends_with_a_blank_line() {
159        let e = Event::data("hello").event("greeting").id("7").retry(1500);
160        assert_eq!(
161            e.encode(),
162            "event: greeting\nid: 7\nretry: 1500\ndata: hello\n\n"
163        );
164    }
165
166    #[test]
167    fn multiline_data_becomes_multiple_data_lines() {
168        assert_eq!(
169            Event::data("a\nb\r\nc").encode(),
170            "data: a\ndata: b\ndata: c\n\n"
171        );
172        assert_eq!(Event::data("").encode(), "data: \n\n");
173        assert_eq!(
174            Event::data("trailing\n").encode(),
175            "data: trailing\ndata: \n\n"
176        );
177    }
178
179    #[test]
180    fn newlines_cannot_inject_fields() {
181        let e = Event::data("x").event("a\nretry: 1").id("1\r\ndata: evil");
182        let wire = e.encode();
183        assert_eq!(wire, "event: aretry: 1\nid: 1data: evil\ndata: x\n\n");
184        assert_eq!(wire.matches("\n\n").count(), 1);
185    }
186
187    #[test]
188    fn comments_and_json() {
189        assert_eq!(Event::comment("keep-alive").encode(), ": keep-alive\n\n");
190        assert_eq!(Event::comment("a\nb").encode(), ": a\n: b\n\n");
191        let e = Event::json(&serde_json::json!({ "up": true })).unwrap();
192        assert_eq!(e.encode(), "data: {\"up\":true}\n\n");
193    }
194}