1use std::collections::HashMap;
37use std::net::SocketAddr;
38use std::sync::Arc;
39use std::time::Duration;
40
41use futures_util::stream::SplitSink;
42use futures_util::{SinkExt, StreamExt};
43use tokio::net::{TcpListener, TcpStream};
44use tokio_tungstenite::WebSocketStream;
45use tokio_tungstenite::tungstenite::Message as WsMessage;
46use tokio_tungstenite::tungstenite::handshake::server::{
47 ErrorResponse, Request as WsUpgradeRequest, Response as WsUpgradeResponse,
48};
49
50use a2a_protocol_types::jsonrpc::{
51 JsonRpcError, JsonRpcErrorResponse, JsonRpcId, JsonRpcRequest, JsonRpcSuccessResponse,
52 JsonRpcVersion,
53};
54
55use crate::error::ServerError;
56use crate::handler::{RequestHandler, SendMessageResult};
57use crate::streaming::EventQueueReader;
58
59mod shutdown;
60
61const MAX_WS_MESSAGE_SIZE: usize = 4 * 1024 * 1024;
70
71const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
78
79const CLOSE_TIMEOUT: Duration = Duration::from_secs(1);
83
84const MAX_REQUESTS_PER_CONNECTION: u32 = 64;
87
88pub const DEFAULT_WS_IDLE_TIMEOUT: Duration = Duration::from_secs(75);
100
101pub struct WebSocketDispatcher {
121 handler: Arc<RequestHandler>,
122 handshake_timeout: Duration,
123 require_version_header: bool,
124 max_connections: Option<usize>,
125 idle_timeout: Option<Duration>,
126 completion_grace: Duration,
127 task_grace: Duration,
128 drain_timeout: Duration,
129}
130
131#[derive(Default)]
134struct Connections {
135 closing: tokio_util::sync::CancellationToken,
138 tracker: tokio_util::task::TaskTracker,
140 accepted: std::sync::atomic::AtomicU64,
141}
142
143impl WebSocketDispatcher {
144 #[must_use]
146 pub const fn new(handler: Arc<RequestHandler>) -> Self {
147 Self {
148 handler,
149 handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
150 require_version_header: true,
151 max_connections: None,
152 idle_timeout: None,
153 completion_grace: crate::serve::DEFAULT_COMPLETION_GRACE,
154 task_grace: crate::serve::DEFAULT_TASK_GRACE,
155 drain_timeout: crate::serve::DEFAULT_DRAIN_TIMEOUT,
156 }
157 }
158
159 #[must_use]
166 pub const fn accept_missing_version_header(mut self) -> Self {
167 self.require_version_header = false;
168 self
169 }
170
171 #[must_use]
176 pub const fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
177 self.handshake_timeout = timeout;
178 self
179 }
180
181 #[must_use]
202 pub const fn with_max_connections(mut self, max: usize) -> Self {
203 self.max_connections = Some(max);
204 self
205 }
206
207 #[must_use]
238 pub const fn with_idle_timeout(mut self, timeout: Duration) -> Self {
239 self.idle_timeout = Some(timeout);
240 self
241 }
242
243 #[must_use]
251 pub const fn with_completion_grace(mut self, grace: Duration) -> Self {
252 self.completion_grace = grace;
253 self
254 }
255
256 #[must_use]
260 pub const fn with_task_grace(mut self, grace: Duration) -> Self {
261 self.task_grace = grace;
262 self
263 }
264
265 #[must_use]
269 pub const fn with_drain_timeout(mut self, timeout: Duration) -> Self {
270 self.drain_timeout = timeout;
271 self
272 }
273
274 pub async fn serve(
288 self: Arc<Self>,
289 addr: impl tokio::net::ToSocketAddrs,
290 ) -> std::io::Result<()> {
291 let listener = TcpListener::bind(addr).await?;
292
293 trace_info!(
294 addr = %listener.local_addr().unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0))),
295 "A2A WebSocket server listening"
296 );
297
298 self.accept_loop(listener, std::future::pending(), &Connections::default())
299 .await;
300 Ok(())
301 }
302
303 pub async fn serve_with_addr(
311 self: Arc<Self>,
312 addr: impl tokio::net::ToSocketAddrs,
313 ) -> std::io::Result<SocketAddr> {
314 let listener = TcpListener::bind(addr).await?;
315 let local_addr = listener.local_addr()?;
316
317 trace_info!(%local_addr, "A2A WebSocket server listening");
318
319 tokio::spawn(async move {
320 self.accept_loop(listener, std::future::pending(), &Connections::default())
321 .await;
322 });
323
324 Ok(local_addr)
325 }
326
327 async fn accept_loop(
330 self: Arc<Self>,
331 listener: TcpListener,
332 stop: impl std::future::Future<Output = ()>,
333 conns: &Connections,
334 ) {
335 let mut stop = std::pin::pin!(stop);
336 let limiter = Arc::new(tokio::sync::Semaphore::new(
340 self.max_connections
341 .unwrap_or(tokio::sync::Semaphore::MAX_PERMITS),
342 ));
343 loop {
344 let Ok(permit) = (tokio::select! {
349 biased;
350 () = &mut stop => return,
351 acquired = Arc::clone(&limiter).acquire_owned() => acquired,
352 }) else {
353 return;
357 };
358 let accept = tokio::select! {
359 biased;
360 () = &mut stop => return,
361 accept = listener.accept() => accept,
362 };
363 let (stream, _peer) = match accept {
364 Ok(pair) => pair,
365 Err(e) => {
366 trace_warn!(error = %e, "accept() failed; retrying");
370 let backoff = crate::serve::accept_retry_backoff(&e);
371 tokio::time::sleep(backoff).await;
375 continue;
376 }
377 };
378 conns
379 .accepted
380 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
381 let dispatcher = Arc::clone(&self);
382 let closing = conns.closing.clone();
383 conns.tracker.spawn(async move {
384 trace_debug!("WebSocket connection accepted");
385 if let Err(_e) = dispatcher.handle_connection(stream, &closing).await {
386 trace_warn!(error = %_e, "WebSocket connection error");
387 }
388 drop(permit);
392 });
393 }
394 }
395
396 #[allow(clippy::result_large_err)]
400 async fn handle_connection(
401 &self,
402 stream: TcpStream,
403 closing: &tokio_util::sync::CancellationToken,
404 ) -> Result<(), WsError> {
405 let _ = stream.set_nodelay(true);
408
409 let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
412 .max_message_size(Some(MAX_WS_MESSAGE_SIZE))
413 .max_frame_size(Some(MAX_WS_MESSAGE_SIZE));
414
415 let mut upgrade_headers: Option<HashMap<String, String>> = None;
419 let require_version = self.require_version_header;
420 let callback = |req: &WsUpgradeRequest, resp: WsUpgradeResponse| {
421 check_a2a_version(req, require_version)?;
422 upgrade_headers = Some(extract_upgrade_headers(req));
423 Ok(resp)
424 };
425
426 let ws_stream = tokio::time::timeout(
429 self.handshake_timeout,
430 tokio_tungstenite::accept_hdr_async_with_config(stream, callback, Some(ws_config)),
431 )
432 .await
433 .map_err(|_| WsError::HandshakeTimeout)?
434 .map_err(WsError::Handshake)?;
435
436 let headers = Arc::new(upgrade_headers.unwrap_or_default());
437
438 let (writer, reader) = ws_stream.split();
439 let writer: WsSink = Arc::new(Connection {
440 sink: tokio::sync::Mutex::new(writer),
441 activity: ActivityClock::new(),
442 });
443
444 self.read_loop(reader, &writer, &headers, closing).await;
445
446 let _ = tokio::time::timeout(CLOSE_TIMEOUT, async {
451 let mut w = writer.sink.lock().await;
452 let _ = w.close().await;
453 drop(w);
454 })
455 .await;
456
457 Ok(())
458 }
459
460 async fn read_loop(
462 &self,
463 mut reader: futures_util::stream::SplitStream<WebSocketStream<TcpStream>>,
464 writer: &WsSink,
465 headers: &Arc<HashMap<String, String>>,
466 closing: &tokio_util::sync::CancellationToken,
467 ) {
468 let semaphore = Arc::new(tokio::sync::Semaphore::new(
470 MAX_REQUESTS_PER_CONNECTION as usize,
471 ));
472 let peer_gone = tokio_util::sync::CancellationToken::new();
477 let _cancel_on_exit = peer_gone.clone().drop_guard();
478
479 writer.activity.touch();
483 loop {
484 let next = tokio::select! {
487 biased;
488 () = closing.cancelled() => {
489 let _ = semaphore.acquire_many(MAX_REQUESTS_PER_CONNECTION).await;
493 return;
494 }
495 next = Self::next_frame(&mut reader, writer, self.idle_timeout) => next,
496 };
497 let Some(msg) = next else { break };
498 writer.activity.touch();
499 match msg {
500 Ok(WsMessage::Text(text)) => {
501 let Ok(permit) = semaphore.clone().try_acquire_owned() else {
522 let err_resp = JsonRpcErrorResponse::new(
527 best_effort_request_id(&text),
528 JsonRpcError::new(
529 -32000,
530 "server busy: too many concurrent requests".to_string(),
531 ),
532 );
533 send_json(writer, &err_resp).await;
534 continue;
535 };
536
537 let writer = Arc::clone(writer);
538 let handler = Arc::clone(&self.handler);
539 let headers = Arc::clone(headers);
540 let peer_gone = peer_gone.clone();
541 tokio::spawn(async move {
542 tokio::select! {
544 () = Box::pin(process_ws_message(&handler, &text, writer, &headers)) => {}
545 () = peer_gone.cancelled() => {}
546 }
547 drop(permit); });
549 }
550 Ok(WsMessage::Binary(_)) => {
551 let err_resp = JsonRpcErrorResponse::new(
555 None,
556 JsonRpcError::new(
557 -32700,
558 "binary frames are not supported; send JSON-RPC as text frames"
559 .to_string(),
560 ),
561 );
562 send_json(writer, &err_resp).await;
563 }
564 Ok(WsMessage::Close(_)) | Err(_) => break,
565 Ok(_) => {}
570 }
571 }
572 }
573
574 async fn next_frame(
586 reader: &mut futures_util::stream::SplitStream<WebSocketStream<TcpStream>>,
587 writer: &WsSink,
588 idle_timeout: Option<Duration>,
589 ) -> Option<Result<WsMessage, tokio_tungstenite::tungstenite::Error>> {
590 let Some(idle) = idle_timeout else {
591 return reader.next().await;
592 };
593
594 let mut pinged = false;
610 loop {
611 let idle_for = writer.activity.idle_for();
612 if idle_for >= idle {
613 trace_debug!("WebSocket connection idle past its budget; closing");
614 return None;
615 }
616 let half = idle / 2;
617 let (wait, ping_now) = if pinged || idle_for >= half {
618 (idle.saturating_sub(idle_for), false)
619 } else {
620 (half.saturating_sub(idle_for), true)
621 };
622
623 if let Ok(frame) = tokio::time::timeout(wait, reader.next()).await {
626 return frame;
627 }
628 if ping_now {
632 if let Ok(mut w) = writer.sink.try_lock() {
640 let budget = idle.saturating_sub(writer.activity.idle_for());
641 let _ =
644 tokio::time::timeout(budget, w.send(WsMessage::Ping(Vec::new().into())))
645 .await;
646 }
647 pinged = true;
651 }
652 }
653 }
654}
655
656fn extract_upgrade_headers(req: &WsUpgradeRequest) -> HashMap<String, String> {
661 let mut map: HashMap<String, String> = req
662 .headers()
663 .iter()
664 .filter_map(|(k, v)| {
665 v.to_str()
666 .ok()
667 .map(|val| (k.as_str().to_lowercase(), val.to_owned()))
668 })
669 .collect();
670 map.insert(":path".to_owned(), req.uri().path().to_owned());
674 map
675}
676
677#[allow(clippy::result_large_err)]
685fn check_a2a_version(req: &WsUpgradeRequest, require: bool) -> Result<(), ErrorResponse> {
686 let value = req
687 .headers()
688 .get(a2a_protocol_types::A2A_VERSION_HEADER)
689 .and_then(|v| v.to_str().ok());
690 let v = value.unwrap_or("").trim();
691 if v.is_empty() {
692 if !require {
693 return Ok(());
694 }
695 } else {
697 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
698 if major == Some(1) {
699 return Ok(());
700 }
701 }
702 let a2a_err = a2a_protocol_types::error::A2aError::version_not_supported(if v.is_empty() {
706 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)"
707 .to_owned()
708 } else {
709 format!("unsupported A2A version: {v}; this server supports 1.x")
710 });
711 let mut error_obj = serde_json::json!({
712 "error": {
713 "code": a2a_err.code.http_status(),
714 "status": a2a_err.code.grpc_status(),
715 "message": a2a_err.message,
716 }
717 });
718 let details = a2a_err.error_info_data(None);
719 if !details.is_null() {
720 error_obj["error"]["details"] = details;
721 }
722 let body = error_obj.to_string();
723 let resp = tokio_tungstenite::tungstenite::http::Response::builder()
724 .status(400)
725 .header("content-type", "application/json")
726 .body(Some(body))
727 .unwrap_or_else(|_| {
728 let mut r = ErrorResponse::new(Some(String::new()));
729 *r.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::BAD_REQUEST;
730 r
731 });
732 Err(resp)
733}
734
735fn best_effort_request_id(text: &str) -> JsonRpcId {
738 let v: serde_json::Value = serde_json::from_str(text).ok()?;
739 match v.get("id") {
740 Some(serde_json::Value::Null) | None => None,
741 Some(id) => Some(id.clone()),
742 }
743}
744
745#[derive(Debug)]
747enum WsError {
748 Handshake(tokio_tungstenite::tungstenite::Error),
749 HandshakeTimeout,
750}
751
752impl std::fmt::Display for WsError {
753 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
754 match self {
755 Self::Handshake(e) => write!(f, "WebSocket handshake failed: {e}"),
756 Self::HandshakeTimeout => write!(f, "WebSocket handshake timed out"),
757 }
758 }
759}
760
761struct Connection {
771 sink: tokio::sync::Mutex<SplitSink<WebSocketStream<TcpStream>, WsMessage>>,
772 activity: ActivityClock,
773}
774
775type WsSink = Arc<Connection>;
776
777struct ActivityClock {
785 start: std::time::Instant,
786 last_ms: std::sync::atomic::AtomicU64,
787}
788
789impl ActivityClock {
790 fn new() -> Self {
791 Self {
792 start: std::time::Instant::now(),
793 last_ms: std::sync::atomic::AtomicU64::new(0),
794 }
795 }
796
797 fn now_ms(&self) -> u64 {
801 u64::try_from(self.start.elapsed().as_millis()).unwrap_or(u64::MAX)
802 }
803
804 fn touch(&self) {
805 self.last_ms
806 .store(self.now_ms(), std::sync::atomic::Ordering::Relaxed);
807 }
808
809 fn idle_for(&self) -> Duration {
810 Duration::from_millis(
811 self.now_ms()
812 .saturating_sub(self.last_ms.load(std::sync::atomic::Ordering::Relaxed)),
813 )
814 }
815}
816
817#[allow(clippy::too_many_lines)]
828async fn process_ws_message(
829 handler: &RequestHandler,
830 text: &str,
831 writer: WsSink,
832 headers: &HashMap<String, String>,
833) {
834 let started = std::time::Instant::now();
835 let parsed = serde_json::from_str::<serde_json::Value>(text)
839 .map_err(|e| (-32700, format!("parse error: {e}")))
840 .and_then(|v| {
841 serde_json::from_value::<JsonRpcRequest>(v)
842 .map_err(|e| (-32600, format!("Invalid Request: {e}")))
843 });
844 let rpc_req = match parsed {
845 Ok(req) => req,
846 Err((code, message)) => {
847 crate::rpc_span::record_unrouted(
848 handler,
849 crate::rpc_span::RpcSystem::JsonRpc,
850 started,
851 &code.to_string(),
852 );
853 let err_resp = JsonRpcErrorResponse::new(None, JsonRpcError::new(code, message));
854 send_json(&writer, &err_resp).await;
855 return;
856 }
857 };
858
859 let id = rpc_req.id.to_response_id();
860
861 match rpc_req.method.as_str() {
862 "SendMessage" => {
863 dispatch_send_message(handler, &rpc_req, false, headers, id, &writer).await;
864 }
865 "SendStreamingMessage" | "message/stream" => {
866 dispatch_send_message(handler, &rpc_req, true, headers, id, &writer).await;
867 }
868 "GetTask" => {
869 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
870 Box::pin(async move {
871 let params: a2a_protocol_types::params::TaskQueryParams =
872 serde_json::from_value(p).map_err(|e| {
873 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
874 })?;
875 h.on_get_task(params, Some(hdr))
876 .await
877 .map(|r| serde_json::to_value(&r).unwrap_or_default())
878 .map_err(|e| e.to_a2a_error())
879 })
880 })
881 .await;
882 }
883 "ListTasks" => {
884 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
885 Box::pin(async move {
886 let params: a2a_protocol_types::params::ListTasksParams =
887 serde_json::from_value(p).map_err(|e| {
888 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
889 })?;
890 h.on_list_tasks(params, Some(hdr))
891 .await
892 .map(|r| serde_json::to_value(&r).unwrap_or_default())
893 .map_err(|e| e.to_a2a_error())
894 })
895 })
896 .await;
897 }
898 "CancelTask" => {
899 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
900 Box::pin(async move {
901 let params: a2a_protocol_types::params::CancelTaskParams =
902 serde_json::from_value(p).map_err(|e| {
903 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
904 })?;
905 h.on_cancel_task(params, Some(hdr))
906 .await
907 .map(|r| serde_json::to_value(&r).unwrap_or_default())
908 .map_err(|e| e.to_a2a_error())
909 })
910 })
911 .await;
912 }
913 "SubscribeToTask" => {
914 let call = async {
915 let params = parse_params::<a2a_protocol_types::params::TaskIdParams>(
916 rpc_req.params.as_ref(),
917 )?;
918 handler.on_resubscribe(params, Some(headers)).await
919 };
920 match ws_span(handler, &rpc_req, headers).run(call).await {
921 Ok(reader) => {
922 stream_events(&writer, reader, id).await;
923 }
924 Err(e) => {
925 send_error(&writer, id, &e).await;
926 }
927 }
928 }
929 "CreateTaskPushNotificationConfig" => {
930 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
931 Box::pin(async move {
932 let params: a2a_protocol_types::push::TaskPushNotificationConfig =
933 serde_json::from_value(p).map_err(|e| {
934 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
935 })?;
936 h.on_set_push_config(params, Some(hdr))
937 .await
938 .map(|r| serde_json::to_value(&r).unwrap_or_default())
939 .map_err(|e| e.to_a2a_error())
940 })
941 })
942 .await;
943 }
944 "GetTaskPushNotificationConfig" => {
945 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
946 Box::pin(async move {
947 let params: a2a_protocol_types::params::GetPushConfigParams =
948 serde_json::from_value(p).map_err(|e| {
949 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
950 })?;
951 h.on_get_push_config(params, Some(hdr))
952 .await
953 .map(|r| serde_json::to_value(&r).unwrap_or_default())
954 .map_err(|e| e.to_a2a_error())
955 })
956 })
957 .await;
958 }
959 "ListTaskPushNotificationConfigs" => {
960 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
961 Box::pin(async move {
962 let params: a2a_protocol_types::params::ListPushConfigsParams =
963 serde_json::from_value(p).map_err(|e| {
964 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
965 })?;
966 h.on_list_push_configs(¶ms.task_id, params.tenant.as_deref(), Some(hdr))
967 .await
968 .map(|configs| {
969 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
970 configs,
971 next_page_token: None,
972 };
973 serde_json::to_value(&resp).unwrap_or_default()
974 })
975 .map_err(|e| e.to_a2a_error())
976 })
977 })
978 .await;
979 }
980 "DeleteTaskPushNotificationConfig" => {
981 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
982 Box::pin(async move {
983 let params: a2a_protocol_types::params::DeletePushConfigParams =
984 serde_json::from_value(p).map_err(|e| {
985 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
986 })?;
987 h.on_delete_push_config(params, Some(hdr))
988 .await
989 .map(|()| serde_json::json!({}))
990 .map_err(|e| e.to_a2a_error())
991 })
992 })
993 .await;
994 }
995 "GetExtendedAgentCard" => {
996 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, _p, hdr| {
997 Box::pin(async move {
998 h.on_get_extended_agent_card(Some(hdr))
999 .await
1000 .map(|r| serde_json::to_value(&r).unwrap_or_default())
1001 .map_err(|e| e.to_a2a_error())
1002 })
1003 })
1004 .await;
1005 }
1006 other => {
1007 let refused: Result<(), ServerError> = ws_span(handler, &rpc_req, headers)
1008 .run(async { Err(ServerError::MethodNotFound(other.to_owned())) })
1009 .await;
1010 if let Err(err) = refused {
1011 send_error(&writer, id, &err).await;
1012 }
1013 }
1014 }
1015}
1016
1017fn ws_span(
1019 handler: &RequestHandler,
1020 rpc_req: &JsonRpcRequest,
1021 headers: &HashMap<String, String>,
1022) -> crate::rpc_span::ServerSpan {
1023 crate::rpc_span::ServerSpan::open(
1024 handler,
1025 crate::rpc_span::RpcSystem::JsonRpc,
1026 &rpc_req.method,
1027 Some(headers),
1028 )
1029}
1030
1031async fn dispatch_send_message(
1033 handler: &RequestHandler,
1034 rpc_req: &JsonRpcRequest,
1035 streaming: bool,
1036 headers: &HashMap<String, String>,
1037 id: JsonRpcId,
1038 writer: &WsSink,
1039) {
1040 let call = async {
1041 let params =
1042 parse_params::<a2a_protocol_types::params::MessageSendParams>(rpc_req.params.as_ref())?;
1043 handler
1044 .on_send_message(params, streaming, Some(headers))
1045 .await
1046 };
1047 match ws_span(handler, rpc_req, headers).run(call).await {
1048 Ok(SendMessageResult::Response(resp)) => {
1049 let result = serde_json::to_value(&resp).unwrap_or(serde_json::Value::Null);
1050 let success = JsonRpcSuccessResponse {
1051 jsonrpc: JsonRpcVersion,
1052 id,
1053 result,
1054 };
1055 send_json(writer, &success).await;
1056 }
1057 Ok(SendMessageResult::Stream(reader)) => {
1058 stream_events(writer, reader, id).await;
1059 }
1060 Err(e) => {
1061 send_error(writer, id, &e).await;
1062 }
1063 }
1064}
1065
1066async fn stream_events(
1068 writer: &WsSink,
1069 mut reader: crate::streaming::InMemoryQueueReader,
1070 id: JsonRpcId,
1071) {
1072 while let Some(event) = reader.read().await {
1073 match event.map(|e| e.event) {
1074 Ok(stream_resp) => {
1075 let envelope = JsonRpcSuccessResponse {
1083 jsonrpc: JsonRpcVersion,
1084 id: id.clone(),
1085 result: stream_resp,
1086 };
1087 let json = serde_json::to_string(&envelope).unwrap_or_default();
1088 let mut w = writer.sink.lock().await;
1089 if w.send(WsMessage::Text(json.into())).await.is_err() {
1090 return; }
1092 drop(w);
1093 writer.activity.touch();
1096 }
1097 Err(e) => {
1098 let err_resp =
1099 JsonRpcErrorResponse::new(id.clone(), JsonRpcError::new(-32000, e.to_string()));
1100 send_json(writer, &err_resp).await;
1101 return;
1102 }
1103 }
1104 }
1105
1106 let success = JsonRpcSuccessResponse {
1108 jsonrpc: JsonRpcVersion,
1109 id,
1110 result: serde_json::json!({"status": "stream_complete"}),
1111 };
1112 send_json(writer, &success).await;
1113}
1114
1115async fn dispatch_simple<'a, F>(
1117 handler: &'a RequestHandler,
1118 rpc_req: &JsonRpcRequest,
1119 id: JsonRpcId,
1120 headers: &'a HashMap<String, String>,
1121 writer: &WsSink,
1122 f: F,
1123) where
1124 F: FnOnce(
1125 &'a RequestHandler,
1126 serde_json::Value,
1127 &'a HashMap<String, String>,
1128 ) -> std::pin::Pin<
1129 Box<
1130 dyn std::future::Future<
1131 Output = Result<serde_json::Value, a2a_protocol_types::error::A2aError>,
1132 > + Send
1133 + 'a,
1134 >,
1135 >,
1136{
1137 let params = rpc_req.params.clone().unwrap_or(serde_json::Value::Null);
1138 match ws_span(handler, rpc_req, headers)
1139 .run(f(handler, params, headers))
1140 .await
1141 {
1142 Ok(result) => {
1143 let success = JsonRpcSuccessResponse {
1144 jsonrpc: JsonRpcVersion,
1145 id,
1146 result,
1147 };
1148 send_json(writer, &success).await;
1149 }
1150 Err(e) => {
1151 let err_resp =
1152 JsonRpcErrorResponse::new(id, JsonRpcError::new(e.code.as_i32(), e.message));
1153 send_json(writer, &err_resp).await;
1154 }
1155 }
1156}
1157
1158async fn send_json<T: serde::Serialize + Sync>(writer: &WsSink, value: &T) {
1160 let json = serde_json::to_string(value).unwrap_or_default();
1161 let mut w = writer.sink.lock().await;
1162 let _ = w.send(WsMessage::Text(json.into())).await;
1163 drop(w);
1164 writer.activity.touch();
1165}
1166
1167async fn send_error(writer: &WsSink, id: JsonRpcId, err: &ServerError) {
1169 let a2a_err = err.to_a2a_error();
1170 let resp = JsonRpcErrorResponse::new(
1171 id,
1172 JsonRpcError::new(a2a_err.code.as_i32(), a2a_err.message),
1173 );
1174 send_json(writer, &resp).await;
1175}
1176
1177fn parse_params<T: serde::de::DeserializeOwned>(
1179 params: Option<&serde_json::Value>,
1180) -> Result<T, ServerError> {
1181 let value = params.cloned().unwrap_or(serde_json::Value::Null);
1182 serde_json::from_value(value)
1183 .map_err(|e| ServerError::InvalidParams(format!("invalid params: {e}")))
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use super::*;
1189
1190 #[test]
1191 fn parse_params_with_valid_json() {
1192 let value = Some(serde_json::json!({"id": "task-1"}));
1193 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1194 parse_params(value.as_ref());
1195 assert!(result.is_ok());
1196 assert_eq!(result.unwrap().id, "task-1");
1197 }
1198
1199 #[test]
1200 fn parse_params_with_none_returns_error() {
1201 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> = parse_params(None);
1202 assert!(result.is_err());
1203 }
1204
1205 #[test]
1206 fn parse_params_with_wrong_type_returns_error() {
1207 let value = Some(serde_json::json!("not an object"));
1208 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1209 parse_params(value.as_ref());
1210 assert!(result.is_err());
1211 }
1212
1213 #[test]
1215 fn ws_error_display_contains_message() {
1216 let err = WsError::Handshake(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
1217 let s = err.to_string();
1218 assert!(s.contains("WebSocket handshake failed"));
1219 }
1220
1221 #[test]
1222 fn ws_error_display_handshake_timeout() {
1223 let s = WsError::HandshakeTimeout.to_string();
1224 assert!(s.contains("timed out"), "got: {s}");
1225 }
1226
1227 #[test]
1230 fn best_effort_request_id_extracts_string_and_number() {
1231 assert_eq!(
1232 best_effort_request_id(r#"{"jsonrpc":"2.0","id":"req-1","method":"GetTask"}"#),
1233 Some(serde_json::json!("req-1"))
1234 );
1235 assert_eq!(
1236 best_effort_request_id(r#"{"jsonrpc":"2.0","id":7,"method":"GetTask"}"#),
1237 Some(serde_json::json!(7))
1238 );
1239 }
1240
1241 #[test]
1242 fn best_effort_request_id_none_for_missing_null_or_invalid() {
1243 assert_eq!(best_effort_request_id(r#"{"jsonrpc":"2.0"}"#), None);
1244 assert_eq!(best_effort_request_id(r#"{"id":null}"#), None);
1245 assert_eq!(best_effort_request_id("not json {{"), None);
1246 }
1247
1248 #[test]
1250 fn websocket_dispatcher_new() {
1251 use crate::RequestHandlerBuilder;
1252 use crate::agent_executor;
1253 use std::sync::Arc;
1254 struct DummyExec;
1255 agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
1256 let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
1257 let _dispatcher = WebSocketDispatcher::new(handler);
1258 }
1259
1260 use crate::RequestHandlerBuilder;
1263 use crate::agent_executor;
1264 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
1265 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
1266 use futures_util::{SinkExt, StreamExt};
1267
1268 struct EchoExec;
1269 agent_executor!(EchoExec, |ctx, queue| async {
1270 queue
1271 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1272 task_id: ctx.task_id.clone(),
1273 context_id: ContextId::new(ctx.context_id.clone()),
1274 status: TaskStatus::new(TaskState::Working),
1275 metadata: None,
1276 }))
1277 .await?;
1278 queue
1279 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1280 task_id: ctx.task_id.clone(),
1281 context_id: ContextId::new(ctx.context_id.clone()),
1282 status: TaskStatus::new(TaskState::Completed),
1283 metadata: None,
1284 }))
1285 .await?;
1286 Ok(())
1287 });
1288
1289 async fn spawn_ws_server() -> std::net::SocketAddr {
1290 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1291 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1292 dispatcher
1293 .serve_with_addr("127.0.0.1:0")
1294 .await
1295 .expect("bind to port 0")
1296 }
1297
1298 #[tokio::test(flavor = "multi_thread")]
1322 async fn a_silent_peer_is_closed_once_its_idle_budget_is_spent() {
1323 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1324 let mut ws = ws_connect(addr).await;
1325
1326 tokio::time::sleep(Duration::from_secs(4)).await;
1327
1328 let ended = tokio::time::timeout(Duration::from_secs(5), async {
1331 loop {
1332 match ws.next().await {
1333 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1334 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1335 Some(Ok(other)) => panic!("unexpected frame on an idle connection: {other:?}"),
1336 }
1337 }
1338 })
1339 .await;
1340
1341 assert!(
1342 ended.is_ok(),
1343 "a connection idle past two 2s budgets must be closed; it was still open"
1344 );
1345 }
1346
1347 #[tokio::test(flavor = "multi_thread")]
1360 async fn a_peer_that_answers_the_keepalive_keeps_its_connection() {
1361 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1362 let mut ws = ws_connect(addr).await;
1363
1364 let died = tokio::time::timeout(Duration::from_secs(6), async {
1367 loop {
1368 match ws.next().await {
1369 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1370 other => return other,
1371 }
1372 }
1373 })
1374 .await;
1375 assert!(
1376 died.is_err(),
1377 "a responsive peer must not be closed; got {died:?}"
1378 );
1379
1380 ws.send(WsMessage::Text(send_message_json("keepalive").into()))
1382 .await
1383 .expect("send after three idle budgets");
1384 let text = tokio::time::timeout(Duration::from_secs(5), async {
1385 loop {
1386 match ws.next().await {
1387 Some(Ok(WsMessage::Text(t))) => return t,
1388 Some(Ok(_)) => {}
1389 other => panic!("connection died mid-request: {other:?}"),
1390 }
1391 }
1392 })
1393 .await
1394 .expect("a response within 5s");
1395 assert!(
1396 text.contains("\"result\""),
1397 "expected a JSON-RPC result, got: {text}"
1398 );
1399 }
1400
1401 #[tokio::test(flavor = "multi_thread")]
1414 async fn the_defaults_are_no_idle_timeout_and_no_connection_ceiling() {
1415 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1416 let default = WebSocketDispatcher::new(handler);
1417 assert!(
1418 default.idle_timeout.is_none(),
1419 "the idle timeout is opt-in: a WebSocket subscription may be legitimately \
1420 silent for hours, and a default that closes it is a knob nobody enables"
1421 );
1422 assert!(
1423 default.max_connections.is_none(),
1424 "unbounded by default, matching ServeConfig::max_connections"
1425 );
1426
1427 let addr = spawn_ws_server().await;
1428 let mut ws = ws_connect(addr).await;
1429 let died = tokio::time::timeout(Duration::from_secs(3), ws.next()).await;
1430 assert!(
1431 died.is_err(),
1432 "with no idle timeout nothing should arrive or close: {died:?}"
1433 );
1434 ws.send(WsMessage::Text(send_message_json("still-here").into()))
1435 .await
1436 .expect("send after idling");
1437 let text = read_text(&mut ws).await;
1438 assert!(
1439 text.contains("\"result\""),
1440 "expected a result, got: {text}"
1441 );
1442 }
1443
1444 #[tokio::test(flavor = "multi_thread")]
1451 async fn max_connections_bounds_accepted_connections() {
1452 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1453
1454 let addr = spawn_ws_server_with(|d| d.with_max_connections(2)).await;
1455
1456 let first = ws_connect(addr).await;
1457 let second = ws_connect(addr).await;
1458
1459 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1460 req.headers_mut()
1461 .insert("a2a-version", "1.0".parse().expect("header"));
1462 let third = tokio::time::timeout(
1463 Duration::from_secs(2),
1464 tokio_tungstenite::connect_async(req),
1465 )
1466 .await;
1467 assert!(
1468 third.is_err(),
1469 "the third handshake must not complete against a ceiling of 2; got {:?}",
1470 third.map(|r| r.is_ok())
1471 );
1472
1473 drop(first);
1477 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1478 req.headers_mut()
1479 .insert("a2a-version", "1.0".parse().expect("header"));
1480 let replacement = tokio::time::timeout(
1481 Duration::from_secs(5),
1482 tokio_tungstenite::connect_async(req),
1483 )
1484 .await;
1485 assert!(
1486 matches!(replacement, Ok(Ok(_))),
1487 "a freed permit must admit the next peer"
1488 );
1489 drop(second);
1490 }
1491
1492 struct TrickleExec {
1496 count: usize,
1497 step: Duration,
1498 }
1499
1500 impl crate::executor::AgentExecutor for TrickleExec {
1501 fn execute<'a>(
1502 &'a self,
1503 ctx: &'a crate::request_context::RequestContext,
1504 queue: &'a dyn crate::streaming::EventQueueWriter,
1505 ) -> std::pin::Pin<
1506 Box<
1507 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1508 + Send
1509 + 'a,
1510 >,
1511 > {
1512 Box::pin(async move {
1513 let status = |state: TaskState| {
1514 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1515 task_id: ctx.task_id.clone(),
1516 context_id: ContextId::new(ctx.context_id.clone()),
1517 status: TaskStatus::new(state),
1518 metadata: None,
1519 })
1520 };
1521 for _ in 0..self.count {
1522 tokio::time::sleep(self.step).await;
1523 queue.write(status(TaskState::Working)).await?;
1524 }
1525 queue.write(status(TaskState::Completed)).await?;
1526 Ok(())
1527 })
1528 }
1529 }
1530
1531 #[tokio::test(flavor = "multi_thread")]
1542 async fn a_stream_to_a_silent_consumer_is_pinged_once() {
1543 let idle = Duration::from_millis(1_000);
1544 let executor = TrickleExec {
1547 count: 12,
1548 step: Duration::from_millis(250),
1549 };
1550 let addr = spawn_ws_server_with_executor(executor, |d| d.with_idle_timeout(idle)).await;
1551 let mut ws = ws_connect(addr).await;
1552
1553 ws.send(WsMessage::Text(streaming_message_json("trickle").into()))
1554 .await
1555 .expect("send the streaming request");
1556
1557 tokio::time::sleep(Duration::from_millis(3_000) + 2 * idle).await;
1560
1561 let mut pings = 0;
1562 let mut texts = 0;
1563 let closed = tokio::time::timeout(Duration::from_secs(5), async {
1564 loop {
1565 match ws.next().await {
1566 Some(Ok(WsMessage::Ping(_))) => pings += 1,
1567 Some(Ok(WsMessage::Text(_))) => texts += 1,
1568 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1569 Some(Ok(other)) => panic!("unexpected frame: {other:?}"),
1570 }
1571 }
1572 })
1573 .await;
1574 assert!(
1575 closed.is_ok(),
1576 "a consumer that never reads must be closed once the stream ends and the idle budget passes"
1577 );
1578 assert!(
1579 texts >= 12,
1580 "the whole stream was written while the consumer read nothing; got {texts} text frames"
1581 );
1582 assert_eq!(
1583 pings, 1,
1584 "one Ping at the half-budget, and no more without an inbound frame"
1585 );
1586 }
1587
1588 fn streaming_message_json(id: &str) -> String {
1589 serde_json::json!({
1590 "jsonrpc": "2.0",
1591 "method": "SendStreamingMessage",
1592 "id": id,
1593 "params": {
1594 "message": {
1595 "messageId": "msg-stream",
1596 "role": "ROLE_USER",
1597 "parts": [{"text": "stream"}]
1598 }
1599 }
1600 })
1601 .to_string()
1602 }
1603
1604 async fn spawn_ws_server_with(
1606 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1607 ) -> std::net::SocketAddr {
1608 spawn_ws_server_with_executor(EchoExec, configure).await
1609 }
1610
1611 async fn spawn_ws_server_with_executor(
1613 executor: impl crate::executor::AgentExecutor + 'static,
1614 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1615 ) -> std::net::SocketAddr {
1616 let handler = Arc::new(RequestHandlerBuilder::new(executor).build().unwrap());
1617 let dispatcher = Arc::new(configure(WebSocketDispatcher::new(handler)));
1618 dispatcher
1619 .serve_with_addr("127.0.0.1:0")
1620 .await
1621 .expect("bind to port 0")
1622 }
1623
1624 async fn ws_connect(
1625 addr: std::net::SocketAddr,
1626 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
1627 {
1628 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1629 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1630 req.headers_mut()
1631 .insert("a2a-version", "1.0".parse().expect("header"));
1632 let (ws, _) = tokio_tungstenite::connect_async(req)
1633 .await
1634 .expect("ws connect");
1635 ws
1636 }
1637
1638 async fn read_text(
1640 ws: &mut tokio_tungstenite::WebSocketStream<
1641 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1642 >,
1643 ) -> String {
1644 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1645 .await
1646 .expect("timeout waiting for WS frame")
1647 .expect("stream ended")
1648 .expect("ws error");
1649 msg.into_text()
1650 .expect("not a text frame")
1651 .as_str()
1652 .to_owned()
1653 }
1654
1655 fn send_message_json(id: &str) -> String {
1656 serde_json::json!({
1657 "jsonrpc": "2.0",
1658 "method": "SendMessage",
1659 "id": id,
1660 "params": {
1661 "message": {
1662 "messageId": "msg-1",
1663 "role": "ROLE_USER",
1664 "parts": [{"text": "hello"}]
1665 }
1666 }
1667 })
1668 .to_string()
1669 }
1670
1671 #[tokio::test]
1673 async fn ws_send_message_success() {
1674 let addr = spawn_ws_server().await;
1675 let mut ws = ws_connect(addr).await;
1676
1677 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
1678 .await
1679 .unwrap();
1680
1681 let text = read_text(&mut ws).await;
1682 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1683 assert_eq!(v["id"], "sm-1");
1684 assert!(v.get("result").is_some(), "expected result key: {text}");
1686 }
1687
1688 #[tokio::test]
1690 async fn ws_get_task_not_found() {
1691 let addr = spawn_ws_server().await;
1692 let mut ws = ws_connect(addr).await;
1693
1694 let req = serde_json::json!({
1695 "jsonrpc": "2.0",
1696 "method": "GetTask",
1697 "id": "gt-1",
1698 "params": {"id": "nonexistent"}
1699 })
1700 .to_string();
1701 ws.send(WsMessage::Text(req.into())).await.unwrap();
1702
1703 let text = read_text(&mut ws).await;
1704 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1705 assert!(v.get("error").is_some(), "expected error: {text}");
1706 }
1707
1708 #[tokio::test]
1710 async fn ws_list_tasks_success() {
1711 let addr = spawn_ws_server().await;
1712 let mut ws = ws_connect(addr).await;
1713
1714 let req = serde_json::json!({
1715 "jsonrpc": "2.0",
1716 "method": "ListTasks",
1717 "id": "lt-1",
1718 "params": {}
1719 })
1720 .to_string();
1721 ws.send(WsMessage::Text(req.into())).await.unwrap();
1722
1723 let text = read_text(&mut ws).await;
1724 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1725 assert_eq!(v["id"], "lt-1");
1726 assert!(v.get("result").is_some(), "expected result: {text}");
1727 }
1728
1729 #[tokio::test]
1731 async fn ws_cancel_task_not_found() {
1732 let addr = spawn_ws_server().await;
1733 let mut ws = ws_connect(addr).await;
1734
1735 let req = serde_json::json!({
1736 "jsonrpc": "2.0",
1737 "method": "CancelTask",
1738 "id": "ct-1",
1739 "params": {"id": "nonexistent"}
1740 })
1741 .to_string();
1742 ws.send(WsMessage::Text(req.into())).await.unwrap();
1743
1744 let text = read_text(&mut ws).await;
1745 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1746 assert!(v.get("error").is_some(), "expected error: {text}");
1747 }
1748
1749 #[tokio::test]
1751 async fn ws_subscribe_task_not_found() {
1752 let addr = spawn_ws_server().await;
1753 let mut ws = ws_connect(addr).await;
1754
1755 let req = serde_json::json!({
1756 "jsonrpc": "2.0",
1757 "method": "SubscribeToTask",
1758 "id": "sub-1",
1759 "params": {"id": "nonexistent"}
1760 })
1761 .to_string();
1762 ws.send(WsMessage::Text(req.into())).await.unwrap();
1763
1764 let text = read_text(&mut ws).await;
1765 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1766 assert!(v.get("error").is_some(), "expected error: {text}");
1767 }
1768
1769 #[tokio::test]
1771 async fn ws_unknown_method_error() {
1772 let addr = spawn_ws_server().await;
1773 let mut ws = ws_connect(addr).await;
1774
1775 let req = serde_json::json!({
1776 "jsonrpc": "2.0",
1777 "method": "FooBar",
1778 "id": "unk-1",
1779 "params": {}
1780 })
1781 .to_string();
1782 ws.send(WsMessage::Text(req.into())).await.unwrap();
1783
1784 let text = read_text(&mut ws).await;
1785 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1786 assert!(v.get("error").is_some(), "expected error: {text}");
1787 let msg = v["error"]["message"].as_str().unwrap_or("");
1788 assert!(
1789 msg.to_lowercase().contains("method")
1790 || msg.to_lowercase().contains("not found")
1791 || msg.to_lowercase().contains("unsupported"),
1792 "error message should mention method not found: {msg}"
1793 );
1794 }
1795
1796 #[tokio::test]
1798 async fn ws_invalid_json_parse_error() {
1799 let addr = spawn_ws_server().await;
1800 let mut ws = ws_connect(addr).await;
1801
1802 ws.send(WsMessage::Text("this is not json {{".into()))
1803 .await
1804 .unwrap();
1805
1806 let text = read_text(&mut ws).await;
1807 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1808 assert_eq!(v["error"]["code"], -32700, "expected parse error code");
1809 }
1810
1811 #[tokio::test]
1819 async fn ws_oversized_message_rejected() {
1820 let addr = spawn_ws_server().await;
1821 let mut ws = ws_connect(addr).await;
1822
1823 let big = "x".repeat(4 * 1024 * 1024 + 1);
1825 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1829 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1832 .await
1833 .expect("server should react to the oversized message");
1834 match outcome {
1835 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1836 Some(Ok(frame)) => panic!(
1837 "server must not answer an oversized message with a frame, got: {frame:?}"
1838 ),
1839 }
1840 }
1841 }
1842
1843 #[tokio::test]
1847 async fn ws_large_message_under_cap_still_processed() {
1848 let addr = spawn_ws_server().await;
1849 let mut ws = ws_connect(addr).await;
1850
1851 let big = "x".repeat(3 * 1024 * 1024);
1852 ws.send(WsMessage::Text(big.into())).await.unwrap();
1853
1854 let text = read_text(&mut ws).await;
1855 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1856 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1857 }
1858
1859 #[tokio::test]
1861 async fn ws_ping_pong_response() {
1862 let addr = spawn_ws_server().await;
1863 let mut ws = ws_connect(addr).await;
1864
1865 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1866
1867 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1868 loop {
1869 let msg = ws.next().await.unwrap().unwrap();
1870 if let WsMessage::Pong(data) = msg {
1871 return data;
1872 }
1873 }
1874 })
1875 .await
1876 .expect("should get pong within 3s");
1877 assert_eq!(pong, vec![42, 43]);
1878 }
1879
1880 #[tokio::test]
1882 async fn ws_get_task_invalid_params() {
1883 let addr = spawn_ws_server().await;
1884 let mut ws = ws_connect(addr).await;
1885
1886 let req = serde_json::json!({
1888 "jsonrpc": "2.0",
1889 "method": "GetTask",
1890 "id": "gti-1",
1891 "params": {"wrong_field": 123}
1892 })
1893 .to_string();
1894 ws.send(WsMessage::Text(req.into())).await.unwrap();
1895
1896 let text = read_text(&mut ws).await;
1897 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1898 assert!(
1899 v.get("error").is_some(),
1900 "expected error for bad params: {text}"
1901 );
1902 }
1903
1904 #[tokio::test]
1906 async fn ws_send_streaming_message_events() {
1907 let addr = spawn_ws_server().await;
1908 let mut ws = ws_connect(addr).await;
1909
1910 let req = serde_json::json!({
1911 "jsonrpc": "2.0",
1912 "method": "SendStreamingMessage",
1913 "id": "ssm-1",
1914 "params": {
1915 "message": {
1916 "messageId": "msg-stream-1",
1917 "role": "ROLE_USER",
1918 "parts": [{"text": "stream me"}]
1919 }
1920 }
1921 })
1922 .to_string();
1923 ws.send(WsMessage::Text(req.into())).await.unwrap();
1924
1925 let mut frames = Vec::new();
1927 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1928 loop {
1929 let msg = ws.next().await.unwrap().unwrap();
1930 let text = msg.into_text().unwrap();
1931 let done = text.contains("stream_complete");
1932 frames.push(text);
1933 if done {
1934 break;
1935 }
1936 }
1937 });
1938 timeout.await.expect("streaming should complete within 5s");
1939
1940 assert!(
1942 frames.len() >= 3,
1943 "expected >= 3 frames, got {}: {:?}",
1944 frames.len(),
1945 frames
1946 );
1947 assert!(frames.last().unwrap().contains("stream_complete"));
1949 }
1950
1951 #[tokio::test]
1953 async fn ws_send_message_invalid_params() {
1954 let addr = spawn_ws_server().await;
1955 let mut ws = ws_connect(addr).await;
1956
1957 let req = serde_json::json!({
1958 "jsonrpc": "2.0",
1959 "method": "SendMessage",
1960 "id": "smi-1",
1961 "params": {"not_message": true}
1962 })
1963 .to_string();
1964 ws.send(WsMessage::Text(req.into())).await.unwrap();
1965
1966 let text = read_text(&mut ws).await;
1967 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1968 assert!(
1969 v.get("error").is_some(),
1970 "expected error for bad send params: {text}"
1971 );
1972 }
1973
1974 #[tokio::test]
1976 async fn ws_subscribe_invalid_params() {
1977 let addr = spawn_ws_server().await;
1978 let mut ws = ws_connect(addr).await;
1979
1980 let req = serde_json::json!({
1981 "jsonrpc": "2.0",
1982 "method": "SubscribeToTask",
1983 "id": "subi-1",
1984 "params": {}
1985 })
1986 .to_string();
1987 ws.send(WsMessage::Text(req.into())).await.unwrap();
1988
1989 let text = read_text(&mut ws).await;
1990 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1991 assert!(
1992 v.get("error").is_some(),
1993 "expected error for bad subscribe params: {text}"
1994 );
1995 }
1996
1997 #[tokio::test]
1999 async fn ws_cancel_task_invalid_params() {
2000 let addr = spawn_ws_server().await;
2001 let mut ws = ws_connect(addr).await;
2002
2003 let req = serde_json::json!({
2004 "jsonrpc": "2.0",
2005 "method": "CancelTask",
2006 "id": "cti-1",
2007 "params": {"wrong": 1}
2008 })
2009 .to_string();
2010 ws.send(WsMessage::Text(req.into())).await.unwrap();
2011
2012 let text = read_text(&mut ws).await;
2013 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2014 assert!(v.get("error").is_some(), "expected error: {text}");
2015 }
2016
2017 #[tokio::test]
2019 async fn ws_list_tasks_with_filters() {
2020 let addr = spawn_ws_server().await;
2021 let mut ws = ws_connect(addr).await;
2022
2023 let req = serde_json::json!({
2024 "jsonrpc": "2.0",
2025 "method": "ListTasks",
2026 "id": "ltf-1",
2027 "params": {
2028 "contextId": "ctx-1",
2029 "pageSize": 10
2030 }
2031 })
2032 .to_string();
2033 ws.send(WsMessage::Text(req.into())).await.unwrap();
2034
2035 let text = read_text(&mut ws).await;
2036 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2037 assert_eq!(v["id"], "ltf-1");
2038 assert!(v.get("result").is_some(), "expected result: {text}");
2039 }
2040
2041 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
2044
2045 async fn ws_call(
2047 ws: &mut tokio_tungstenite::WebSocketStream<
2048 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
2049 >,
2050 req: serde_json::Value,
2051 ) -> serde_json::Value {
2052 ws.send(WsMessage::Text(req.to_string().into()))
2053 .await
2054 .expect("send");
2055 let text = read_text(ws).await;
2056 serde_json::from_str(&text).expect("response should be JSON")
2057 }
2058
2059 #[tokio::test]
2064 async fn ws_legacy_method_names_rejected() {
2065 let addr = spawn_ws_server().await;
2066 let mut ws = ws_connect(addr).await;
2067
2068 for legacy in ["message/send", "tasks/list", "tasks/get"] {
2069 let v = ws_call(
2070 &mut ws,
2071 serde_json::json!({
2072 "jsonrpc": "2.0",
2073 "method": legacy,
2074 "id": format!("legacy-{legacy}"),
2075 "params": {}
2076 }),
2077 )
2078 .await;
2079 assert_eq!(
2080 v["error"]["code"].as_i64(),
2081 Some(-32601),
2082 "v0.3-style name {legacy} must be MethodNotFound: {v}"
2083 );
2084 }
2085 }
2086
2087 #[tokio::test]
2091 async fn ws_calls_are_recorded_with_the_code_they_were_answered_with() {
2092 type Seen = Arc<std::sync::Mutex<Vec<(Option<String>, Option<String>)>>>;
2093 struct Recording(Seen);
2094 impl crate::Metrics for Recording {
2095 fn on_rpc_call(&self, call: &crate::RpcCall<'_>) {
2096 assert_eq!(call.system, "jsonrpc");
2097 self.0.lock().unwrap().push((
2098 call.method.map(str::to_owned),
2099 call.error_type.map(str::to_owned),
2100 ));
2101 }
2102 }
2103 let seen = Seen::default();
2104 let handler = Arc::new(
2105 RequestHandlerBuilder::new(EchoExec)
2106 .with_metrics(Recording(Arc::clone(&seen)))
2107 .build()
2108 .unwrap(),
2109 );
2110 let addr = Arc::new(WebSocketDispatcher::new(handler))
2111 .serve_with_addr("127.0.0.1:0")
2112 .await
2113 .expect("bind to port 0");
2114 let mut ws = ws_connect(addr).await;
2115
2116 ws.send(WsMessage::Text("not json".into())).await.unwrap();
2117 let _ = read_text(&mut ws).await;
2118 for (id, method, params) in [
2119 ("a", "tasks/get", serde_json::json!({})),
2120 ("b", "GetTask", serde_json::json!({"id": "nope"})),
2121 ] {
2122 let _ = ws_call(
2123 &mut ws,
2124 serde_json::json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params}),
2125 )
2126 .await;
2127 }
2128
2129 let s = |v: &str| Some(v.to_owned());
2130 assert_eq!(
2131 *seen.lock().unwrap(),
2132 [
2133 (None, s("-32700")),
2134 (s("_OTHER"), s("-32601")),
2135 (s("lf.a2a.v1.A2AService/GetTask"), s("-32001")),
2136 ]
2137 );
2138 }
2139
2140 #[tokio::test]
2143 #[allow(clippy::too_many_lines)]
2144 async fn ws_push_config_methods_routed() {
2145 use crate::push::PushSender;
2146 use a2a_protocol_types::push::TaskPushNotificationConfig;
2147
2148 struct NoopSender;
2149 impl PushSender for NoopSender {
2150 fn send<'a>(
2151 &'a self,
2152 _url: &'a str,
2153 _event: &'a StreamResponse,
2154 _config: &'a TaskPushNotificationConfig,
2155 ) -> std::pin::Pin<
2156 Box<
2157 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
2158 + Send
2159 + 'a,
2160 >,
2161 > {
2162 Box::pin(async { Ok(()) })
2163 }
2164 fn allows_private_urls(&self) -> bool {
2165 true
2166 }
2167 }
2168
2169 let handler = Arc::new(
2170 RequestHandlerBuilder::new(EchoExec)
2171 .with_push_sender(NoopSender)
2172 .build()
2173 .unwrap(),
2174 );
2175 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
2176 let addr = dispatcher
2177 .serve_with_addr("127.0.0.1:0")
2178 .await
2179 .expect("bind");
2180 let mut ws = ws_connect(addr).await;
2181
2182 let v = ws_call(
2184 &mut ws,
2185 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
2186 )
2187 .await;
2188 let task_id = v["result"]["task"]["id"]
2189 .as_str()
2190 .expect("task id in send result")
2191 .to_owned();
2192
2193 let v = ws_call(
2195 &mut ws,
2196 serde_json::json!({
2197 "jsonrpc": "2.0",
2198 "method": "CreateTaskPushNotificationConfig",
2199 "id": "pc-1",
2200 "params": {
2201 "taskId": task_id,
2202 "url": "https://example.com/hook"
2203 }
2204 }),
2205 )
2206 .await;
2207 assert!(v.get("result").is_some(), "set push config failed: {v}");
2208 let config_id = v["result"]["id"]
2209 .as_str()
2210 .expect("server-assigned config id")
2211 .to_owned();
2212
2213 let v = ws_call(
2215 &mut ws,
2216 serde_json::json!({
2217 "jsonrpc": "2.0",
2218 "method": "GetTaskPushNotificationConfig",
2219 "id": "pc-2",
2220 "params": {"taskId": task_id, "id": config_id}
2221 }),
2222 )
2223 .await;
2224 assert!(v.get("result").is_some(), "get push config failed: {v}");
2225
2226 let v = ws_call(
2228 &mut ws,
2229 serde_json::json!({
2230 "jsonrpc": "2.0",
2231 "method": "ListTaskPushNotificationConfigs",
2232 "id": "pc-3",
2233 "params": {"taskId": task_id}
2234 }),
2235 )
2236 .await;
2237 assert!(v.get("result").is_some(), "list push configs failed: {v}");
2238 assert!(
2239 v["result"]["configs"].is_array(),
2240 "expected configs array: {v}"
2241 );
2242
2243 let v = ws_call(
2245 &mut ws,
2246 serde_json::json!({
2247 "jsonrpc": "2.0",
2248 "method": "DeleteTaskPushNotificationConfig",
2249 "id": "pc-4",
2250 "params": {"taskId": task_id, "id": config_id}
2251 }),
2252 )
2253 .await;
2254 assert!(v.get("result").is_some(), "delete push config failed: {v}");
2255 }
2256
2257 #[tokio::test]
2260 async fn ws_get_extended_agent_card_routed() {
2261 let addr = spawn_ws_server().await;
2262 let mut ws = ws_connect(addr).await;
2263
2264 let v = ws_call(
2265 &mut ws,
2266 serde_json::json!({
2267 "jsonrpc": "2.0",
2268 "method": "GetExtendedAgentCard",
2269 "id": "card-1",
2270 "params": {}
2271 }),
2272 )
2273 .await;
2274 let err = v.get("error").expect("expected an error response");
2277 assert_ne!(
2278 err["code"], -32601,
2279 "GetExtendedAgentCard must be routed, got: {v}"
2280 );
2281 }
2282
2283 #[tokio::test]
2287 async fn ws_upgrade_headers_drive_tenant_resolution() {
2288 use crate::tenant_resolver::HeaderTenantResolver;
2289
2290 let handler = Arc::new(
2291 RequestHandlerBuilder::new(EchoExec)
2292 .with_tenant_resolver(HeaderTenantResolver::default())
2293 .require_resolved_tenant()
2294 .build()
2295 .unwrap(),
2296 );
2297 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
2298 let addr = dispatcher
2299 .serve_with_addr("127.0.0.1:0")
2300 .await
2301 .expect("bind");
2302
2303 let mut ws = ws_connect(addr).await;
2305 let v = ws_call(
2306 &mut ws,
2307 serde_json::json!({
2308 "jsonrpc": "2.0",
2309 "method": "ListTasks",
2310 "id": "t-1",
2311 "params": {}
2312 }),
2313 )
2314 .await;
2315 let err = v.get("error").expect("headerless request must be rejected");
2316 let msg = err["message"].as_str().unwrap_or("");
2317 assert!(
2318 msg.contains("tenant"),
2319 "expected strict-tenancy rejection, got: {v}"
2320 );
2321
2322 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2324 req.headers_mut()
2325 .insert("a2a-version", "1.0".parse().unwrap());
2326 req.headers_mut()
2327 .insert("x-tenant-id", "acme".parse().unwrap());
2328 let (mut ws, _) = tokio_tungstenite::connect_async(req)
2329 .await
2330 .expect("connect");
2331 let v = ws_call(
2332 &mut ws,
2333 serde_json::json!({
2334 "jsonrpc": "2.0",
2335 "method": "ListTasks",
2336 "id": "t-2",
2337 "params": {}
2338 }),
2339 )
2340 .await;
2341 assert!(
2342 v.get("result").is_some(),
2343 "tenant header on the upgrade request must reach the resolver: {v}"
2344 );
2345 }
2346
2347 #[tokio::test]
2349 async fn ws_version_mismatch_rejects_handshake() {
2350 let addr = spawn_ws_server().await;
2351
2352 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2353 req.headers_mut()
2354 .insert("a2a-version", "2.0".parse().unwrap());
2355 let outcome = tokio_tungstenite::connect_async(req).await;
2356 assert!(
2357 outcome.is_err(),
2358 "handshake with A2A-Version 2.0 must be rejected"
2359 );
2360
2361 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2363 req.headers_mut()
2364 .insert("a2a-version", "1.0".parse().unwrap());
2365 assert!(
2366 tokio_tungstenite::connect_async(req).await.is_ok(),
2367 "handshake with A2A-Version 1.0 must succeed"
2368 );
2369 }
2370
2371 #[tokio::test]
2382 async fn ws_missing_version_header_rejected_by_default() {
2383 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2384
2385 let addr = spawn_ws_server().await;
2386 let req = format!("ws://{addr}").into_client_request().unwrap();
2388 assert!(
2389 tokio_tungstenite::connect_async(req).await.is_err(),
2390 "a handshake with no A2A-Version header must be rejected by default \
2391 (§3.6.2 reads it as 0.3)"
2392 );
2393 }
2394
2395 #[tokio::test]
2396 async fn ws_missing_version_header_accepted_with_optout() {
2397 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2398
2399 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2400 let dispatcher =
2401 Arc::new(WebSocketDispatcher::new(handler).accept_missing_version_header());
2402 let addr = dispatcher
2403 .serve_with_addr("127.0.0.1:0")
2404 .await
2405 .expect("bind to port 0");
2406
2407 let req = format!("ws://{addr}").into_client_request().unwrap();
2408 assert!(
2409 tokio_tungstenite::connect_async(req).await.is_ok(),
2410 "accept_missing_version_header() must restore the tolerant behaviour"
2411 );
2412 }
2413
2414 #[tokio::test]
2423 async fn ws_version_rejection_body_carries_error_details() {
2424 use tokio_tungstenite::tungstenite::Error as WsError;
2425 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2426
2427 let addr = spawn_ws_server().await;
2428 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2429 req.headers_mut()
2430 .insert("a2a-version", "2.0".parse().unwrap());
2431
2432 match tokio_tungstenite::connect_async(req).await {
2433 Err(WsError::Http(resp)) => {
2434 assert_eq!(resp.status(), 400, "version rejection is HTTP 400");
2435 let body = resp.body().as_ref().expect("rejection carries a body");
2436 let text = String::from_utf8_lossy(body);
2437 let json: serde_json::Value =
2438 serde_json::from_str(&text).expect("rejection body is JSON");
2439 assert!(
2440 !json["error"]["details"].is_null(),
2441 "the AIP-193 details block must be present, got: {text}"
2442 );
2443 }
2444 other => panic!("expected an HTTP 400 rejection, got: {other:?}"),
2445 }
2446 }
2447
2448 #[tokio::test]
2462 async fn ws_lagged_stream_reports_server_error_code() {
2463 struct FloodExec;
2464 agent_executor!(FloodExec, |ctx, queue| async {
2465 for _ in 0..512 {
2466 queue
2467 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
2468 task_id: ctx.task_id.clone(),
2469 context_id: ContextId::new(ctx.context_id.clone()),
2470 status: TaskStatus::new(TaskState::Working),
2471 metadata: None,
2472 }))
2473 .await?;
2474 }
2475 Ok(())
2476 });
2477
2478 let handler = Arc::new(
2479 RequestHandlerBuilder::new(FloodExec)
2480 .with_event_queue_capacity(1)
2481 .build()
2482 .unwrap(),
2483 );
2484 let addr = Arc::new(WebSocketDispatcher::new(handler))
2485 .serve_with_addr("127.0.0.1:0")
2486 .await
2487 .expect("bind to port 0");
2488 let mut ws = ws_connect(addr).await;
2489
2490 let req = serde_json::json!({
2491 "jsonrpc": "2.0",
2492 "method": "SendStreamingMessage",
2493 "id": "lag-1",
2494 "params": {
2495 "message": {
2496 "messageId": "msg-lag-1",
2497 "role": "ROLE_USER",
2498 "parts": [{"text": "flood"}]
2499 }
2500 }
2501 })
2502 .to_string();
2503 ws.send(WsMessage::Text(req.into())).await.unwrap();
2504
2505 let found = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2506 while let Some(Ok(msg)) = ws.next().await {
2507 let Ok(text) = msg.into_text() else { continue };
2508 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2509 continue;
2510 };
2511 if let Some(code) = v["error"]["code"].as_i64() {
2512 return Some(code);
2513 }
2514 }
2515 None
2516 })
2517 .await
2518 .expect("the lagged stream must produce an error frame within 10s");
2519
2520 assert_eq!(
2521 found,
2522 Some(-32000),
2523 "a lagged stream must be reported with the JSON-RPC server-error \
2524 code -32000, not a positive code"
2525 );
2526 }
2527
2528 #[tokio::test]
2539 async fn ws_over_concurrency_limit_is_rejected_with_server_error_code() {
2540 struct BlockingExec;
2541 agent_executor!(BlockingExec, |_ctx, _queue| async {
2542 std::future::pending::<()>().await;
2544 Ok(())
2545 });
2546
2547 let handler = Arc::new(RequestHandlerBuilder::new(BlockingExec).build().unwrap());
2548 let addr = Arc::new(WebSocketDispatcher::new(handler))
2549 .serve_with_addr("127.0.0.1:0")
2550 .await
2551 .expect("bind to port 0");
2552 let mut ws = ws_connect(addr).await;
2553
2554 for i in 0..65 {
2556 let req = serde_json::json!({
2557 "jsonrpc": "2.0",
2558 "method": "SendMessage",
2559 "id": format!("busy-{i}"),
2560 "params": {
2561 "message": {
2562 "messageId": format!("msg-busy-{i}"),
2563 "role": "ROLE_USER",
2564 "parts": [{"text": "block"}]
2565 }
2566 }
2567 })
2568 .to_string();
2569 ws.send(WsMessage::Text(req.into())).await.unwrap();
2570 }
2571
2572 let code = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2574 while let Some(Ok(msg)) = ws.next().await {
2575 let Ok(text) = msg.into_text() else { continue };
2576 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2577 continue;
2578 };
2579 if let Some(c) = v["error"]["code"].as_i64() {
2580 assert!(
2581 v["error"]["message"]
2582 .as_str()
2583 .is_some_and(|m| m.contains("server busy")),
2584 "expected the back-pressure rejection, got: {v}"
2585 );
2586 return Some(c);
2587 }
2588 }
2589 None
2590 })
2591 .await
2592 .expect("the over-limit request must be answered within 10s");
2593
2594 assert_eq!(
2595 code,
2596 Some(-32000),
2597 "back-pressure must be reported with the JSON-RPC server-error \
2598 code -32000, not a positive code"
2599 );
2600 }
2601
2602 #[tokio::test]
2605 async fn ws_handshake_timeout_disconnects_stalled_peer() {
2606 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2607 let dispatcher = Arc::new(
2608 WebSocketDispatcher::new(handler)
2609 .with_handshake_timeout(std::time::Duration::from_millis(200)),
2610 );
2611 let addr = dispatcher
2612 .serve_with_addr("127.0.0.1:0")
2613 .await
2614 .expect("bind");
2615
2616 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
2618 let mut buf = [0u8; 16];
2619 let read = tokio::time::timeout(
2622 std::time::Duration::from_secs(5),
2623 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
2624 )
2625 .await
2626 .expect("server should close the stalled connection");
2627 assert!(
2628 matches!(read, Ok(0) | Err(_)),
2629 "expected EOF/reset from server, got: {read:?}"
2630 );
2631 }
2632
2633 #[tokio::test]
2635 async fn ws_binary_frame_gets_error_response() {
2636 let addr = spawn_ws_server().await;
2637 let mut ws = ws_connect(addr).await;
2638
2639 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
2640 .await
2641 .unwrap();
2642
2643 let text = read_text(&mut ws).await;
2644 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2645 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
2646 assert!(
2647 v["error"]["message"]
2648 .as_str()
2649 .unwrap_or("")
2650 .contains("binary"),
2651 "error should explain binary frames are unsupported: {v}"
2652 );
2653 }
2654}