Skip to main content

rust_web_server/sse/
mod.rs

1//! Server-Sent Events (SSE).
2//!
3//! [`Sse::stream`] answers with a `text/event-stream` response at once and
4//! returns an [`SseSender`]: events sent through it reach the client as they
5//! are sent — AI token output, progress, live updates — over HTTP/1.1,
6//! HTTP/2 and HTTP/3 alike. The stream ends when every sender is dropped; a
7//! client that went away shows up as [`SseClosed`] on the next send (a
8//! keep-alive comment every 15 seconds finds it even when nothing is sent).
9//!
10//! [`Sse`] instead assembles a complete, buffered body from a known sequence
11//! of [`SseEvent`]s.
12//!
13//! # Wire format
14//!
15//! Each event is a block of `field: value\n` lines terminated by a blank line:
16//!
17//! ```text
18//! id: 1
19//! event: update
20//! data: {"count":42}
21//!
22//! ```
23//!
24//! Lines with no field name (starting with `:`) are comments and are ignored
25//! by clients; they are used here as keep-alive pings.
26//!
27//! # Streaming
28//!
29//! ```rust,no_run
30//! use rust_web_server::app::App;
31//! use rust_web_server::sse::{Sse, SseEvent};
32//!
33//! let app = App::with_state(()).get("/ticks", |_req, _params, _conn, _state| {
34//!     let (events, response) = Sse::stream();
35//!     std::thread::spawn(move || {
36//!         for n in 1.. {
37//!             let event = SseEvent::data(n.to_string()).id(n.to_string());
38//!             if events.send(event).is_err() {
39//!                 break; // the client went away
40//!             }
41//!             std::thread::sleep(std::time::Duration::from_secs(1));
42//!         }
43//!     });
44//!     response
45//! });
46//! ```
47//!
48//! # Buffered
49//!
50//! ```rust,no_run
51//! use rust_web_server::sse::{Sse, SseEvent};
52//!
53//! let response = Sse::new()
54//!     .event("connected", "ready")
55//!     .push(SseEvent::data(r#"{"count":1}"#).id("1").event_type("update"))
56//!     .push(SseEvent::data(r#"{"count":2}"#).id("2").event_type("update"))
57//!     .comment("keep-alive")
58//!     .into_response();
59//! ```
60
61#[cfg(test)]
62mod tests;
63
64use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TrySendError};
65use std::time::Duration;
66
67use crate::core::New;
68use crate::header::Header;
69use crate::range::Range;
70use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
71
72// ── SseEvent ─────────────────────────────────────────────────────────────────
73
74/// A single Server-Sent Event.
75///
76/// Build with [`SseEvent::data`] and chain the optional setter methods.
77/// Use [`Sse::push`] to add it to a response.
78pub struct SseEvent {
79    id: Option<String>,
80    event: Option<String>,
81    data: String,
82    retry_ms: Option<u32>,
83}
84
85impl SseEvent {
86    /// Create an event with the given data payload. Multi-line strings produce
87    /// multiple `data:` lines, which the client concatenates with `\n`.
88    pub fn data(data: impl Into<String>) -> Self {
89        SseEvent { id: None, event: None, data: data.into(), retry_ms: None }
90    }
91
92    /// Set the `id` field. Clients use this to resume from the last event
93    /// seen after a reconnection (`Last-Event-ID` request header).
94    pub fn id(mut self, id: impl Into<String>) -> Self {
95        self.id = Some(id.into());
96        self
97    }
98
99    /// Set the event type (`event` field). Clients listen for specific types
100    /// with `addEventListener("type", handler)`. Defaults to `"message"`.
101    pub fn event_type(mut self, event: impl Into<String>) -> Self {
102        self.event = Some(event.into());
103        self
104    }
105
106    /// Set the `retry` reconnection delay in milliseconds. Overrides the
107    /// client's default reconnect interval.
108    pub fn retry(mut self, ms: u32) -> Self {
109        self.retry_ms = Some(ms);
110        self
111    }
112
113    /// Encode this event as SSE wire-format bytes. `data` is split into
114    /// one `data:` line per line (at `\n`, `\r\n` or a lone `\r`, all line
115    /// ends to the client); line breaks in `id` and the event type are
116    /// dropped, so neither can add fields of its own.
117    pub fn encode(&self) -> Vec<u8> {
118        let one_line = |value: &str| value.replace(['\r', '\n'], "");
119        let mut out = String::new();
120        if let Some(id) = &self.id {
121            out.push_str("id: ");
122            out.push_str(&one_line(id));
123            out.push('\n');
124        }
125        if let Some(event) = &self.event {
126            out.push_str("event: ");
127            out.push_str(&one_line(event));
128            out.push('\n');
129        }
130        for line in self.data.replace("\r\n", "\n").split(['\n', '\r']) {
131            out.push_str("data: ");
132            out.push_str(line);
133            out.push('\n');
134        }
135        if let Some(ms) = self.retry_ms {
136            out.push_str("retry: ");
137            out.push_str(&ms.to_string());
138            out.push('\n');
139        }
140        out.push('\n');
141        out.into_bytes()
142    }
143}
144
145// ── Sse ──────────────────────────────────────────────────────────────────────
146
147/// Builder for a buffered Server-Sent Events response.
148///
149/// Call [`Sse::into_response`] to obtain a [`Response`] with:
150/// - `200 OK`
151/// - `Content-Type: text/event-stream`
152/// - `Cache-Control: no-cache`
153/// - `X-Accel-Buffering: no` (disables nginx proxy buffering)
154///
155/// # Example
156///
157/// ```rust,no_run
158/// use rust_web_server::sse::Sse;
159///
160/// let response = Sse::new()
161///     .event("open", "")
162///     .data(r#"{"msg":"hello"}"#)
163///     .retry(5000)
164///     .into_response();
165/// ```
166pub struct Sse {
167    chunks: Vec<Vec<u8>>,
168}
169
170impl Sse {
171    /// Create an empty SSE response builder.
172    pub fn new() -> Self {
173        Sse { chunks: Vec::new() }
174    }
175
176    /// Append a named event with `data`.
177    ///
178    /// `event_type` becomes the `event:` field; `data` becomes the `data:` field.
179    pub fn event(mut self, event_type: &str, data: &str) -> Self {
180        self.chunks.push(
181            SseEvent::data(data).event_type(event_type).encode(),
182        );
183        self
184    }
185
186    /// Append a data-only event (no `event:` type field; clients receive it as
187    /// `"message"`).
188    pub fn data(mut self, data: &str) -> Self {
189        self.chunks.push(SseEvent::data(data).encode());
190        self
191    }
192
193    /// Append a fully configured [`SseEvent`] (id, type, retry, data).
194    pub fn push(mut self, event: SseEvent) -> Self {
195        self.chunks.push(event.encode());
196        self
197    }
198
199    /// Append a `retry:` directive that tells the client how many milliseconds
200    /// to wait before reconnecting after the connection is lost.
201    pub fn retry(mut self, ms: u32) -> Self {
202        self.chunks.push(format!("retry: {}\n\n", ms).into_bytes());
203        self
204    }
205
206    /// Append an SSE comment line (starts with `:`). Used as a keep-alive ping
207    /// or annotation; browsers ignore comment content.
208    pub fn comment(mut self, text: &str) -> Self {
209        self.chunks.push(comment_bytes(text));
210        self
211    }
212
213    /// A streaming response and the [`SseSender`] that feeds it: 32 events
214    /// buffered, a keep-alive comment after 15 quiet seconds. See
215    /// [`SseStream`] to change either.
216    pub fn stream() -> (SseSender, Response) {
217        SseStream::new().start()
218    }
219
220    /// Finalise the builder and return a [`Response`] with the correct SSE
221    /// headers and the accumulated event body.
222    pub fn into_response(self) -> Response {
223        let body: Vec<u8> = self.chunks.into_iter().flatten().collect();
224
225        let mut response = Response::new();
226        response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
227        response.reason_phrase =
228            STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
229        response.headers.push(Header {
230            name: Header::_CACHE_CONTROL.into(),
231            value: "no-cache".into(),
232        });
233        response.headers.push(Header {
234            name: "X-Accel-Buffering".into(),
235            value: "no".into(),
236        });
237        response.content_range_list = vec![Range::get_content_range(
238            body,
239            "text/event-stream".to_string(),
240        )];
241        response
242    }
243}
244
245/// `: text` — one comment line per line of `text`.
246fn comment_bytes(text: &str) -> Vec<u8> {
247    let mut out = String::new();
248    for line in text.replace("\r\n", "\n").split(['\n', '\r']) {
249        out.push_str(": ");
250        out.push_str(line);
251        out.push('\n');
252    }
253    out.push('\n');
254    out.into_bytes()
255}
256
257/// The `text/event-stream` response headers: no caching, no proxy buffering.
258fn stream_response(body: Option<Box<dyn std::io::Read + Send>>) -> Response {
259    let mut response = Response::new();
260    response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
261    response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
262    response.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "text/event-stream".into() });
263    response.headers.push(Header { name: Header::_CACHE_CONTROL.into(), value: "no-cache".into() });
264    response.headers.push(Header { name: "X-Accel-Buffering".into(), value: "no".into() });
265    response.stream_pipe = body;
266    response
267}
268
269// ── Streaming ─────────────────────────────────────────────────────────────────
270
271/// Options for a streaming SSE response; [`Sse::stream`] uses the defaults.
272///
273/// ```rust,no_run
274/// use std::time::Duration;
275/// use rust_web_server::sse::SseStream;
276///
277/// let (events, response) = SseStream::new().buffer(256).keep_alive(Duration::from_secs(5)).start();
278/// ```
279pub struct SseStream {
280    buffer: usize,
281    keep_alive: Duration,
282}
283
284impl Default for SseStream {
285    fn default() -> Self {
286        SseStream::new()
287    }
288}
289
290impl SseStream {
291    /// 32 events buffered, keep-alive every 15 seconds.
292    pub fn new() -> Self {
293        SseStream { buffer: 32, keep_alive: Duration::from_secs(15) }
294    }
295
296    /// How many events may wait for a slow client before
297    /// [`SseSender::send`] blocks (and [`SseSender::try_send`] drops).
298    pub fn buffer(mut self, events: usize) -> Self {
299        self.buffer = events.max(1);
300        self
301    }
302
303    /// After this long with nothing sent, a `: keep-alive` comment goes out:
304    /// proxies keep the connection, and a client that left is noticed.
305    pub fn keep_alive(mut self, interval: Duration) -> Self {
306        self.keep_alive = interval.max(Duration::from_millis(10));
307        self
308    }
309
310    /// The sender and the response to return from the handler.
311    pub fn start(self) -> (SseSender, Response) {
312        let (tx, rx) = mpsc::sync_channel(self.buffer);
313        let response = stream_response(Some(Box::new(SseReader::new(rx, self.keep_alive))));
314        (SseSender { tx }, response)
315    }
316}
317
318/// Sends events to one streaming SSE response; clone it to send from
319/// several places. The stream ends when the last clone is dropped.
320#[derive(Clone)]
321pub struct SseSender {
322    tx: SyncSender<Vec<u8>>,
323}
324
325/// The client is gone (or the response was never sent): stop sending.
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub struct SseClosed;
328
329impl std::fmt::Display for SseClosed {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        f.write_str("the SSE stream is closed")
332    }
333}
334
335impl std::error::Error for SseClosed {}
336
337impl SseSender {
338    /// Send `event`, waiting while the buffer is full (a slow client slows
339    /// the sender down rather than growing memory). Blocks the thread: in
340    /// async code use [`Self::send_async`] or [`Self::try_send`].
341    pub fn send(&self, event: SseEvent) -> Result<(), SseClosed> {
342        self.tx.send(event.encode()).map_err(|_| SseClosed)
343    }
344
345    /// Send `event` if there's room: `Ok(false)` when the buffer is full and
346    /// the event was dropped.
347    pub fn try_send(&self, event: SseEvent) -> Result<bool, SseClosed> {
348        match self.tx.try_send(event.encode()) {
349            Ok(()) => Ok(true),
350            Err(TrySendError::Full(_)) => Ok(false),
351            Err(TrySendError::Disconnected(_)) => Err(SseClosed),
352        }
353    }
354
355    /// [`Self::send`] without blocking the async runtime: waits for room
356    /// with short sleeps.
357    #[cfg(feature = "http2")]
358    pub async fn send_async(&self, event: SseEvent) -> Result<(), SseClosed> {
359        let mut frame = event.encode();
360        loop {
361            match self.tx.try_send(frame) {
362                Ok(()) => return Ok(()),
363                Err(TrySendError::Disconnected(_)) => return Err(SseClosed),
364                Err(TrySendError::Full(back)) => {
365                    frame = back;
366                    tokio::time::sleep(Duration::from_millis(5)).await;
367                }
368            }
369        }
370    }
371
372    /// Send a data-only event (the client's `message` event).
373    pub fn data(&self, data: &str) -> Result<(), SseClosed> {
374        self.send(SseEvent::data(data))
375    }
376
377    /// Send a comment line (ignored by clients).
378    pub fn comment(&self, text: &str) -> Result<(), SseClosed> {
379        self.tx.send(comment_bytes(text)).map_err(|_| SseClosed)
380    }
381}
382
383/// The body of a streaming SSE response: a blocking [`std::io::Read`] over
384/// the channel of encoded events, so the server's streaming writers (HTTP/1.1
385/// chunked, HTTP/2 and HTTP/3 DATA) drive it. Waits for the next event;
386/// after `keep_alive` with none, yields a `: keep-alive` comment instead;
387/// at EOF when every sender is gone.
388pub(crate) struct SseReader {
389    rx: Receiver<Vec<u8>>,
390    leftover: Vec<u8>,
391    offset: usize,
392    keep_alive: Duration,
393}
394
395impl SseReader {
396    pub(crate) fn new(rx: Receiver<Vec<u8>>, keep_alive: Duration) -> Self {
397        SseReader { rx, leftover: Vec::new(), offset: 0, keep_alive }
398    }
399}
400
401impl std::io::Read for SseReader {
402    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
403        if self.offset >= self.leftover.len() {
404            self.leftover = match self.rx.recv_timeout(self.keep_alive) {
405                Ok(frame) => frame,
406                Err(RecvTimeoutError::Timeout) => b": keep-alive\n\n".to_vec(),
407                Err(RecvTimeoutError::Disconnected) => return Ok(0),
408            };
409            self.offset = 0;
410        }
411        let n = (self.leftover.len() - self.offset).min(buf.len());
412        buf[..n].copy_from_slice(&self.leftover[self.offset..self.offset + n]);
413        self.offset += n;
414        Ok(n)
415    }
416}