Skip to main content

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