Skip to main content

gateway_core/
stream.rs

1#[derive(Debug, Clone, PartialEq, Eq)]
2pub struct SseEvent {
3    pub event: Option<String>,
4    pub data: String,
5}
6
7#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
8pub enum StreamParseError {
9    #[error("SSE buffer exceeded {0} bytes")]
10    BufferLimit(usize),
11    #[error("stream ended with an incomplete SSE event")]
12    Incomplete,
13}
14
15pub struct SseDecoder {
16    buffer: String,
17    /// How much of `buffer` is already known to hold no event delimiter, so a
18    /// partial event is never rescanned once per chunk that extends it.
19    scanned: usize,
20    max_buffer_bytes: usize,
21}
22
23impl Default for SseDecoder {
24    fn default() -> Self {
25        Self::new(1024 * 1024)
26    }
27}
28
29impl SseDecoder {
30    pub fn new(max_buffer_bytes: usize) -> Self {
31        Self {
32            buffer: String::new(),
33            scanned: 0,
34            max_buffer_bytes,
35        }
36    }
37
38    pub fn push(&mut self, chunk: &str) -> Result<Vec<SseEvent>, StreamParseError> {
39        self.buffer.push_str(chunk);
40        if self.buffer.len() > self.max_buffer_bytes {
41            return Err(StreamParseError::BufferLimit(self.max_buffer_bytes));
42        }
43        let mut events = Vec::new();
44        // Scanning and draining per event would reread and reshuffle the whole
45        // buffer once per event, which is quadratic in what an upstream sends:
46        // a chunk of nothing but delimiters costs the gateway far more than it
47        // costs the provider. A cursor plus a single drain keeps it linear.
48        let mut search = self.scanned;
49        let mut consumed = 0;
50        while let Some(offset) = event_end(&self.buffer[search..]) {
51            let end = search + offset;
52            let block = self.buffer[consumed..end].replace('\r', "");
53            let delimiter_len = if self.buffer[end..].starts_with("\r\n\r\n") {
54                4
55            } else {
56                2
57            };
58            consumed = end + delimiter_len;
59            search = consumed;
60            if let Some(event) = parse_event(&block) {
61                events.push(event);
62            }
63        }
64        self.buffer.drain(..consumed);
65        // A delimiter can straddle the next chunk, so the tail stays unscanned.
66        let mut scanned = self.buffer.len().saturating_sub(DELIMITER_OVERLAP);
67        while scanned > 0 && !self.buffer.is_char_boundary(scanned) {
68            scanned -= 1;
69        }
70        self.scanned = scanned;
71        Ok(events)
72    }
73
74    /// What the decoder is still holding between events, for the out-of-tree
75    /// fuzz project to assert the buffer stays bounded and never retains a
76    /// complete event. Compiled only under `--cfg fuzzing`, which nothing but
77    /// [`fuzz/`](https://github.com/Litvue/axond/tree/main/fuzz) sets, so this
78    /// widens no published API.
79    #[cfg(fuzzing)]
80    pub fn fuzz_buffered(&self) -> &str {
81        &self.buffer
82    }
83
84    pub fn finish(self) -> Result<(), StreamParseError> {
85        if self.buffer.trim().is_empty() {
86            Ok(())
87        } else {
88            Err(StreamParseError::Incomplete)
89        }
90    }
91}
92
93/// The longest prefix of an event delimiter that can end a chunk: `\r\n\r`.
94const DELIMITER_OVERLAP: usize = 3;
95
96/// Where the first event delimiter starts, whichever of the two it is.
97///
98/// One left-to-right pass rather than a search for each delimiter: searching
99/// separately costs a scan of the whole buffer for the delimiter that is not
100/// there, on every event, which a stream of LF-delimited events pays in full.
101fn event_end(buffer: &str) -> Option<usize> {
102    let bytes = buffer.as_bytes();
103    for index in 0..bytes.len().saturating_sub(1) {
104        if bytes[index] == b'\n' && bytes[index + 1] == b'\n' {
105            return Some(index);
106        }
107        if bytes[index] == b'\r' && bytes[index..].starts_with(b"\r\n\r\n") {
108            return Some(index);
109        }
110    }
111    None
112}
113
114fn parse_event(block: &str) -> Option<SseEvent> {
115    let mut event = None;
116    let mut data = Vec::new();
117    for line in block.lines() {
118        if line.starts_with(':') {
119            continue;
120        }
121        if let Some(value) = line.strip_prefix("event:") {
122            event = Some(value.trim_start().to_owned());
123        } else if let Some(value) = line.strip_prefix("data:") {
124            data.push(value.trim_start());
125        }
126    }
127    (!data.is_empty()).then(|| SseEvent {
128        event,
129        data: data.join("\n"),
130    })
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn parses_fragmented_and_multiline_events() {
139        let mut decoder = SseDecoder::default();
140        assert!(
141            decoder
142                .push("event: delta\r\ndata: {\"a\":")
143                .unwrap()
144                .is_empty()
145        );
146        let events = decoder.push("1}\r\ndata: tail\r\n\r\n").unwrap();
147        assert_eq!(events[0].event.as_deref(), Some("delta"));
148        assert_eq!(events[0].data, "{\"a\":1}\ntail");
149        decoder.finish().unwrap();
150    }
151
152    /// What the `sse_decode` fuzz target asserts over arbitrary bodies, pinned
153    /// here for the cases the corpus is built from: where a chunk boundary
154    /// falls cannot change what a stream decodes to.
155    #[test]
156    fn every_chunk_boundary_decodes_a_body_identically() {
157        for body in [
158            "data: one\n\ndata: two\n\n",
159            "event: delta\r\ndata: {\"a\":1}\r\n\r\n: keep-alive\r\n\r\ndata: [DONE]\r\n\r\n",
160            "data: first\ndata: second\n\ndata: tail",
161            ": comment only\n\n\n\ndata: \n\n",
162        ] {
163            let mut whole = SseDecoder::default();
164            let expected = whole.push(body).unwrap();
165            for cut in 1..body.len() {
166                if !body.is_char_boundary(cut) {
167                    continue;
168                }
169                let mut split = SseDecoder::default();
170                let mut events = split.push(&body[..cut]).unwrap();
171                events.extend(split.push(&body[cut..]).unwrap());
172                assert_eq!(events, expected, "{body:?} split at {cut}");
173            }
174        }
175    }
176
177    /// A stream that ends mid-event is a controlled error, not a panic and not
178    /// a silently accepted truncation.
179    #[test]
180    fn a_truncated_final_event_is_refused_by_finish() {
181        let mut decoder = SseDecoder::default();
182        assert!(decoder.push("data: complete\n\ndata: trunc").unwrap().len() == 1);
183        assert_eq!(decoder.finish(), Err(StreamParseError::Incomplete));
184    }
185
186    /// The scan cursor must not skip a delimiter that arrives one byte at a
187    /// time, which is the boundary case it exists to avoid rescanning.
188    #[test]
189    fn a_delimiter_split_byte_by_byte_still_terminates_an_event() {
190        let body = "event: delta\r\ndata: one\r\n\r\ndata: two\n\n";
191        let mut decoder = SseDecoder::default();
192        let mut events = Vec::new();
193        for byte in 0..body.len() {
194            events.extend(decoder.push(&body[byte..=byte]).unwrap());
195        }
196        assert_eq!(events.len(), 2);
197        assert_eq!(events[0].event.as_deref(), Some("delta"));
198        assert_eq!(events[0].data, "one");
199        assert_eq!(events[1].data, "two");
200        decoder.finish().unwrap();
201    }
202
203    /// A stream of nothing but delimiters is the cheapest thing an upstream can
204    /// send and used to be the most expensive thing to parse: scanning and
205    /// draining per event made the cost quadratic in the chunk.
206    #[test]
207    fn a_chunk_of_many_tiny_events_is_parsed_in_one_pass() {
208        let events = 200_000;
209        let mut decoder = SseDecoder::new(8 * 1024 * 1024);
210        let decoded = decoder.push(&"data: x\n\n".repeat(events)).unwrap();
211        assert_eq!(decoded.len(), events);
212        decoder.finish().unwrap();
213    }
214
215    /// The buffer limit is the bound on what an upstream can make the gateway
216    /// hold for a stream that never terminates an event.
217    #[test]
218    fn an_unterminated_event_trips_the_buffer_limit() {
219        let mut decoder = SseDecoder::new(64);
220        assert_eq!(
221            decoder.push(&"data: ".repeat(64)),
222            Err(StreamParseError::BufferLimit(64))
223        );
224    }
225}