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}