Skip to main content

netget/server/sse/
wire.rs

1//! Server-sent events (WHATWG HTML ยง9.2, `text/event-stream`): encoding for the server and the
2//! event-stream parser the client uses. Shared so both roles agree on the format byte for byte.
3use anyhow::{ensure, Result};
4
5/// A line the parser will hold before giving up on the stream.
6pub const MAX_LINE_BYTES: usize = 64 * 1024;
7/// One event's data at most.
8pub const MAX_EVENT_BYTES: usize = 1024 * 1024;
9
10fn single_line(field: &str, value: &str) -> Result<()> {
11    ensure!(
12        !value.contains(['\r', '\n']),
13        "{field} is one line: it may not contain CR or LF"
14    );
15    Ok(())
16}
17
18/// One event: `event:`, `id:`, `retry:` and a `data:` line per line of `data`, then a blank line.
19pub fn encode_event(
20    event: Option<&str>,
21    id: Option<&str>,
22    data: &str,
23    retry_ms: Option<u64>,
24) -> Result<Vec<u8>> {
25    ensure!(
26        data.len() <= MAX_EVENT_BYTES,
27        "data is at most {MAX_EVENT_BYTES} bytes"
28    );
29    let mut out = String::with_capacity(data.len() + 64);
30    if let Some(event) = event.filter(|e| !e.is_empty()) {
31        single_line("event", event)?;
32        out.push_str("event: ");
33        out.push_str(event);
34        out.push('\n');
35    }
36    if let Some(id) = id {
37        single_line("id", id)?;
38        ensure!(
39            !id.contains('\0'),
40            "id may not contain NUL (the client would ignore it)"
41        );
42        out.push_str("id: ");
43        out.push_str(id);
44        out.push('\n');
45    }
46    if let Some(retry) = retry_ms {
47        out.push_str(&format!("retry: {retry}\n"));
48    }
49    for line in data.split("\r\n").flat_map(|l| l.split(['\r', '\n'])) {
50        out.push_str("data: ");
51        out.push_str(line);
52        out.push('\n');
53    }
54    out.push('\n');
55    Ok(out.into_bytes())
56}
57
58/// A comment line (`: text`), which clients ignore; servers send it to keep a stream alive.
59pub fn encode_comment(text: &str) -> Result<Vec<u8>> {
60    single_line("comment", text)?;
61    Ok(format!(": {text}\n\n").into_bytes())
62}
63
64/// A `retry:` field on its own, telling the client how long to wait before reconnecting.
65pub fn encode_retry(retry_ms: u64) -> Vec<u8> {
66    format!("retry: {retry_ms}\n\n").into_bytes()
67}
68
69/// One dispatched event.
70#[derive(Debug, Clone, PartialEq)]
71pub struct Event {
72    /// The event type (`message` when the stream named none).
73    pub event: String,
74    pub data: String,
75    /// The last event ID buffer after this event, which a reconnect sends as `Last-Event-ID`.
76    pub last_event_id: String,
77}
78
79/// The WHATWG event-stream interpretation, fed bytes as they arrive.
80#[derive(Default)]
81pub struct Parser {
82    pending: Vec<u8>,
83    started: bool,
84    skip_lf: bool,
85    event: String,
86    data: String,
87    has_data: bool,
88    pub last_event_id: String,
89    /// The most recent valid `retry:` value.
90    pub retry_ms: Option<u64>,
91}
92
93impl Parser {
94    pub fn with_last_event_id(id: &str) -> Self {
95        Self {
96            last_event_id: id.to_string(),
97            ..Self::default()
98        }
99    }
100
101    /// Start a new connection's stream (the last event ID and retry carry over).
102    pub fn reset_stream(&mut self) {
103        self.pending.clear();
104        self.started = false;
105        self.skip_lf = false;
106        self.event.clear();
107        self.data.clear();
108        self.has_data = false;
109    }
110
111    /// Feed bytes; returns the events they completed. Fails on a line or event over its bound.
112    pub fn feed(&mut self, bytes: &[u8]) -> Result<Vec<Event>> {
113        let mut out = Vec::new();
114        for &b in bytes {
115            if self.skip_lf {
116                self.skip_lf = false;
117                if b == b'\n' {
118                    continue;
119                }
120            }
121            match b {
122                b'\r' | b'\n' => {
123                    self.skip_lf = b == b'\r';
124                    let line = std::mem::take(&mut self.pending);
125                    if let Some(e) = self.line(&line)? {
126                        out.push(e);
127                    }
128                }
129                _ => {
130                    ensure!(
131                        self.pending.len() < MAX_LINE_BYTES,
132                        "an event-stream line longer than {MAX_LINE_BYTES} bytes"
133                    );
134                    self.pending.push(b);
135                }
136            }
137        }
138        Ok(out)
139    }
140
141    fn line(&mut self, raw: &[u8]) -> Result<Option<Event>> {
142        let mut line = String::from_utf8_lossy(raw).into_owned();
143        if !self.started {
144            self.started = true;
145            if let Some(rest) = line.strip_prefix('\u{feff}') {
146                line = rest.to_string();
147            }
148        }
149        if line.is_empty() {
150            return Ok(self.dispatch());
151        }
152        if line.starts_with(':') {
153            return Ok(None);
154        }
155        let (field, value) = match line.split_once(':') {
156            Some((f, v)) => (f.to_string(), v.strip_prefix(' ').unwrap_or(v).to_string()),
157            None => (line.clone(), String::new()),
158        };
159        match field.as_str() {
160            "event" => self.event = value,
161            "data" => {
162                ensure!(
163                    self.data.len() + value.len() < MAX_EVENT_BYTES,
164                    "an event's data over {MAX_EVENT_BYTES} bytes"
165                );
166                self.data.push_str(&value);
167                self.data.push('\n');
168                self.has_data = true;
169            }
170            "id" if !value.contains('\0') => self.last_event_id = value,
171            "retry" if !value.is_empty() && value.bytes().all(|b| b.is_ascii_digit()) => {
172                self.retry_ms = value.parse().ok();
173            }
174            _ => {}
175        }
176        Ok(None)
177    }
178
179    fn dispatch(&mut self) -> Option<Event> {
180        let event = std::mem::take(&mut self.event);
181        if !self.has_data {
182            return None;
183        }
184        self.has_data = false;
185        let mut data = std::mem::take(&mut self.data);
186        data.pop();
187        Some(Event {
188            event: if event.is_empty() {
189                "message".into()
190            } else {
191                event
192            },
193            data,
194            last_event_id: self.last_event_id.clone(),
195        })
196    }
197}