Skip to main content

zai_rs/model/
sse_parser.rs

1//! Shared SSE (Server-Sent Events) line parsing utilities.
2//!
3//! Extracts the common logic of buffering raw byte chunks, splitting on `\n`,
4//! trimming `\r\n`, and yielding `data:` field payloads.
5
6use std::{collections::VecDeque, pin::Pin};
7
8use futures_util::{Stream, StreamExt};
9
10use crate::{ZaiError, ZaiResult, client::error::codes};
11
12pub(crate) type DecodedSseStream<T> = Pin<Box<dyn Stream<Item = ZaiResult<T>> + Send + 'static>>;
13
14/// Incremental SSE event parser.
15///
16/// Unlike [`extract_sse_data_lines`], this parser follows event boundaries and
17/// joins multiple `data:` lines in the same event with `\n`.
18#[derive(Debug, Default)]
19pub struct SseEventParser {
20    buf: Vec<u8>,
21    event_data: Vec<Vec<u8>>,
22}
23
24impl SseEventParser {
25    /// Create a new empty SSE event parser.
26    pub fn new() -> Self {
27        Self::default()
28    }
29
30    /// Bytes retained for the current incomplete line/event.
31    pub(crate) fn buffered_len(&self) -> usize {
32        let payload_bytes = self
33            .event_data
34            .iter()
35            .fold(0usize, |total, line| total.saturating_add(line.len()));
36        self.buf
37            .len()
38            .saturating_add(payload_bytes)
39            .saturating_add(self.event_data.len().saturating_sub(1))
40    }
41
42    /// Push a transport byte chunk and return completed SSE event payloads.
43    pub fn push(&mut self, new_bytes: &[u8]) -> Vec<Vec<u8>> {
44        self.buf.extend_from_slice(new_bytes);
45        let mut events = Vec::with_capacity(4);
46
47        // Scan forward without a per-line `drain` (which memmoves the entire
48        // tail on every line → O(n^2) per chunk, and this runs for every
49        // streaming token). Track consumed bytes and drop them once at the end.
50        let mut consumed = 0;
51        while let Some(rel) = self.buf[consumed..].iter().position(|&b| b == b'\n') {
52            let newline = consumed + rel;
53            // Line body is `[consumed, end)`; strip a single trailing CR.
54            let end = if newline > consumed && self.buf[newline - 1] == b'\r' {
55                newline - 1
56            } else {
57                newline
58            };
59            let line = &self.buf[consumed..end];
60            consumed = newline + 1;
61
62            if line.is_empty() {
63                // Common case (exactly one `data:` line per event — the norm for
64                // chat token streams): move the single buffer straight out and
65                // skip the extra allocation+copy that `join_event_data` would
66                // do. Multi-line events still join (and keep buffer reuse).
67                match self.event_data.len() {
68                    0 => {},
69                    1 => {
70                        events.push(self.event_data.swap_remove(0));
71                    },
72                    _ => {
73                        events.push(join_event_data(&self.event_data));
74                        self.event_data.clear();
75                    },
76                }
77                continue;
78            }
79
80            if line.starts_with(b":") {
81                continue;
82            }
83
84            if let Some(rest) = line.strip_prefix(b"data:") {
85                self.event_data.push(trim_one_leading_space(rest).to_vec());
86            }
87        }
88
89        self.buf.drain(..consumed);
90        events
91    }
92
93    /// Flush any event buffered by `data:` lines that never saw a terminating
94    /// blank line.
95    ///
96    /// Per the SSE spec an event is dispatched on a blank line. If the transport
97    /// closes after a final `data: {...}\n` with **no** following blank line
98    /// (a reverse proxy stripping trailing whitespace, a truncated TLS frame at
99    /// connection close, a non-conformant emitter), [`SseEventParser::push`]
100    /// leaves that event buffered in `event_data` and it would otherwise be
101    /// silently dropped — including the last content/usage chunk, or even the
102    /// `[DONE]` marker if its trailing blank line was lost.
103    ///
104    /// Call this once the byte stream has ended to emit any such trailing event.
105    /// Returns an empty `Vec` when nothing is buffered. Any incomplete line
106    /// still in `buf` (a `data:` line with no trailing newline) is intentionally
107    /// NOT emitted — it is not a complete SSE line.
108    pub fn finish(&mut self) -> Vec<Vec<u8>> {
109        match self.event_data.len() {
110            0 => Vec::new(),
111            1 => vec![self.event_data.swap_remove(0)],
112            _ => {
113                let event = join_event_data(&self.event_data);
114                self.event_data.clear();
115                vec![event]
116            },
117        }
118    }
119}
120
121struct RequiredDoneState {
122    raw: crate::client::transport::SseByteStream,
123    parser: SseEventParser,
124    pending: VecDeque<Vec<u8>>,
125    input_finished: bool,
126    terminated: bool,
127}
128
129/// Decode a typed SSE stream whose successful completion requires `[DONE]`.
130///
131/// Transport failures, oversized events, malformed items, and in-band business
132/// errors are each yielded once and then terminate the stream. EOF without the
133/// terminal marker is an error rather than normal completion.
134pub(crate) fn decode_required_done_stream<T, F>(
135    raw: crate::client::transport::SseByteStream,
136    decode: F,
137) -> DecodedSseStream<T>
138where
139    T: Send + 'static,
140    F: Fn(&[u8]) -> ZaiResult<T> + Send + 'static,
141{
142    let payloads = required_done_payloads(raw);
143    let stream = futures_util::stream::unfold(
144        (payloads, decode, false),
145        |(mut payloads, decode, terminated)| async move {
146            if terminated {
147                return None;
148            }
149            let item = payloads.next().await?;
150            let item = item.and_then(|payload| decode(&payload));
151            let terminated = item.is_err();
152            Some((item, (payloads, decode, terminated)))
153        },
154    );
155    Box::pin(stream)
156}
157
158fn required_done_payloads(
159    raw: crate::client::transport::SseByteStream,
160) -> Pin<Box<dyn Stream<Item = ZaiResult<Vec<u8>>> + Send + 'static>> {
161    let state = RequiredDoneState {
162        raw,
163        parser: SseEventParser::new(),
164        pending: VecDeque::new(),
165        input_finished: false,
166        terminated: false,
167    };
168    let stream = futures_util::stream::unfold(state, |mut state| async move {
169        loop {
170            if state.terminated {
171                return None;
172            }
173
174            if let Some(payload) = state.pending.pop_front() {
175                if payload == b"[DONE]" {
176                    return None;
177                }
178                if (payload.len() as u64) > crate::client::transport::limits::JSON_RESPONSE_MAX {
179                    state.terminated = true;
180                    return Some((Err(event_too_large()), state));
181                }
182                if let Some(error) = std::str::from_utf8(&payload)
183                    .ok()
184                    .and_then(crate::client::transport::decode::extract_error_envelope)
185                {
186                    state.terminated = true;
187                    return Some((Err(business_error(error)), state));
188                }
189                return Some((Ok(payload), state));
190            }
191
192            if state.input_finished {
193                state.pending.extend(state.parser.finish());
194                if state.pending.is_empty() {
195                    state.terminated = true;
196                    return Some((Err(ended_without_done()), state));
197                }
198                continue;
199            }
200
201            match state.raw.next().await {
202                Some(Ok(chunk)) => {
203                    state.pending.extend(state.parser.push(&chunk));
204                    if state.parser.buffered_len()
205                        > crate::client::transport::limits::JSON_RESPONSE_MAX as usize
206                    {
207                        state.terminated = true;
208                        return Some((Err(event_too_large()), state));
209                    }
210                },
211                Some(Err(error)) => {
212                    state.terminated = true;
213                    return Some((Err(error), state));
214                },
215                None => state.input_finished = true,
216            }
217        }
218    });
219    Box::pin(stream)
220}
221
222fn event_too_large() -> ZaiError {
223    ZaiError::ApiError {
224        code: codes::SDK_VALIDATION,
225        message: format!(
226            "SSE event exceeded limit ({} bytes)",
227            crate::client::transport::limits::JSON_RESPONSE_MAX
228        ),
229    }
230}
231
232fn ended_without_done() -> ZaiError {
233    ZaiError::ApiError {
234        code: codes::SDK_IO,
235        message: "SSE stream ended before the required [DONE] event".to_string(),
236    }
237}
238
239fn business_error(error: crate::client::transport::decode::BusinessError) -> ZaiError {
240    let code = error
241        .code
242        .as_ref()
243        .and_then(crate::client::transport::parse_business_code)
244        .unwrap_or_default();
245    ZaiError::from_api_response(200, code, error.message)
246}
247
248fn trim_one_leading_space(bytes: &[u8]) -> &[u8] {
249    bytes.strip_prefix(b" ").unwrap_or(bytes)
250}
251
252fn join_event_data(lines: &[Vec<u8>]) -> Vec<u8> {
253    let total = lines.iter().map(std::vec::Vec::len).sum::<usize>() + lines.len().saturating_sub(1);
254    let mut event = Vec::with_capacity(total);
255    for (idx, line) in lines.iter().enumerate() {
256        if idx > 0 {
257            event.push(b'\n');
258        }
259        event.extend_from_slice(line);
260    }
261    event
262}
263
264/// Process a new chunk of bytes, extract completed SSE data lines.
265///
266/// Appends `new_bytes` to `buf`, then extracts all complete lines (delimited
267/// by `\n`). For each line, it:
268/// - Strips trailing `\r` and `\n`
269/// - Skips empty lines
270/// - Strips the `data:` prefix and its optional single leading space
271///
272/// Return owned byte vectors containing each `data:` payload.
273/// Lines that are not `data:` fields are silently skipped.
274///
275/// If a `data: [DONE]` line is encountered, it is yielded as a
276/// `[b"[DONE]"]` entry so the caller can detect stream termination.
277pub fn extract_sse_data_lines(buf: &mut Vec<u8>, new_bytes: &[u8]) -> Vec<Vec<u8>> {
278    buf.extend_from_slice(new_bytes);
279    let mut results = Vec::new();
280
281    let Some(last_newline) = buf.iter().rposition(|&b| b == b'\n') else {
282        return results;
283    };
284
285    let completed = &buf[..=last_newline];
286    for line_with_nl in completed.split_inclusive(|&b| b == b'\n') {
287        let mut line = line_with_nl;
288        if let Some(line_without_nl) = line.strip_suffix(b"\n") {
289            line = line_without_nl;
290        }
291        if let Some(line_without_cr) = line.strip_suffix(b"\r") {
292            line = line_without_cr;
293        }
294        if line.is_empty() {
295            continue;
296        }
297        if let Some(rest) = line.strip_prefix(b"data:") {
298            results.push(trim_one_leading_space(rest).to_vec());
299        }
300    }
301
302    buf.drain(..=last_newline);
303
304    results
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310    use bytes::Bytes;
311
312    #[test]
313    fn test_single_complete_line() {
314        let mut buf = Vec::new();
315        let lines = extract_sse_data_lines(&mut buf, b"data: hello\n");
316        assert_eq!(lines.len(), 1);
317        assert_eq!(lines[0], b"hello");
318    }
319
320    #[test]
321    fn test_data_field_without_optional_space() {
322        let mut buf = Vec::new();
323        let lines = extract_sse_data_lines(&mut buf, b"data:hello\n");
324        assert_eq!(lines, vec![b"hello".to_vec()]);
325    }
326
327    #[test]
328    fn test_partial_then_complete() {
329        let mut buf = Vec::new();
330        let lines1 = extract_sse_data_lines(&mut buf, b"data: hel");
331        assert!(lines1.is_empty());
332
333        let lines2 = extract_sse_data_lines(&mut buf, b"lo\n");
334        assert_eq!(lines2.len(), 1);
335        assert_eq!(lines2[0], b"hello");
336    }
337
338    #[test]
339    fn test_crlf_line_endings() {
340        let mut buf = Vec::new();
341        let lines = extract_sse_data_lines(&mut buf, b"data: world\r\n");
342        assert_eq!(lines.len(), 1);
343        assert_eq!(lines[0], b"world");
344    }
345
346    #[test]
347    fn test_multiple_events_in_one_chunk() {
348        let mut buf = Vec::new();
349        let lines = extract_sse_data_lines(&mut buf, b"data: first\n\ndata: second\n");
350        assert_eq!(lines.len(), 2);
351        assert_eq!(lines[0], b"first");
352        assert_eq!(lines[1], b"second");
353    }
354
355    #[test]
356    fn test_done_marker() {
357        let mut buf = Vec::new();
358        let lines = extract_sse_data_lines(&mut buf, b"data: [DONE]\n");
359        assert_eq!(lines.len(), 1);
360        assert_eq!(lines[0], b"[DONE]");
361    }
362
363    #[test]
364    fn test_non_data_lines_skipped() {
365        let mut buf = Vec::new();
366        let lines = extract_sse_data_lines(&mut buf, b": comment\nid: 123\ndata: payload\n");
367        assert_eq!(lines.len(), 1);
368        assert_eq!(lines[0], b"payload");
369    }
370
371    #[test]
372    fn test_empty_lines_ignored() {
373        let mut buf = Vec::new();
374        let lines = extract_sse_data_lines(&mut buf, b"\n\n\ndata: hello\n\n");
375        assert_eq!(lines.len(), 1);
376        assert_eq!(lines[0], b"hello");
377    }
378
379    #[test]
380    fn event_parser_yields_complete_events() {
381        let mut parser = SseEventParser::new();
382        assert!(parser.push(b"data: hel").is_empty());
383        assert_eq!(parser.push(b"lo\r\n\r\n"), vec![b"hello".to_vec()]);
384    }
385
386    #[test]
387    fn event_parser_joins_multi_data_lines() {
388        let mut parser = SseEventParser::new();
389        let events = parser.push(b"data: {\"a\":\ndata: 1}\n\n");
390        assert_eq!(events, vec![b"{\"a\":\n1}".to_vec()]);
391    }
392
393    #[test]
394    fn event_parser_ignores_comments_and_non_data_fields() {
395        let mut parser = SseEventParser::new();
396        let events = parser.push(b": keepalive\nid: 1\ndata: payload\n\n");
397        assert_eq!(events, vec![b"payload".to_vec()]);
398    }
399
400    #[test]
401    fn event_parser_done_marker_split_across_chunks() {
402        // Regression guard for the scan-in-place rewrite: a `[DONE]` payload
403        // split across two transport chunks must still reassemble into one
404        // `[DONE]` event.
405        let mut parser = SseEventParser::new();
406        assert!(parser.push(b"data: [DO").is_empty());
407        assert_eq!(parser.push(b"NE]\n\n"), vec![b"[DONE]".to_vec()]);
408    }
409
410    #[test]
411    fn event_parser_lone_cr_then_lf() {
412        // A CR ending one chunk followed by an LF starting the next must not
413        // leave a stray CR in the payload.
414        let mut parser = SseEventParser::new();
415        assert!(parser.push(b"data: hi\r").is_empty());
416        assert_eq!(parser.push(b"\n\n"), vec![b"hi".to_vec()]);
417    }
418
419    #[test]
420    fn extract_sse_handles_crlf_split_across_chunks() {
421        let mut buf = Vec::new();
422        assert!(extract_sse_data_lines(&mut buf, b"data: hello\r").is_empty());
423        let lines = extract_sse_data_lines(&mut buf, b"\n");
424        assert_eq!(lines, vec![b"hello".to_vec()]);
425    }
426
427    #[test]
428    fn finish_flushes_trailing_event_without_blank_line() {
429        // Regression: a final `data:` line whose terminating blank line was
430        // lost (truncated frame, proxy stripping trailing whitespace, a
431        // non-conformant emitter) must still be emitted via finish() rather
432        // than silently dropped — including the last content/[DONE] chunk.
433        let mut parser = SseEventParser::new();
434        assert!(parser.push(b"data: hello\n").is_empty()); // no blank line -> buffered
435        assert_eq!(parser.finish(), vec![b"hello".to_vec()]);
436        // finish() is idempotent.
437        assert!(parser.finish().is_empty());
438    }
439
440    #[test]
441    fn finish_flushes_multi_data_trailing_event_joined() {
442        let mut parser = SseEventParser::new();
443        assert!(parser.push(b"data: {\"a\":\ndata: 1}\n").is_empty());
444        assert_eq!(parser.finish(), vec![b"{\"a\":\n1}".to_vec()]);
445    }
446
447    #[test]
448    fn finish_noop_when_event_already_dispatched() {
449        let mut parser = SseEventParser::new();
450        assert_eq!(parser.push(b"data: hello\n\n"), vec![b"hello".to_vec()]);
451        assert!(parser.finish().is_empty());
452    }
453
454    #[test]
455    fn finish_emits_trailing_done_marker_without_blank_line() {
456        // The terminal [DONE] can also lose its trailing blank line; it must
457        // still surface so consumers can stop.
458        let mut parser = SseEventParser::new();
459        assert!(parser.push(b"data: [DONE]\n").is_empty());
460        assert_eq!(parser.finish(), vec![b"[DONE]".to_vec()]);
461    }
462
463    #[tokio::test]
464    async fn required_done_stream_handles_transport_fragmentation() {
465        let raw: crate::client::transport::SseByteStream = Box::pin(futures_util::stream::iter([
466            Ok(Bytes::from_static(b"data: {\"value\":")),
467            Ok(Bytes::from_static(b"1}\n\ndata: [DO")),
468            Ok(Bytes::from_static(b"NE]\n\n")),
469        ]));
470        let mut stream = decode_required_done_stream(raw, |payload| {
471            serde_json::from_slice::<serde_json::Value>(payload).map_err(ZaiError::from)
472        });
473        assert_eq!(stream.next().await.unwrap().unwrap()["value"], 1);
474        assert!(stream.next().await.is_none());
475    }
476
477    #[tokio::test]
478    async fn required_done_stream_reports_missing_marker_once() {
479        let raw: crate::client::transport::SseByteStream =
480            Box::pin(futures_util::stream::iter([Ok(Bytes::from_static(
481                b"data: {\"value\":1}\n\n",
482            ))]));
483        let mut stream = decode_required_done_stream(raw, |payload| {
484            serde_json::from_slice::<serde_json::Value>(payload).map_err(ZaiError::from)
485        });
486        assert!(stream.next().await.unwrap().is_ok());
487        let error = stream.next().await.unwrap().unwrap_err();
488        assert_eq!(error.code(), Some(codes::SDK_IO));
489        assert!(error.message().contains("[DONE]"));
490        assert!(stream.next().await.is_none());
491    }
492
493    #[tokio::test]
494    async fn required_done_stream_terminates_after_decode_or_business_error() {
495        for body in [
496            b"data: not-json\n\ndata: {\"value\":1}\n\ndata: [DONE]\n\n".as_slice(),
497            b"data: {\"error\":{\"code\":1302,\"message\":\"limited\"}}\n\ndata: [DONE]\n\n"
498                .as_slice(),
499        ] {
500            let raw: crate::client::transport::SseByteStream = Box::pin(
501                futures_util::stream::iter([Ok(Bytes::copy_from_slice(body))]),
502            );
503            let mut stream = decode_required_done_stream(raw, |payload| {
504                serde_json::from_slice::<serde_json::Value>(payload).map_err(ZaiError::from)
505            });
506            assert!(stream.next().await.unwrap().is_err());
507            assert!(stream.next().await.is_none());
508        }
509    }
510}