Skip to content

Commit c1563e1

Browse files
committed
fix: handle SSE comment lines safely
1 parent d3176f8 commit c1563e1

1 file changed

Lines changed: 39 additions & 11 deletions

File tree

crates/rmcp/src/transport/common/client_side_sse.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,19 @@ enum BoundedSseStreamError {
2828
#[derive(Debug)]
2929
struct SseEventSizeLimiter {
3030
max_size: usize,
31-
event_size: usize,
31+
retained_size: usize,
3232
line_size: usize,
33+
line_is_comment: bool,
3334
previous_was_cr: bool,
3435
}
3536

3637
impl SseEventSizeLimiter {
3738
fn new(max_size: usize) -> Self {
3839
Self {
3940
max_size,
40-
event_size: 0,
41+
retained_size: 0,
4142
line_size: 0,
43+
line_is_comment: false,
4244
previous_was_cr: false,
4345
}
4446
}
@@ -59,8 +61,10 @@ impl SseEventSizeLimiter {
5961
}
6062
b'\n' => self.finish_line()?,
6163
_ => {
64+
if self.line_size == 0 {
65+
self.line_is_comment = byte == b':';
66+
}
6267
self.line_size = self.line_size.saturating_add(1);
63-
self.event_size = self.event_size.saturating_add(1);
6468
self.check_limit()?;
6569
}
6670
}
@@ -70,18 +74,21 @@ impl SseEventSizeLimiter {
7074

7175
fn finish_line(&mut self) -> Result<(), ()> {
7276
if self.line_size == 0 {
73-
self.event_size = 0;
74-
} else {
77+
self.retained_size = 0;
78+
} else if !self.line_is_comment {
7579
// The SSE parser inserts a newline when joining multiple data fields.
76-
self.event_size = self.event_size.saturating_add(1);
77-
self.check_limit()?;
78-
self.line_size = 0;
80+
self.retained_size = self
81+
.retained_size
82+
.saturating_add(self.line_size)
83+
.saturating_add(1);
7984
}
80-
Ok(())
85+
self.line_size = 0;
86+
self.line_is_comment = false;
87+
self.check_limit()
8188
}
8289

8390
fn check_limit(&self) -> Result<(), ()> {
84-
if self.event_size > self.max_size {
91+
if self.retained_size.saturating_add(self.line_size) > self.max_size {
8592
Err(())
8693
} else {
8794
Ok(())
@@ -270,7 +277,8 @@ pub(crate) trait SseStreamReconnect {
270277
tracing::warn!("sse stream error: {error}");
271278
}
272279
}
273-
fn map_fatal_stream_error(&mut self, _error: SseError) -> Option<Self::Error> {
280+
fn map_fatal_stream_error(&mut self, error: SseError) -> Option<Self::Error> {
281+
tracing::warn!("fatal sse stream error: {error}");
274282
None
275283
}
276284
}
@@ -589,6 +597,26 @@ mod tests {
589597
assert_eq!(second.data.as_deref(), Some("world"));
590598
}
591599

600+
#[tokio::test]
601+
async fn bounded_sse_stream_discards_completed_comment_lines_from_limit() {
602+
let source = futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(
603+
b": ping\n: ping\n: ping\n",
604+
))]);
605+
let mut stream = bounded_sse_stream(source, 6);
606+
607+
assert!(stream.next().await.is_none());
608+
}
609+
610+
#[tokio::test]
611+
async fn bounded_sse_stream_comments_do_not_reset_accumulated_data() {
612+
let source = futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(
613+
b"data: a\n: ping\ndata: b\n",
614+
))]);
615+
let mut stream = bounded_sse_stream(source, 14);
616+
617+
assert!(stream.next().await.unwrap().is_err());
618+
}
619+
592620
#[tokio::test]
593621
async fn bounded_sse_stream_counts_multiline_data_join_newlines() {
594622
let source = futures::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(

0 commit comments

Comments
 (0)