1use 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::{
21 JsonRpcError, JsonRpcErrorResponse, JsonRpcId, JsonRpcSuccessResponse, JsonRpcVersion,
22};
23
24use crate::streaming::event_queue::{EventQueueReader, InMemoryQueueReader};
25
26pub(crate) const DEFAULT_KEEP_ALIVE: Duration = Duration::from_secs(30);
28
29pub(crate) const DEFAULT_SSE_CHANNEL_CAPACITY: usize = 64;
31
32#[must_use]
36pub fn write_event(event_type: &str, data: &str) -> Bytes {
37 let mut buf = String::with_capacity(event_type.len() + data.len() + 32);
38 buf.push_str("event: ");
39 buf.push_str(event_type);
40 buf.push('\n');
41 for line in data.lines() {
42 buf.push_str("data: ");
43 buf.push_str(line);
44 buf.push('\n');
45 }
46 buf.push('\n');
47 Bytes::from(buf)
48}
49
50std::thread_local! {
58 static SSE_FRAME_BUF: std::cell::RefCell<Vec<u8>> =
59 std::cell::RefCell::new(Vec::with_capacity(1024));
60}
61
62fn build_sse_message_frame<T: serde::Serialize>(value: &T) -> Result<Bytes, serde_json::Error> {
71 SSE_FRAME_BUF.with(|cell| {
72 let mut buf = cell.borrow_mut();
73 buf.clear();
74 buf.extend_from_slice(b"event: message\ndata: ");
75 serde_json::to_writer(&mut *buf, value)?;
76 buf.extend_from_slice(b"\n\n");
77 Ok(Bytes::from(buf.clone()))
78 })
79}
80
81#[must_use]
83pub const fn write_keep_alive() -> Bytes {
84 Bytes::from_static(b": keep-alive\n\n")
85}
86
87#[derive(Debug)]
91pub struct SseBodyWriter {
92 tx: tokio::sync::mpsc::Sender<Result<Frame<Bytes>, Infallible>>,
93}
94
95#[allow(clippy::result_unit_err)]
100impl SseBodyWriter {
101 pub async fn send_event(&self, event_type: &str, data: &str) -> Result<(), ()> {
107 let frame = Frame::data(write_event(event_type, data));
108 self.tx.send(Ok(frame)).await.map_err(|_| ())
109 }
110
111 async fn send_raw_frame(&self, bytes: Bytes) -> Result<(), ()> {
120 let frame = Frame::data(bytes);
121 self.tx.send(Ok(frame)).await.map_err(|_| ())
122 }
123
124 pub async fn send_keep_alive(&self) -> Result<(), ()> {
130 let frame = Frame::data(write_keep_alive());
131 self.tx.send(Ok(frame)).await.map_err(|_| ())
132 }
133
134 #[mutants::skip]
139 pub fn close(self) {
140 drop(self);
141 }
142}
143
144struct ChannelBody {
150 rx: tokio::sync::mpsc::Receiver<Result<Frame<Bytes>, Infallible>>,
151}
152
153impl hyper::body::Body for ChannelBody {
154 type Data = Bytes;
155 type Error = Infallible;
156
157 fn poll_frame(
158 mut self: Pin<&mut Self>,
159 cx: &mut Context<'_>,
160 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
161 self.rx.poll_recv(cx)
162 }
163}
164
165fn stream_error_payload(
175 err: &a2a_protocol_types::error::A2aError,
176 jsonrpc_envelope_id: Option<&JsonRpcId>,
177) -> Result<String, serde_json::Error> {
178 jsonrpc_envelope_id.map_or_else(
179 || serde_json::to_string(err),
180 |id| {
181 let mut jsonrpc_error = JsonRpcError::new(err.code.as_i32(), err.message.clone());
182 jsonrpc_error.data.clone_from(&err.data);
185 serde_json::to_string(&JsonRpcErrorResponse::new(id.clone(), jsonrpc_error))
186 },
187 )
188}
189
190#[must_use]
207#[allow(clippy::too_many_lines)]
208pub fn build_sse_response(
209 mut reader: InMemoryQueueReader,
210 keep_alive_interval: Option<Duration>,
211 channel_capacity: Option<usize>,
212 jsonrpc_envelope_id: Option<JsonRpcId>,
213) -> hyper::Response<http_body_util::combinators::BoxBody<Bytes, Infallible>> {
214 trace_info!("building SSE response stream");
215 let interval = keep_alive_interval.unwrap_or(DEFAULT_KEEP_ALIVE);
216 let cap = channel_capacity.unwrap_or(DEFAULT_SSE_CHANNEL_CAPACITY);
217 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(cap);
218
219 let body_writer = SseBodyWriter { tx };
220
221 tokio::spawn(async move {
222 tokio::task::yield_now().await;
230
231 let keep_alive_deadline = tokio::time::sleep(interval);
240 tokio::pin!(keep_alive_deadline);
241
242 loop {
243 tokio::select! {
244 biased;
245
246 event = reader.read() => {
247 match event {
248 Some(Ok(stream_response)) => {
249 let frame_bytes = if let Some(ref envelope_id) = jsonrpc_envelope_id {
254 let envelope = JsonRpcSuccessResponse {
257 jsonrpc: JsonRpcVersion,
258 id: envelope_id.clone(),
259 result: stream_response,
260 };
261 build_sse_message_frame(&envelope)
262 } else {
263 build_sse_message_frame(&stream_response)
265 };
266 let frame_bytes = match frame_bytes {
267 Ok(b) => b,
268 Err(e) => {
269 let err = a2a_protocol_types::error::A2aError::internal(
274 format!("event serialization failed: {e}"),
275 );
276 if let Ok(data) =
277 stream_error_payload(&err, jsonrpc_envelope_id.as_ref())
278 {
279 let _ = body_writer.send_event("error", &data).await;
280 }
281 break;
282 }
283 };
284 if body_writer.send_raw_frame(frame_bytes).await.is_err() {
285 break;
286 }
287 keep_alive_deadline.as_mut().reset(
289 tokio::time::Instant::now() + interval,
290 );
291 }
292 Some(Err(e)) => {
293 let Ok(data) =
304 stream_error_payload(&e, jsonrpc_envelope_id.as_ref())
305 else {
306 break;
307 };
308 let _ = body_writer.send_event("error", &data).await;
309 break;
310 }
311 None => break,
312 }
313 }
314 () = &mut keep_alive_deadline => {
315 if body_writer.send_keep_alive().await.is_err() {
316 break;
317 }
318 keep_alive_deadline.as_mut().reset(
319 tokio::time::Instant::now() + interval,
320 );
321 }
322 }
323 }
324
325 drop(body_writer);
326 });
327
328 let body = ChannelBody { rx };
329
330 hyper::Response::builder()
331 .status(200)
332 .header("content-type", "text/event-stream")
333 .header("cache-control", "no-cache")
334 .header("transfer-encoding", "chunked")
335 .body(body.boxed())
336 .unwrap_or_else(|_| {
337 hyper::Response::new(
338 http_body_util::Full::new(Bytes::from_static(b"SSE response build error")).boxed(),
339 )
340 })
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
350 fn write_event_single_line_data() {
351 let frame = write_event("message", r#"{"hello":"world"}"#);
352 let expected = "event: message\ndata: {\"hello\":\"world\"}\n\n";
353 assert_eq!(
354 frame,
355 Bytes::from(expected),
356 "single-line data should produce one data: line"
357 );
358 }
359
360 #[test]
361 fn write_event_multiline_data() {
362 let frame = write_event("error", "line1\nline2\nline3");
363 let expected = "event: error\ndata: line1\ndata: line2\ndata: line3\n\n";
364 assert_eq!(
365 frame,
366 Bytes::from(expected),
367 "multiline data should produce separate data: lines"
368 );
369 }
370
371 #[test]
372 fn write_event_empty_data() {
373 let frame = write_event("ping", "");
374 let expected = "event: ping\n\n";
376 assert_eq!(
377 frame,
378 Bytes::from(expected),
379 "empty data should produce no data: lines"
380 );
381 }
382
383 #[test]
384 fn write_event_empty_event_type() {
385 let frame = write_event("", "payload");
386 let expected = "event: \ndata: payload\n\n";
387 assert_eq!(
388 frame,
389 Bytes::from(expected),
390 "empty event type should still produce valid SSE frame"
391 );
392 }
393
394 #[test]
397 fn write_keep_alive_format() {
398 let frame = write_keep_alive();
399 assert_eq!(
400 frame,
401 Bytes::from_static(b": keep-alive\n\n"),
402 "keep-alive should be an SSE comment terminated by double newline"
403 );
404 }
405
406 #[tokio::test]
409 async fn sse_body_writer_send_event_delivers_frame() {
410 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
411 let writer = SseBodyWriter { tx };
412
413 writer
414 .send_event("message", "hello")
415 .await
416 .expect("send_event should succeed while receiver is alive");
417
418 let received = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
424 .await
425 .expect("send_event must deliver a frame; a timeout here means it returned Ok without sending")
426 .expect("channel should still be open");
427 let frame = received.expect("frame result should be Ok");
428 let data = frame.into_data().expect("frame should be a data frame");
429 assert_eq!(
430 data,
431 write_event("message", "hello"),
432 "received frame should match write_event output"
433 );
434 }
435
436 #[tokio::test]
437 async fn sse_body_writer_send_keep_alive_delivers_comment() {
438 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
439 let writer = SseBodyWriter { tx };
440
441 writer
442 .send_keep_alive()
443 .await
444 .expect("send_keep_alive should succeed while receiver is alive");
445
446 let received = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
448 .await
449 .expect("send_keep_alive must deliver a frame; a timeout here means it returned Ok without sending")
450 .expect("channel should still be open");
451 let frame = received.expect("frame result should be Ok");
452 let data = frame.into_data().expect("frame should be a data frame");
453 assert_eq!(
454 data,
455 write_keep_alive(),
456 "should receive keep-alive comment"
457 );
458 }
459
460 #[tokio::test]
461 async fn sse_body_writer_send_fails_after_receiver_dropped() {
462 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
463 let writer = SseBodyWriter { tx };
464 drop(rx);
465
466 let result = writer.send_event("message", "data").await;
467 assert!(
468 result.is_err(),
469 "send_event should return Err after receiver is dropped"
470 );
471 }
472
473 #[tokio::test]
474 async fn sse_body_writer_keep_alive_fails_after_receiver_dropped() {
475 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
476 let writer = SseBodyWriter { tx };
477 drop(rx);
478
479 let result = writer.send_keep_alive().await;
480 assert!(
481 result.is_err(),
482 "send_keep_alive should return Err after receiver is dropped"
483 );
484 }
485
486 #[tokio::test]
487 async fn sse_body_writer_close_drops_sender() {
488 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
489 let writer = SseBodyWriter { tx };
490
491 writer.close();
492
493 let result = rx.recv().await;
494 assert!(
495 result.is_none(),
496 "receiver should return None after writer is closed"
497 );
498 }
499
500 #[tokio::test]
503 async fn build_sse_response_has_correct_headers() {
504 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
505
506 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
507
508 assert_eq!(response.status(), 200, "status should be 200 OK");
509 assert_eq!(
510 response
511 .headers()
512 .get("content-type")
513 .map(hyper::http::HeaderValue::as_bytes),
514 Some(b"text/event-stream".as_slice()),
515 "Content-Type should be text/event-stream"
516 );
517 assert_eq!(
518 response
519 .headers()
520 .get("cache-control")
521 .map(hyper::http::HeaderValue::as_bytes),
522 Some(b"no-cache".as_slice()),
523 "Cache-Control should be no-cache"
524 );
525 assert_eq!(
526 response
527 .headers()
528 .get("transfer-encoding")
529 .map(hyper::http::HeaderValue::as_bytes),
530 Some(b"chunked".as_slice()),
531 "Transfer-Encoding should be chunked"
532 );
533 }
534
535 #[tokio::test]
536 async fn build_sse_response_with_custom_keep_alive_and_capacity() {
537 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
539
540 let response = build_sse_response(
541 reader,
542 Some(Duration::from_secs(5)),
543 Some(16),
544 Some(Some(serde_json::json!(1))),
545 );
546
547 assert_eq!(response.status(), 200);
548 assert_eq!(
549 response
550 .headers()
551 .get("content-type")
552 .map(hyper::http::HeaderValue::as_bytes),
553 Some(b"text/event-stream".as_slice()),
554 );
555 }
556
557 #[tokio::test]
569 async fn build_sse_response_client_disconnect_stops_stream() {
570 use crate::streaming::event_queue::EventQueueWriter;
571 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
572 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
573
574 let event = || {
575 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
576 task_id: TaskId::new("t1"),
577 context_id: ContextId::new("c1"),
578 status: TaskStatus {
579 state: TaskState::Working,
580 message: None,
581 timestamp: None,
582 },
583 metadata: None,
584 })
585 };
586
587 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
588 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
589
590 drop(response);
592 tokio::time::sleep(Duration::from_millis(50)).await;
593
594 writer
597 .write(event())
598 .await
599 .expect("the reader is still subscribed until the loop notices");
600
601 let mut observed_shutdown = false;
605 for _ in 0..200 {
606 tokio::time::sleep(Duration::from_millis(10)).await;
607 if writer.write(event()).await.is_err() {
608 observed_shutdown = true;
609 break;
610 }
611 }
612 assert!(
613 observed_shutdown,
614 "the writer loop never dropped its reader after the client disconnected"
615 );
616 }
617
618 #[tokio::test]
619 async fn build_sse_response_ends_on_reader_close() {
620 use http_body_util::BodyExt;
622
623 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
624
625 drop(writer);
627
628 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
629
630 let frame = response.body_mut().frame().await;
632 if let Some(Ok(_)) = frame {
634 let next = response.body_mut().frame().await;
636 assert!(
637 next.is_none() || matches!(next, Some(Ok(_))),
638 "stream should eventually end"
639 );
640 }
641 }
642
643 #[tokio::test]
644 async fn build_sse_response_streams_error_event() {
645 use a2a_protocol_types::error::A2aError;
647 use http_body_util::BodyExt;
648
649 let (tx, rx) = tokio::sync::broadcast::channel(8);
652 let reader = crate::streaming::event_queue::InMemoryQueueReader::new(rx);
653
654 let err = A2aError::internal("something broke");
655 tx.send(Err(err)).expect("send should succeed");
656 drop(tx);
657
658 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
659
660 let frame = response
661 .body_mut()
662 .frame()
663 .await
664 .expect("should have a frame")
665 .expect("frame should be Ok");
666 let data = frame.into_data().expect("should be a data frame");
667 let text = String::from_utf8_lossy(&data);
668
669 assert!(
670 text.starts_with("event: error\n"),
671 "error event frame should start with 'event: error\\n', got: {text}"
672 );
673 }
674
675 #[tokio::test]
676 async fn build_sse_response_streams_events() {
677 use crate::streaming::event_queue::EventQueueWriter;
678 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
679 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
680 use http_body_util::BodyExt;
681
682 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
683
684 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
685 task_id: TaskId::new("t1"),
686 context_id: ContextId::new("c1"),
687 status: TaskStatus {
688 state: TaskState::Working,
689 message: None,
690 timestamp: None,
691 },
692 metadata: None,
693 });
694
695 writer.write(event).await.expect("write should succeed");
697 drop(writer);
698
699 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
700
701 let frame = response
703 .body_mut()
704 .frame()
705 .await
706 .expect("should have a frame")
707 .expect("frame should be Ok");
708 let data = frame.into_data().expect("should be a data frame");
709 let text = String::from_utf8_lossy(&data);
710
711 assert!(
712 text.starts_with("event: message\n"),
713 "SSE frame should start with 'event: message\\n', got: {text}"
714 );
715 assert!(
716 text.contains("data: "),
717 "SSE frame should contain a data: line"
718 );
719 assert!(
721 text.contains("\"jsonrpc\""),
722 "data should contain JSON-RPC envelope"
723 );
724 assert!(
725 text.contains("\"result\""),
726 "data should contain result field"
727 );
728 let json_part = text
730 .lines()
731 .find_map(|l| l.strip_prefix("data: "))
732 .expect("frame must carry a data line");
733 let envelope: serde_json::Value =
734 serde_json::from_str(json_part).expect("data must be valid JSON");
735 assert_eq!(
736 envelope["id"],
737 serde_json::json!(1),
738 "SSE envelope must echo the request id, got: {envelope}"
739 );
740 }
741
742 #[tokio::test]
745 async fn build_sse_response_echoes_string_request_id() {
746 use crate::streaming::event_queue::EventQueueWriter;
747 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
748 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
749 use http_body_util::BodyExt;
750
751 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
752 writer
753 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
754 task_id: TaskId::new("t1"),
755 context_id: ContextId::new("c1"),
756 status: TaskStatus {
757 state: TaskState::Working,
758 message: None,
759 timestamp: None,
760 },
761 metadata: None,
762 }))
763 .await
764 .expect("write should succeed");
765 drop(writer);
766
767 let mut response =
768 build_sse_response(reader, None, None, Some(Some(serde_json::json!("req-abc"))));
769 let frame = response
770 .body_mut()
771 .frame()
772 .await
773 .expect("should have a frame")
774 .expect("frame should be Ok");
775 let data = frame.into_data().expect("should be a data frame");
776 let text = String::from_utf8_lossy(&data);
777 let json_part = text
778 .lines()
779 .find_map(|l| l.strip_prefix("data: "))
780 .expect("frame must carry a data line");
781 let envelope: serde_json::Value =
782 serde_json::from_str(json_part).expect("data must be valid JSON");
783 assert_eq!(envelope["id"], serde_json::json!("req-abc"));
784 }
785}