1use crate::Conn;
19use futures_lite::{AsyncRead, stream::Stream};
20use std::{
21 collections::VecDeque,
22 error::Error,
23 fmt::{self, Debug, Display, Formatter},
24 ops::{Deref, DerefMut},
25 pin::Pin,
26 task::{Context, Poll, ready},
27 time::Duration,
28};
29use trillium_http::{KnownHeaderName, Status};
30
31const READ_BUF_LEN: usize = 8 * 1024;
32
33impl Conn {
34 pub async fn into_sse(mut self) -> Result<EventStream, SseError> {
50 if self.status().is_some() {
51 return Err(SseError::new(self, SseErrorKind::AlreadyExecuted));
52 }
53
54 let accept = self.request_headers().get_str(KnownHeaderName::Accept);
58 if accept.is_none_or(|accept| accept.trim() == "*/*") {
59 self.request_headers_mut()
60 .insert(KnownHeaderName::Accept, "text/event-stream");
61 }
62
63 if let Err(e) = (&mut self).await {
64 return Err(SseError::new(self, e.into()));
65 }
66
67 let status = self.status().expect("Response did not include status");
68 if !status.is_success() {
69 return Err(SseError::new(self, SseErrorKind::Status(status)));
70 }
71
72 if !is_event_stream(
73 self.response_headers()
74 .get_str(KnownHeaderName::ContentType),
75 ) {
76 let content_type = self
77 .response_headers()
78 .get_str(KnownHeaderName::ContentType)
79 .map(String::from);
80 return Err(SseError::new(
81 self,
82 SseErrorKind::UnexpectedContentType(content_type),
83 ));
84 }
85
86 Ok(EventStream::new(self))
87 }
88}
89
90fn is_event_stream(content_type: Option<&str>) -> bool {
93 content_type.is_some_and(|ct| {
94 ct.split(';')
95 .next()
96 .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case("text/event-stream"))
97 })
98}
99
100#[derive(Debug, Clone, Eq, PartialEq)]
109pub struct Event {
110 data: String,
111 event_type: Option<String>,
112 id: Option<String>,
113 retry: Option<Duration>,
114}
115
116impl Event {
117 #[must_use]
119 pub fn data(&self) -> &str {
120 &self.data
121 }
122
123 #[must_use]
125 pub fn event_type(&self) -> Option<&str> {
126 self.event_type.as_deref()
127 }
128
129 #[must_use]
131 pub fn id(&self) -> Option<&str> {
132 self.id.as_deref()
133 }
134
135 #[must_use]
140 pub fn retry(&self) -> Option<Duration> {
141 self.retry
142 }
143}
144
145#[derive(Debug)]
152pub struct EventStream {
153 conn: Conn,
154 decoder: Decoder,
155 pending: VecDeque<Event>,
156 read_buf: Box<[u8]>,
157 done: bool,
158}
159
160impl EventStream {
161 fn new(conn: Conn) -> Self {
162 Self {
163 conn,
164 decoder: Decoder::default(),
165 pending: VecDeque::new(),
166 read_buf: vec![0; READ_BUF_LEN].into_boxed_slice(),
167 done: false,
168 }
169 }
170
171 pub fn conn(&self) -> &Conn {
174 &self.conn
175 }
176}
177
178impl Stream for EventStream {
179 type Item = trillium_http::Result<Event>;
180
181 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
182 let this = self.get_mut();
183 loop {
184 if let Some(event) = this.pending.pop_front() {
185 return Poll::Ready(Some(Ok(event)));
186 }
187 if this.done {
188 return Poll::Ready(None);
189 }
190
191 let mut response_body = this.conn.response_body();
192 match ready!(Pin::new(&mut response_body).poll_read(cx, &mut this.read_buf)) {
193 Ok(0) => {
195 this.done = true;
196 return Poll::Ready(None);
197 }
198 Ok(n) => this.decoder.push(&this.read_buf[..n], &mut this.pending),
199 Err(e) => {
200 this.done = true;
201 return Poll::Ready(Some(Err(e.into())));
202 }
203 }
204 }
205 }
206}
207
208#[derive(Debug, Default)]
214struct Decoder {
215 line: Vec<u8>,
216 last_char_was_cr: bool,
217 data: String,
218 event_type: Option<String>,
219 id: Option<String>,
220 retry: Option<Duration>,
221 has_data: bool,
222}
223
224impl Decoder {
225 fn push(&mut self, bytes: &[u8], out: &mut VecDeque<Event>) {
226 for &byte in bytes {
227 match byte {
228 b'\r' => {
229 self.line_done(out);
230 self.last_char_was_cr = true;
231 }
232 b'\n' if self.last_char_was_cr => self.last_char_was_cr = false,
233 b'\n' => self.line_done(out),
234 _ => {
235 self.last_char_was_cr = false;
236 self.line.push(byte);
237 }
238 }
239 }
240 }
241
242 fn line_done(&mut self, out: &mut VecDeque<Event>) {
243 if self.line.is_empty() {
244 self.dispatch(out);
245 } else {
246 let mut line = std::mem::take(&mut self.line);
247 self.process_field(&line);
248 line.clear();
249 self.line = line;
250 }
251 }
252
253 fn process_field(&mut self, line: &[u8]) {
254 let (field, value) = match memchr::memchr(b':', line) {
255 Some(0) => return, Some(colon) => {
257 let value = &line[colon + 1..];
258 let value = value.strip_prefix(b" ").unwrap_or(value);
259 (&line[..colon], value)
260 }
261 None => (line, &b""[..]),
262 };
263
264 match field {
265 b"event" => self.event_type = Some(String::from_utf8_lossy(value).into_owned()),
266
267 b"data" => {
268 self.data.push_str(&String::from_utf8_lossy(value));
269 self.data.push('\n');
270 self.has_data = true;
271 }
272
273 b"id" => {
274 if !value.contains(&0) {
275 self.id = Some(String::from_utf8_lossy(value).into_owned());
276 }
277 }
278
279 b"retry" => {
280 if !value.is_empty()
281 && value.iter().all(u8::is_ascii_digit)
282 && let Ok(ms) = std::str::from_utf8(value).unwrap_or_default().parse()
283 {
284 self.retry = Some(Duration::from_millis(ms));
285 }
286 }
287
288 _ => {}
289 }
290 }
291
292 fn dispatch(&mut self, out: &mut VecDeque<Event>) {
293 if !self.has_data {
294 self.data.clear();
297 self.event_type = None;
298 return;
299 }
300
301 if self.data.ends_with('\n') {
302 self.data.pop();
303 }
304
305 out.push_back(Event {
306 data: std::mem::take(&mut self.data),
307 event_type: self.event_type.take().filter(|s| !s.is_empty()),
308 id: self.id.clone(),
309 retry: self.retry.take(),
310 });
311 self.has_data = false;
312 }
313}
314
315#[derive(thiserror::Error, Debug)]
317#[non_exhaustive]
318pub enum SseErrorKind {
319 #[error(transparent)]
321 Http(#[from] trillium_http::Error),
322
323 #[error("Unexpected response status {0} for SSE request")]
325 Status(Status),
326
327 #[error("Unexpected content-type for SSE request: {0:?}")]
329 UnexpectedContentType(Option<String>),
330
331 #[error(
335 "Conn::into_sse called after execution — build the conn and await into_sse instead of \
336 awaiting the conn separately"
337 )]
338 AlreadyExecuted,
339
340 #[error("SSE response had no body")]
342 NoBody,
343}
344
345#[derive(Debug)]
350pub struct SseError {
351 pub kind: SseErrorKind,
353 conn: Box<Conn>,
354}
355
356impl SseError {
357 fn new(conn: Conn, kind: SseErrorKind) -> Self {
358 Self {
359 kind,
360 conn: Box::new(conn),
361 }
362 }
363}
364
365impl From<SseError> for Conn {
366 fn from(value: SseError) -> Self {
367 *value.conn
368 }
369}
370
371impl Deref for SseError {
372 type Target = Conn;
373
374 fn deref(&self) -> &Self::Target {
375 &self.conn
376 }
377}
378
379impl DerefMut for SseError {
380 fn deref_mut(&mut self) -> &mut Self::Target {
381 &mut self.conn
382 }
383}
384
385impl Error for SseError {}
386
387impl Display for SseError {
388 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
389 Display::fmt(&self.kind, f)
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 fn decode(input: &[u8]) -> Vec<Event> {
401 let mut whole = Decoder::default();
402 let mut whole_out = VecDeque::new();
403 whole.push(input, &mut whole_out);
404
405 let mut split = Decoder::default();
406 let mut split_out = VecDeque::new();
407 for byte in input {
408 split.push(&[*byte], &mut split_out);
409 }
410
411 assert_eq!(whole_out, split_out, "chunked decode diverged from whole");
412 whole_out.into()
413 }
414
415 #[test]
416 fn fields_comments_and_terminators() {
417 let events =
418 decode(b": this is a comment\nevent: greeting\ndata: hello\nid: 42\nretry: 3000\n\n");
419 assert_eq!(events.len(), 1);
420 let event = &events[0];
421 assert_eq!(event.data(), "hello");
422 assert_eq!(event.event_type(), Some("greeting"));
423 assert_eq!(event.id(), Some("42"));
424 assert_eq!(event.retry(), Some(Duration::from_millis(3000)));
425 }
426
427 #[test]
428 fn multiline_data_joins_with_newline() {
429 let events = decode(b"data: one\ndata: two\ndata:three\n\n");
430 assert_eq!(events[0].data(), "one\ntwo\nthree");
432 }
433
434 #[test]
435 fn crlf_and_cr_terminators() {
436 let crlf = decode(b"data: a\r\n\r\n");
437 assert_eq!(crlf[0].data(), "a");
438 let cr = decode(b"data: b\r\r");
439 assert_eq!(cr[0].data(), "b");
440 }
441
442 #[test]
443 fn empty_data_line_dispatches_empty_event() {
444 let events = decode(b"data\n\n");
446 assert_eq!(events.len(), 1);
447 assert_eq!(events[0].data(), "");
448 }
449
450 #[test]
451 fn blank_lines_without_data_dispatch_nothing() {
452 assert!(decode(b"\n\n\n").is_empty());
453 assert!(decode(b": just a comment\n\n").is_empty());
454 }
455
456 #[test]
457 fn incomplete_trailing_event_is_discarded() {
458 assert!(decode(b"data: pending\n").is_empty());
460 }
461
462 #[test]
463 fn id_persists_across_events_retry_does_not() {
464 let events = decode(b"id: 1\nretry: 500\ndata: a\n\ndata: b\n\n");
465 assert_eq!(events[0].id(), Some("1"));
466 assert_eq!(events[0].retry(), Some(Duration::from_millis(500)));
467 assert_eq!(events[1].id(), Some("1"));
469 assert_eq!(events[1].retry(), None);
470 }
471
472 #[test]
473 fn invalid_retry_is_ignored() {
474 let events = decode(b"retry: not-a-number\ndata: a\n\n");
475 assert_eq!(events[0].retry(), None);
476 }
477}