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>(
71 value: &T,
72 seq: Option<u64>,
73) -> Result<Bytes, serde_json::Error> {
74 SSE_FRAME_BUF.with(|cell| {
75 let mut buf = cell.borrow_mut();
76 buf.clear();
77 if let Some(seq) = seq {
87 buf.extend_from_slice(b"id: ");
88 push_decimal(&mut buf, seq);
89 buf.push(b'\n');
90 }
91 buf.extend_from_slice(b"event: message\ndata: ");
92 serde_json::to_writer(&mut *buf, value)?;
93 buf.extend_from_slice(b"\n\n");
94 Ok(Bytes::from(buf.clone()))
95 })
96}
97
98fn push_decimal(buf: &mut Vec<u8>, n: u64) {
106 let mut digits = [0u8; 20]; let mut i = digits.len();
108 let mut n = n;
109 loop {
110 i -= 1;
111 #[allow(clippy::cast_possible_truncation)]
113 {
114 digits[i] = b'0' + (n % 10) as u8;
115 }
116 n /= 10;
117 if n == 0 {
118 break;
119 }
120 }
121 buf.extend_from_slice(&digits[i..]);
122}
123
124#[must_use]
126pub const fn write_keep_alive() -> Bytes {
127 Bytes::from_static(b": keep-alive\n\n")
128}
129
130#[derive(Debug)]
134pub struct SseBodyWriter {
135 tx: tokio::sync::mpsc::Sender<Result<Frame<Bytes>, Infallible>>,
136}
137
138#[allow(clippy::result_unit_err)]
143impl SseBodyWriter {
144 pub async fn send_event(&self, event_type: &str, data: &str) -> Result<(), ()> {
150 let frame = Frame::data(write_event(event_type, data));
151 self.tx.send(Ok(frame)).await.map_err(|_| ())
152 }
153
154 async fn send_raw_frame(&self, bytes: Bytes) -> Result<(), ()> {
163 let frame = Frame::data(bytes);
164 self.tx.send(Ok(frame)).await.map_err(|_| ())
165 }
166
167 pub async fn send_keep_alive(&self) -> Result<(), ()> {
173 let frame = Frame::data(write_keep_alive());
174 self.tx.send(Ok(frame)).await.map_err(|_| ())
175 }
176
177 #[mutants::skip]
182 pub fn close(self) {
183 drop(self);
184 }
185}
186
187struct ChannelBody {
193 rx: tokio::sync::mpsc::Receiver<Result<Frame<Bytes>, Infallible>>,
194}
195
196impl hyper::body::Body for ChannelBody {
197 type Data = Bytes;
198 type Error = Infallible;
199
200 fn poll_frame(
201 mut self: Pin<&mut Self>,
202 cx: &mut Context<'_>,
203 ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
204 self.rx.poll_recv(cx)
205 }
206}
207
208fn stream_error_payload(
218 err: &a2a_protocol_types::error::A2aError,
219 jsonrpc_envelope_id: Option<&JsonRpcId>,
220) -> Result<String, serde_json::Error> {
221 jsonrpc_envelope_id.map_or_else(
222 || serde_json::to_string(&rest_stream_error(err)),
223 |id| {
224 let mut jsonrpc_error = JsonRpcError::new(err.code.as_i32(), err.message.clone());
225 jsonrpc_error.data.clone_from(&err.data);
228 serde_json::to_string(&JsonRpcErrorResponse::new(id.clone(), jsonrpc_error))
229 },
230 )
231}
232
233const STRUCT_DETAIL_TYPE: &str = "type.googleapis.com/google.protobuf.Struct";
235
236fn rest_stream_error(err: &a2a_protocol_types::error::A2aError) -> serde_json::Value {
253 let mut details = match err.error_info_data(None) {
254 serde_json::Value::Array(items) => items,
255 _ => Vec::new(),
256 };
257 if let Some(data) = &err.data {
258 let mut detail = match data {
259 serde_json::Value::Object(fields) => fields.clone(),
260 other => {
261 let mut fields = serde_json::Map::new();
262 fields.insert("value".into(), other.clone());
263 fields
264 }
265 };
266 detail.insert("@type".into(), STRUCT_DETAIL_TYPE.into());
267 details.push(serde_json::Value::Object(detail));
268 }
269 let mut status = serde_json::json!({
270 "code": err.code.http_status(),
271 "status": err.code.grpc_status(),
272 "message": err.message,
273 });
274 if !details.is_empty() {
275 status["details"] = serde_json::Value::Array(details);
276 }
277 serde_json::json!({ "error": status })
278}
279
280#[must_use]
297#[allow(clippy::too_many_lines)]
298pub fn build_sse_response(
299 mut reader: InMemoryQueueReader,
300 keep_alive_interval: Option<Duration>,
301 channel_capacity: Option<usize>,
302 jsonrpc_envelope_id: Option<JsonRpcId>,
303) -> hyper::Response<http_body_util::combinators::BoxBody<Bytes, Infallible>> {
304 trace_info!("building SSE response stream");
305 let interval = keep_alive_interval.unwrap_or(DEFAULT_KEEP_ALIVE);
306 let cap = channel_capacity.unwrap_or(DEFAULT_SSE_CHANNEL_CAPACITY);
307 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(cap);
308
309 let body_writer = SseBodyWriter { tx };
310
311 tokio::spawn(crate::rpc_span::in_child_span("a2a.sse", async move {
312 tokio::task::yield_now().await;
320
321 let keep_alive_deadline = tokio::time::sleep(interval);
330 tokio::pin!(keep_alive_deadline);
331
332 loop {
333 tokio::select! {
334 biased;
335
336 event = reader.read() => {
337 match event {
338 Some(Ok(queued)) => {
339 let seq = queued.seq;
340 let stream_response = queued.event;
341 let frame_bytes = if let Some(ref envelope_id) = jsonrpc_envelope_id {
346 let envelope = JsonRpcSuccessResponse {
349 jsonrpc: JsonRpcVersion,
350 id: envelope_id.clone(),
351 result: stream_response,
352 };
353 build_sse_message_frame(&envelope, seq)
354 } else {
355 build_sse_message_frame(&stream_response, seq)
357 };
358 let frame_bytes = match frame_bytes {
359 Ok(b) => b,
360 Err(e) => {
361 let err = a2a_protocol_types::error::A2aError::internal(
366 format!("event serialization failed: {e}"),
367 );
368 if let Ok(data) =
369 stream_error_payload(&err, jsonrpc_envelope_id.as_ref())
370 {
371 let _ = body_writer.send_event("error", &data).await;
372 }
373 break;
374 }
375 };
376 if body_writer.send_raw_frame(frame_bytes).await.is_err() {
377 break;
378 }
379 keep_alive_deadline.as_mut().reset(
381 tokio::time::Instant::now() + interval,
382 );
383 }
384 Some(Err(e)) => {
385 let Ok(data) =
396 stream_error_payload(&e, jsonrpc_envelope_id.as_ref())
397 else {
398 break;
399 };
400 let _ = body_writer.send_event("error", &data).await;
401 break;
402 }
403 None => break,
404 }
405 }
406 () = &mut keep_alive_deadline => {
407 if body_writer.send_keep_alive().await.is_err() {
408 break;
409 }
410 keep_alive_deadline.as_mut().reset(
411 tokio::time::Instant::now() + interval,
412 );
413 }
414 }
415 }
416
417 drop(body_writer);
418 }));
419
420 let body = ChannelBody { rx };
421
422 hyper::Response::builder()
423 .status(200)
424 .header("content-type", "text/event-stream")
425 .header("cache-control", "no-cache")
426 .header("transfer-encoding", "chunked")
427 .body(body.boxed())
428 .unwrap_or_else(|_| {
429 hyper::Response::new(
430 http_body_util::Full::new(Bytes::from_static(b"SSE response build error")).boxed(),
431 )
432 })
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
442 fn write_event_single_line_data() {
443 let frame = write_event("message", r#"{"hello":"world"}"#);
444 let expected = "event: message\ndata: {\"hello\":\"world\"}\n\n";
445 assert_eq!(
446 frame,
447 Bytes::from(expected),
448 "single-line data should produce one data: line"
449 );
450 }
451
452 #[test]
453 fn write_event_multiline_data() {
454 let frame = write_event("error", "line1\nline2\nline3");
455 let expected = "event: error\ndata: line1\ndata: line2\ndata: line3\n\n";
456 assert_eq!(
457 frame,
458 Bytes::from(expected),
459 "multiline data should produce separate data: lines"
460 );
461 }
462
463 #[test]
464 fn write_event_empty_data() {
465 let frame = write_event("ping", "");
466 let expected = "event: ping\n\n";
468 assert_eq!(
469 frame,
470 Bytes::from(expected),
471 "empty data should produce no data: lines"
472 );
473 }
474
475 #[test]
476 fn write_event_empty_event_type() {
477 let frame = write_event("", "payload");
478 let expected = "event: \ndata: payload\n\n";
479 assert_eq!(
480 frame,
481 Bytes::from(expected),
482 "empty event type should still produce valid SSE frame"
483 );
484 }
485
486 #[test]
489 fn write_keep_alive_format() {
490 let frame = write_keep_alive();
491 assert_eq!(
492 frame,
493 Bytes::from_static(b": keep-alive\n\n"),
494 "keep-alive should be an SSE comment terminated by double newline"
495 );
496 }
497
498 #[tokio::test]
501 async fn sse_body_writer_send_event_delivers_frame() {
502 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
503 let writer = SseBodyWriter { tx };
504
505 writer
506 .send_event("message", "hello")
507 .await
508 .expect("send_event should succeed while receiver is alive");
509
510 let received = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
516 .await
517 .expect("send_event must deliver a frame; a timeout here means it returned Ok without sending")
518 .expect("channel should still be open");
519 let frame = received.expect("frame result should be Ok");
520 let data = frame.into_data().expect("frame should be a data frame");
521 assert_eq!(
522 data,
523 write_event("message", "hello"),
524 "received frame should match write_event output"
525 );
526 }
527
528 #[tokio::test]
529 async fn sse_body_writer_send_keep_alive_delivers_comment() {
530 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
531 let writer = SseBodyWriter { tx };
532
533 writer
534 .send_keep_alive()
535 .await
536 .expect("send_keep_alive should succeed while receiver is alive");
537
538 let received = tokio::time::timeout(std::time::Duration::from_secs(5), rx.recv())
540 .await
541 .expect("send_keep_alive must deliver a frame; a timeout here means it returned Ok without sending")
542 .expect("channel should still be open");
543 let frame = received.expect("frame result should be Ok");
544 let data = frame.into_data().expect("frame should be a data frame");
545 assert_eq!(
546 data,
547 write_keep_alive(),
548 "should receive keep-alive comment"
549 );
550 }
551
552 #[tokio::test]
553 async fn sse_body_writer_send_fails_after_receiver_dropped() {
554 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
555 let writer = SseBodyWriter { tx };
556 drop(rx);
557
558 let result = writer.send_event("message", "data").await;
559 assert!(
560 result.is_err(),
561 "send_event should return Err after receiver is dropped"
562 );
563 }
564
565 #[tokio::test]
566 async fn sse_body_writer_keep_alive_fails_after_receiver_dropped() {
567 let (tx, rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
568 let writer = SseBodyWriter { tx };
569 drop(rx);
570
571 let result = writer.send_keep_alive().await;
572 assert!(
573 result.is_err(),
574 "send_keep_alive should return Err after receiver is dropped"
575 );
576 }
577
578 #[tokio::test]
579 async fn sse_body_writer_close_drops_sender() {
580 let (tx, mut rx) = tokio::sync::mpsc::channel::<Result<Frame<Bytes>, Infallible>>(8);
581 let writer = SseBodyWriter { tx };
582
583 writer.close();
584
585 let result = rx.recv().await;
586 assert!(
587 result.is_none(),
588 "receiver should return None after writer is closed"
589 );
590 }
591
592 #[tokio::test]
595 async fn build_sse_response_has_correct_headers() {
596 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
597
598 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
599
600 assert_eq!(response.status(), 200, "status should be 200 OK");
601 assert_eq!(
602 response
603 .headers()
604 .get("content-type")
605 .map(hyper::http::HeaderValue::as_bytes),
606 Some(b"text/event-stream".as_slice()),
607 "Content-Type should be text/event-stream"
608 );
609 assert_eq!(
610 response
611 .headers()
612 .get("cache-control")
613 .map(hyper::http::HeaderValue::as_bytes),
614 Some(b"no-cache".as_slice()),
615 "Cache-Control should be no-cache"
616 );
617 assert_eq!(
618 response
619 .headers()
620 .get("transfer-encoding")
621 .map(hyper::http::HeaderValue::as_bytes),
622 Some(b"chunked".as_slice()),
623 "Transfer-Encoding should be chunked"
624 );
625 }
626
627 #[tokio::test]
628 async fn build_sse_response_with_custom_keep_alive_and_capacity() {
629 let (_writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
631
632 let response = build_sse_response(
633 reader,
634 Some(Duration::from_secs(5)),
635 Some(16),
636 Some(Some(serde_json::json!(1))),
637 );
638
639 assert_eq!(response.status(), 200);
640 assert_eq!(
641 response
642 .headers()
643 .get("content-type")
644 .map(hyper::http::HeaderValue::as_bytes),
645 Some(b"text/event-stream".as_slice()),
646 );
647 }
648
649 #[tokio::test]
661 async fn build_sse_response_client_disconnect_stops_stream() {
662 use crate::streaming::event_queue::EventQueueWriter;
663 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
664 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
665
666 let event = || {
667 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
668 task_id: TaskId::new("t1"),
669 context_id: ContextId::new("c1"),
670 status: TaskStatus {
671 state: TaskState::Working,
672 message: None,
673 timestamp: None,
674 },
675 metadata: None,
676 })
677 };
678
679 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
680 let response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
681
682 drop(response);
684 tokio::time::sleep(Duration::from_millis(50)).await;
685
686 writer
689 .write(event())
690 .await
691 .expect("the reader is still subscribed until the loop notices");
692
693 let mut observed_shutdown = false;
697 for _ in 0..200 {
698 tokio::time::sleep(Duration::from_millis(10)).await;
699 if writer.write(event()).await.is_err() {
700 observed_shutdown = true;
701 break;
702 }
703 }
704 assert!(
705 observed_shutdown,
706 "the writer loop never dropped its reader after the client disconnected"
707 );
708 }
709
710 #[tokio::test]
711 async fn build_sse_response_ends_on_reader_close() {
712 use http_body_util::BodyExt;
714
715 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
716
717 drop(writer);
719
720 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
721
722 let frame = response.body_mut().frame().await;
724 if let Some(Ok(_)) = frame {
726 let next = response.body_mut().frame().await;
728 assert!(
729 next.is_none() || matches!(next, Some(Ok(_))),
730 "stream should eventually end"
731 );
732 }
733 }
734
735 #[tokio::test]
736 async fn build_sse_response_streams_error_event() {
737 use a2a_protocol_types::error::A2aError;
739 use http_body_util::BodyExt;
740
741 let (tx, rx) = tokio::sync::broadcast::channel(8);
744 let reader = crate::streaming::event_queue::InMemoryQueueReader::new(rx);
745
746 let err = A2aError::internal("something broke");
747 tx.send(Err(err)).expect("send should succeed");
748 drop(tx);
749
750 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
751
752 let frame = response
753 .body_mut()
754 .frame()
755 .await
756 .expect("should have a frame")
757 .expect("frame should be Ok");
758 let data = frame.into_data().expect("should be a data frame");
759 let text = String::from_utf8_lossy(&data);
760
761 assert!(
762 text.starts_with("event: error\n"),
763 "error event frame should start with 'event: error\\n', got: {text}"
764 );
765 }
766
767 #[tokio::test]
768 async fn build_sse_response_streams_events() {
769 use crate::streaming::event_queue::EventQueueWriter;
770 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
771 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
772 use http_body_util::BodyExt;
773
774 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
775
776 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
777 task_id: TaskId::new("t1"),
778 context_id: ContextId::new("c1"),
779 status: TaskStatus {
780 state: TaskState::Working,
781 message: None,
782 timestamp: None,
783 },
784 metadata: None,
785 });
786
787 writer.write(event).await.expect("write should succeed");
789 drop(writer);
790
791 let mut response = build_sse_response(reader, None, None, Some(Some(serde_json::json!(1))));
792
793 let frame = response
795 .body_mut()
796 .frame()
797 .await
798 .expect("should have a frame")
799 .expect("frame should be Ok");
800 let data = frame.into_data().expect("should be a data frame");
801 let text = String::from_utf8_lossy(&data);
802
803 assert!(
807 text.starts_with("id: 1\nevent: message\n"),
808 "SSE frame should open with its log position then the event line, got: {text}"
809 );
810 assert!(
811 text.contains("data: "),
812 "SSE frame should contain a data: line"
813 );
814 assert!(
816 text.contains("\"jsonrpc\""),
817 "data should contain JSON-RPC envelope"
818 );
819 assert!(
820 text.contains("\"result\""),
821 "data should contain result field"
822 );
823 let json_part = text
825 .lines()
826 .find_map(|l| l.strip_prefix("data: "))
827 .expect("frame must carry a data line");
828 let envelope: serde_json::Value =
829 serde_json::from_str(json_part).expect("data must be valid JSON");
830 assert_eq!(
831 envelope["id"],
832 serde_json::json!(1),
833 "SSE envelope must echo the request id, got: {envelope}"
834 );
835 }
836
837 #[tokio::test]
840 async fn build_sse_response_echoes_string_request_id() {
841 use crate::streaming::event_queue::EventQueueWriter;
842 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
843 use a2a_protocol_types::task::{ContextId, TaskId, TaskState, TaskStatus};
844 use http_body_util::BodyExt;
845
846 let (writer, reader) = crate::streaming::event_queue::new_in_memory_queue();
847 writer
848 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
849 task_id: TaskId::new("t1"),
850 context_id: ContextId::new("c1"),
851 status: TaskStatus {
852 state: TaskState::Working,
853 message: None,
854 timestamp: None,
855 },
856 metadata: None,
857 }))
858 .await
859 .expect("write should succeed");
860 drop(writer);
861
862 let mut response =
863 build_sse_response(reader, None, None, Some(Some(serde_json::json!("req-abc"))));
864 let frame = response
865 .body_mut()
866 .frame()
867 .await
868 .expect("should have a frame")
869 .expect("frame should be Ok");
870 let data = frame.into_data().expect("should be a data frame");
871 let text = String::from_utf8_lossy(&data);
872 let json_part = text
873 .lines()
874 .find_map(|l| l.strip_prefix("data: "))
875 .expect("frame must carry a data line");
876 let envelope: serde_json::Value =
877 serde_json::from_str(json_part).expect("data must be valid JSON");
878 assert_eq!(envelope["id"], serde_json::json!("req-abc"));
879 }
880
881 #[test]
888 fn a_positioned_event_carries_its_id_before_the_event_line() {
889 let frame =
890 build_sse_message_frame(&serde_json::json!({"a": 1}), Some(42)).expect("serialize");
891 assert_eq!(
892 String::from_utf8_lossy(&frame),
893 "id: 42\nevent: message\ndata: {\"a\":1}\n\n"
894 );
895 }
896
897 #[test]
902 fn an_unpositioned_event_carries_no_id_at_all() {
903 let frame = build_sse_message_frame(&serde_json::json!({"a": 1}), None).expect("serialize");
904 let text = String::from_utf8_lossy(&frame);
905 assert!(!text.contains("id:"), "no id line: {text}");
906 assert!(text.starts_with("event: message\n"), "{text}");
907 }
908
909 #[test]
913 fn the_reused_buffer_does_not_carry_an_id_into_the_next_frame() {
914 let _ = build_sse_message_frame(&serde_json::json!({"a": 1}), Some(7)).expect("first");
915 let second = build_sse_message_frame(&serde_json::json!({"a": 2}), None).expect("second");
916 assert!(
917 !String::from_utf8_lossy(&second).contains("id:"),
918 "the second frame must not inherit the first frame's id"
919 );
920 }
921
922 #[test]
923 fn positions_are_formatted_across_the_whole_u64_range() {
924 for (n, expected) in [
928 (0_u64, "0"),
929 (7, "7"),
930 (10, "10"),
931 (1_234_567_890, "1234567890"),
932 (u64::MAX, "18446744073709551615"),
933 ] {
934 let mut buf = Vec::new();
935 push_decimal(&mut buf, n);
936 assert_eq!(String::from_utf8_lossy(&buf), expected);
937 }
938 }
939
940 #[test]
943 fn rest_stream_error_for_an_a2a_error_carries_its_error_info_only() {
944 use a2a_protocol_types::error::A2aError;
945 let v = rest_stream_error(&A2aError::task_not_found("t-1"));
946 assert_eq!(
947 v,
948 serde_json::json!({"error": {
949 "code": 404,
950 "status": "NOT_FOUND",
951 "message": "Task not found: t-1",
952 "details": [{
953 "@type": "type.googleapis.com/google.rpc.ErrorInfo",
954 "reason": "TASK_NOT_FOUND",
955 "domain": "a2a-protocol.org"
956 }]
957 }})
958 );
959 }
960
961 #[test]
962 fn rest_stream_error_without_details_omits_the_member() {
963 use a2a_protocol_types::error::A2aError;
964 let v = rest_stream_error(&A2aError::internal("boom"));
965 assert_eq!(
966 v,
967 serde_json::json!({"error": {"code": 500, "status": "INTERNAL", "message": "boom"}})
968 );
969 }
970
971 #[test]
972 fn rest_stream_error_wraps_non_object_data_under_value() {
973 use a2a_protocol_types::error::{A2aError, ErrorCode};
974 let v = rest_stream_error(&A2aError::with_data(
975 ErrorCode::InternalError,
976 "boom",
977 serde_json::json!([1, 2]),
978 ));
979 assert_eq!(
980 v["error"]["details"],
981 serde_json::json!([{
982 "@type": "type.googleapis.com/google.protobuf.Struct",
983 "value": [1, 2]
984 }])
985 );
986 }
987}