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}