a2a_protocol_server/streaming/
sse.rs1use std::convert::Infallible;
12use std::pin::Pin;
13use std::task::{Context, Poll};
14use std::time::Duration;
15
16use bytes::Bytes;
17use http_body_util::BodyExt;
18use hyper::body::Frame;
19
20use a2a_protocol_types::jsonrpc::{JsonRpcId, JsonRpcSuccessResponse, JsonRpcVersion};
21
22use crate::streaming::event_queue::{EventQueueReader, InMemoryQueueReader};
23
24pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(30);
26
27pub(crate) const DEFAULT_SSE_CHANNEL_CAPACITY: usize = 64;
29
30#[must_use]
34pub fn write_event(event_type: &str, data: &str) -> Bytes {
35 let mut buf = String::with_capacity(event_type.len() + data.len() + 32);
36 buf.push_str("event: ");
37 buf.push_str(event_type);
38 buf.push('\n');
39 for line in data.lines() {
40 buf.push_str("data: ");
41 buf.push_str(line);
42 buf.push('\n');
43 }
44 buf.push('\n');
45 Bytes::from(buf)
46}
47
48std::thread_local! {
56 static SSE_FRAME_BUF: std::cell::RefCell<Vec<u8>> =
57 std::cell::RefCell::new(Vec::with_capacity(1024));
58}
59
60fn build_sse_message_frame<T: serde::Serialize>(value: &T) -> Result<Bytes, serde_json::Error> {
69 SSE_FRAME_BUF.with(|cell| {
70 let mut buf = cell.borrow_mut();
71 buf.clear();
72 buf.extend_from_slice(b"event: message\ndata: ");
73 serde_json::to_writer(&mut *buf, value)?;
74 buf.extend_from_slice(b"\n\n");
75 Ok(Bytes::from(buf.clone()))
76 })
77}
78
79#[must_use]
81pub const fn write_keep_alive() -> Bytes {
82 Bytes::from_static(b": keep-alive\n\n")
83}
84
85#[derive(Debug)]
89pub struct SseBodyWriter {
90 tx: tokio::sync::mpsc::Sender<Result<Frame<Bytes>, Infallible>>,
91}
92
93impl SseBodyWriter {
94 pub async fn send_event(&self, event_type: &str, data: &str) -> Result<(), ()> {
100 let frame = Frame::data(write_event(event_type, data));
101 self.tx.send(Ok(frame)).await.map_err(|_| ())
102 }
103
104 async fn send_raw_frame(&self, bytes: Bytes) -> Result<(), ()> {
113 let frame = Frame::data(bytes);
114 self.tx.send(Ok(frame)).await.map_err(|_| ())
115 }
116
117 pub async fn send_keep_alive(&self) -> Result<(), ()> {
123 let frame = Frame::data(write_keep_alive());
124 self.tx.send(Ok(frame)).await.map_err(|_| ())
125 }
126
127 pub fn close(self) {
129 drop(self);
130 }
131}
132
133struct ChannelBody {
139 rx: tokio::sync::mpsc::Receiver<Result<Frame<Bytes>, Infallible>>,
140}
141
142impl hyper::body::Body for ChannelBody {
143 type Data = Bytes;
144 type Error = Infallible;
145
146 fn poll_frame(
147 mut self: Pin<&mut Self>,
148 cx: &mut Context<'_>,
149 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
150 self.rx.poll_recv(cx)
151 }
152}
153
154#[must_use]
171#[allow(clippy::too_many_lines)]
172pub fn build_sse_response(
173 mut reader: InMemoryQueueReader,
174 keep_alive_interval: Option<Duration>,
175 channel_capacity: Option<usize>,
176 jsonrpc_envelope_id: Option<JsonRpcId>,
177) -> hyper::Response<http_body_util::combinators::BoxBody<Bytes, Infallible>> {
178 trace_info!("building SSE response stream");
179 let interval = keep_alive_interval.unwrap_or(DEFAULT_KEEP_ALIVE);
180 let cap = channel_capacity.unwrap_or(DEFAULT_SSE_CHANNEL_CAPACITY);
181 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(cap);
182
183 let body_writer = SseBodyWriter { tx };
184
185 tokio::spawn(async move {
186 tokio::task::yield_now().await;
194
195 let keep_alive_deadline = tokio::time::sleep(interval);
204 tokio::pin!(keep_alive_deadline);
205
206 loop {
207 tokio::select! {
208 biased;
209
210 event = reader.read() => {
211 match event {
212 Some(Ok(stream_response)) => {
213 let frame_bytes = if let Some(ref envelope_id) = jsonrpc_envelope_id {
218 let envelope = JsonRpcSuccessResponse {
221 jsonrpc: JsonRpcVersion,
222 id: envelope_id.clone(),
223 result: stream_response,
224 };
225 build_sse_message_frame(&envelope)
226 } else {
227 build_sse_message_frame(&stream_response)
229 };
230 let frame_bytes = match frame_bytes {
231 Ok(b) => b,
232 Err(e) => {
233 let err_msg = format!("{{\"error\":\"serialization failed: {e}\"}}");
234 let _ = body_writer.send_event("error", &err_msg).await;
235 break;
236 }
237 };
238 if body_writer.send_raw_frame(frame_bytes).await.is_err() {
239 break;
240 }
241 keep_alive_deadline.as_mut().reset(
243 tokio::time::Instant::now() + interval,
244 );
245 }
246 Some(Err(e)) => {
247 let Ok(data) = serde_json::to_string(&e) else {
248 break;
249 };
250 let _ = body_writer.send_event("error", &data).await;
251 break;
252 }
253 None => break,
254 }
255 }
256 () = &mut keep_alive_deadline => {
257 if body_writer.send_keep_alive().await.is_err() {
258 break;
259 }
260 keep_alive_deadline.as_mut().reset(
261 tokio::time::Instant::now() + interval,
262 );
263 }
264 }
265 }
266
267 drop(body_writer);
268 });
269
270 let body = ChannelBody { rx };
271
272 hyper::Response::builder()
273 .status(200)
274 .header("content-type", "text/event-stream")
275 .header("cache-control", "no-cache")
276 .header("transfer-encoding", "chunked")
277 .body(body.boxed())
278 .unwrap_or_else(|_| {
279 hyper::Response::new(
280 http_body_util::Full::new(Bytes::from_static(b"SSE response build error")).boxed(),
281 )
282 })
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
292 fn write_event_single_line_data() {
293 let frame = write_event("message", r#"{"hello":"world"}"#);
294 let expected = "event: message\ndata: {\"hello\":\"world\"}\n\n";
295 assert_eq!(
296 frame,
297 Bytes::from(expected),
298 "single-line data should produce one data: line"
299 );
300 }
301
302 #[test]
303 fn write_event_multiline_data() {
304 let frame = write_event("error", "line1\nline2\nline3");
305 let expected = "event: error\ndata: line1\ndata: line2\ndata: line3\n\n";
306 assert_eq!(
307 frame,
308 Bytes::from(expected),
309 "multiline data should produce separate data: lines"
310 );
311 }
312
313 #[test]
314 fn write_event_empty_data() {
315 let frame = write_event("ping", "");
316 let expected = "event: ping\n\n";
318 assert_eq!(
319 frame,
320 Bytes::from(expected),
321 "empty data should produce no data: lines"
322 );
323 }
324
325 #[test]
326 fn write_event_empty_event_type() {
327 let frame = write_event("", "payload");
328 let expected = "event: \ndata: payload\n\n";
329 assert_eq!(
330 frame,
331 Bytes::from(expected),
332 "empty event type should still produce valid SSE frame"
333 );
334 }
335
336 #[test]
339 fn write_keep_alive_format() {
340 let frame = write_keep_alive();
341 assert_eq!(
342 frame,
343 Bytes::from_static(b": keep-alive\n\n"),
344 "keep-alive should be an SSE comment terminated by double newline"
345 );
346 }
347
348 #[tokio::test]
351 async fn sse_body_writer_send_event_delivers_frame() {
352 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
353 let writer = SseBodyWriter { tx };
354
355 writer
356 .send_event("message", "hello")
357 .await
358 .expect("send_event should succeed while receiver is alive");
359
360 let received = rx.recv().await.expect("should receive a frame");
361 let frame = received.expect("frame result should be Ok");
362 let data = frame.into_data().expect("frame should be a data frame");
363 assert_eq!(
364 data,
365 write_event("message", "hello"),
366 "received frame should match write_event output"
367 );
368 }
369
370 #[tokio::test]
371 async fn sse_body_writer_send_keep_alive_delivers_comment() {
372 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
373 let writer = SseBodyWriter { tx };
374
375 writer
376 .send_keep_alive()
377 .await
378 .expect("send_keep_alive should succeed while receiver is alive");
379
380 let received = rx.recv().await.expect("should receive a frame");
381 let frame = received.expect("frame result should be Ok");
382 let data = frame.into_data().expect("frame should be a data frame");
383 assert_eq!(
384 data,
385 write_keep_alive(),
386 "should receive keep-alive comment"
387 );
388 }
389
390 #[tokio::test]
391 async fn sse_body_writer_send_fails_after_receiver_dropped() {
392 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
393 let writer = SseBodyWriter { tx };
394 drop(rx);
395
396 let result = writer.send_event("message", "data").await;
397 assert!(
398 result.is_err(),
399 "send_event should return Err after receiver is dropped"
400 );
401 }
402
403 #[tokio::test]
404 async fn sse_body_writer_keep_alive_fails_after_receiver_dropped() {
405 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
406 let writer = SseBodyWriter { tx };
407 drop(rx);
408
409 let result = writer.send_keep_alive().await;
410 assert!(
411 result.is_err(),
412 "send_keep_alive should return Err after receiver is dropped"
413 );
414 }
415
416 #[tokio::test]
417 async fn sse_body_writer_close_drops_sender() {
418 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
419 let writer = SseBodyWriter { tx };
420
421 writer.close();
422
423 let result = rx.recv().await;
424 assert!(
425 result.is_none(),
426 "receiver should return None after writer is closed"
427 );
428 }
429
430 #[tokio::test]
433 async fn build_sse_response_has_correct_headers() {
434 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
435
436 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
437
438 assert_eq!(response.status(), 200, "status should be 200 OK");
439 assert_eq!(
440 response
441 .headers()
442 .get("content-type")
443 .map(hyper::http::HeaderValue::as_bytes),
444 Some(b"text/event-stream".as_slice()),
445 "Content-Type should be text/event-stream"
446 );
447 assert_eq!(
448 response
449 .headers()
450 .get("cache-control")
451 .map(hyper::http::HeaderValue::as_bytes),
452 Some(b"no-cache".as_slice()),
453 "Cache-Control should be no-cache"
454 );
455 assert_eq!(
456 response
457 .headers()
458 .get("transfer-encoding")
459 .map(hyper::http::HeaderValue::as_bytes),
460 Some(b"chunked".as_slice()),
461 "Transfer-Encoding should be chunked"
462 );
463 }
464
465 #[tokio::test]
466 async fn build_sse_response_with_custom_keep_alive_and_capacity() {
467 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
469
470 let response = build_sse_response(
471 reader,
472 Some(Duration::from_secs(5)),
473 Some(16),
474 Some(Some(serde_json::json!(1))),
475 );
476
477 assert_eq!(response.status(), 200);
478 assert_eq!(
479 response
480 .headers()
481 .get("content-type")
482 .map(hyper::http::HeaderValue::as_bytes),
483 Some(b"text/event-stream".as_slice()),
484 );
485 }
486
487 #[tokio::test]
488 async fn build_sse_response_client_disconnect_stops_stream() {
489 use crate::streaming::event_queue::EventQueueWriter;
491 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
492 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
493
494 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
495
496 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
497
498 drop(response);
500
501 tokio::time::sleep(Duration::from_millis(50)).await;
503
504 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
507 task_id: TaskId::new("t1"),
508 context_id: ContextId::new("c1"),
509 status: TaskStatus {
510 state: TaskState::Working,
511 message: None,
512 timestamp: None,
513 },
514 metadata: None,
515 });
516 let _ = writer.write(event).await;
518 drop(writer);
519 }
520
521 #[tokio::test]
522 async fn build_sse_response_ends_on_reader_close() {
523 use http_body_util::BodyExt;
525
526 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
527
528 drop(writer);
530
531 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
532
533 let frame = response.body_mut().frame().await;
535 if let Some(Ok(_)) = frame {
537 let next = response.body_mut().frame().await;
539 assert!(
540 next.is_none() || matches!(next, Some(Ok(_))),
541 "stream should eventually end"
542 );
543 }
544 }
545
546 #[tokio::test]
547 async fn build_sse_response_streams_error_event() {
548 use a2a_protocol_types::error::A2aError;
550 use http_body_util::BodyExt;
551
552 let (tx, rx) = tokio::sync::broadcast::channel(8);
555 let reader = crate::streaming::event_queue::InMemoryQueueReader::new(rx);
556
557 let err = A2aError::internal("something broke");
558 tx.send(Err(err)).expect("send should succeed");
559 drop(tx);
560
561 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
562
563 let frame = response
564 .body_mut()
565 .frame()
566 .await
567 .expect("should have a frame")
568 .expect("frame should be Ok");
569 let data = frame.into_data().expect("should be a data frame");
570 let text = String::from_utf8_lossy(&data);
571
572 assert!(
573 text.starts_with("event: error\n"),
574 "error event frame should start with 'event: error\\n', got: {text}"
575 );
576 }
577
578 #[tokio::test]
579 async fn build_sse_response_streams_events() {
580 use crate::streaming::event_queue::EventQueueWriter;
581 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
582 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
583 use http_body_util::BodyExt;
584
585 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
586
587 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
588 task_id: TaskId::new("t1"),
589 context_id: ContextId::new("c1"),
590 status: TaskStatus {
591 state: TaskState::Working,
592 message: None,
593 timestamp: None,
594 },
595 metadata: None,
596 });
597
598 writer.write(event).await.expect("write should succeed");
600 drop(writer);
601
602 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
603
604 let frame = response
606 .body_mut()
607 .frame()
608 .await
609 .expect("should have a frame")
610 .expect("frame should be Ok");
611 let data = frame.into_data().expect("should be a data frame");
612 let text = String::from_utf8_lossy(&data);
613
614 assert!(
615 text.starts_with("event: message\n"),
616 "SSE frame should start with 'event: message\\n', got: {text}"
617 );
618 assert!(
619 text.contains("data: "),
620 "SSE frame should contain a data: line"
621 );
622 assert!(
624 text.contains("\"jsonrpc\""),
625 "data should contain JSON-RPC envelope"
626 );
627 assert!(
628 text.contains("\"result\""),
629 "data should contain result field"
630 );
631 let json_part = text
633 .lines()
634 .find_map(|l| l.strip_prefix("data: "))
635 .expect("frame must carry a data line");
636 let envelope: serde_json::Value =
637 serde_json::from_str(json_part).expect("data must be valid JSON");
638 assert_eq!(
639 envelope["id"],
640 serde_json::json!(1),
641 "SSE envelope must echo the request id, got: {envelope}"
642 );
643 }
644
645 #[tokio::test]
648 async fn build_sse_response_echoes_string_request_id() {
649 use crate::streaming::event_queue::EventQueueWriter;
650 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
651 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
652 use http_body_util::BodyExt;
653
654 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
655 writer
656 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
657 task_id: TaskId::new("t1"),
658 context_id: ContextId::new("c1"),
659 status: TaskStatus {
660 state: TaskState::Working,
661 message: None,
662 timestamp: None,
663 },
664 metadata: None,
665 }))
666 .await
667 .expect("write should succeed");
668 drop(writer);
669
670 let mut response =
671 build_sse_response(reader, None, None, Some(Some(serde_json::json!("req-abc"))));
672 let frame = response
673 .body_mut()
674 .frame()
675 .await
676 .expect("should have a frame")
677 .expect("frame should be Ok");
678 let data = frame.into_data().expect("should be a data frame");
679 let text = String::from_utf8_lossy(&data);
680 let json_part = text
681 .lines()
682 .find_map(|l| l.strip_prefix("data: "))
683 .expect("frame must carry a data line");
684 let envelope: serde_json::Value =
685 serde_json::from_str(json_part).expect("data must be valid JSON");
686 assert_eq!(envelope["id"], serde_json::json!("req-abc"));
687 }
688}