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
59const MAX_WS_MESSAGE_SIZE: usize = 4 * 1024 * 1024;
68
69const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
76
77pub const DEFAULT_WS_IDLE_TIMEOUT: Duration = Duration::from_secs(75);
89
90pub struct WebSocketDispatcher {
110 handler: Arc<RequestHandler>,
111 handshake_timeout: Duration,
112 require_version_header: bool,
113 max_connections: Option<usize>,
114 idle_timeout: Option<Duration>,
115}
116
117impl WebSocketDispatcher {
118 #[must_use]
120 pub const fn new(handler: Arc<RequestHandler>) -> Self {
121 Self {
122 handler,
123 handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
124 require_version_header: true,
125 max_connections: None,
126 idle_timeout: None,
127 }
128 }
129
130 #[must_use]
137 pub const fn accept_missing_version_header(mut self) -> Self {
138 self.require_version_header = false;
139 self
140 }
141
142 #[must_use]
147 pub const fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
148 self.handshake_timeout = timeout;
149 self
150 }
151
152 #[must_use]
173 pub const fn with_max_connections(mut self, max: usize) -> Self {
174 self.max_connections = Some(max);
175 self
176 }
177
178 #[must_use]
209 pub const fn with_idle_timeout(mut self, timeout: Duration) -> Self {
210 self.idle_timeout = Some(timeout);
211 self
212 }
213
214 pub async fn serve(
224 self: Arc<Self>,
225 addr: impl tokio::net::ToSocketAddrs,
226 ) -> std::io::Result<()> {
227 let listener = TcpListener::bind(addr).await?;
228
229 trace_info!(
230 addr = %listener.local_addr().unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0))),
231 "A2A WebSocket server listening"
232 );
233
234 self.accept_loop(listener).await;
235 Ok(())
236 }
237
238 pub async fn serve_with_addr(
246 self: Arc<Self>,
247 addr: impl tokio::net::ToSocketAddrs,
248 ) -> std::io::Result<SocketAddr> {
249 let listener = TcpListener::bind(addr).await?;
250 let local_addr = listener.local_addr()?;
251
252 trace_info!(%local_addr, "A2A WebSocket server listening");
253
254 tokio::spawn(async move {
255 self.accept_loop(listener).await;
256 });
257
258 Ok(local_addr)
259 }
260
261 async fn accept_loop(self: Arc<Self>, listener: TcpListener) {
263 let limiter = Arc::new(tokio::sync::Semaphore::new(
267 self.max_connections
268 .unwrap_or(tokio::sync::Semaphore::MAX_PERMITS),
269 ));
270 loop {
271 let Ok(permit) = Arc::clone(&limiter).acquire_owned().await else {
272 return;
276 };
277 let (stream, _peer) = match listener.accept().await {
278 Ok(pair) => pair,
279 Err(e) => {
280 trace_warn!(error = %e, "accept() failed; retrying");
284 let backoff = crate::serve::accept_retry_backoff(&e);
285 tokio::time::sleep(backoff).await;
289 continue;
290 }
291 };
292 let dispatcher = Arc::clone(&self);
293 tokio::spawn(async move {
294 trace_debug!("WebSocket connection accepted");
295 if let Err(_e) = dispatcher.handle_connection(stream).await {
296 trace_warn!(error = %_e, "WebSocket connection error");
297 }
298 drop(permit);
302 });
303 }
304 }
305
306 #[allow(clippy::result_large_err)]
310 async fn handle_connection(&self, stream: TcpStream) -> Result<(), WsError> {
311 let _ = stream.set_nodelay(true);
314
315 let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
318 .max_message_size(Some(MAX_WS_MESSAGE_SIZE))
319 .max_frame_size(Some(MAX_WS_MESSAGE_SIZE));
320
321 let mut upgrade_headers: Option<HashMap<String, String>> = None;
325 let require_version = self.require_version_header;
326 let callback = |req: &WsUpgradeRequest, resp: WsUpgradeResponse| {
327 check_a2a_version(req, require_version)?;
328 upgrade_headers = Some(extract_upgrade_headers(req));
329 Ok(resp)
330 };
331
332 let ws_stream = tokio::time::timeout(
335 self.handshake_timeout,
336 tokio_tungstenite::accept_hdr_async_with_config(stream, callback, Some(ws_config)),
337 )
338 .await
339 .map_err(|_| WsError::HandshakeTimeout)?
340 .map_err(WsError::Handshake)?;
341
342 let headers = Arc::new(upgrade_headers.unwrap_or_default());
343
344 let (writer, reader) = ws_stream.split();
345 let writer: WsSink = Arc::new(Connection {
346 sink: tokio::sync::Mutex::new(writer),
347 activity: ActivityClock::new(),
348 });
349
350 self.read_loop(reader, &writer, &headers).await;
351
352 let mut w = writer.sink.lock().await;
355 let _ = w.close().await;
356 drop(w);
357
358 Ok(())
359 }
360
361 async fn read_loop(
363 &self,
364 mut reader: futures_util::stream::SplitStream<WebSocketStream<TcpStream>>,
365 writer: &WsSink,
366 headers: &Arc<HashMap<String, String>>,
367 ) {
368 let semaphore = Arc::new(tokio::sync::Semaphore::new(64));
370
371 writer.activity.touch();
375 while let Some(msg) = Self::next_frame(&mut reader, writer, self.idle_timeout).await {
376 writer.activity.touch();
377 match msg {
378 Ok(WsMessage::Text(text)) => {
379 let Ok(permit) = semaphore.clone().try_acquire_owned() else {
400 let err_resp = JsonRpcErrorResponse::new(
405 best_effort_request_id(&text),
406 JsonRpcError::new(
407 -32000,
408 "server busy: too many concurrent requests".to_string(),
409 ),
410 );
411 send_json(writer, &err_resp).await;
412 continue;
413 };
414
415 let writer = Arc::clone(writer);
416 let handler = Arc::clone(&self.handler);
417 let headers = Arc::clone(headers);
418 tokio::spawn(async move {
419 Box::pin(process_ws_message(&handler, &text, writer, &headers)).await;
421 drop(permit); });
423 }
424 Ok(WsMessage::Binary(_)) => {
425 let err_resp = JsonRpcErrorResponse::new(
429 None,
430 JsonRpcError::new(
431 -32700,
432 "binary frames are not supported; send JSON-RPC as text frames"
433 .to_string(),
434 ),
435 );
436 send_json(writer, &err_resp).await;
437 }
438 Ok(WsMessage::Close(_)) | Err(_) => break,
439 Ok(_) => {}
444 }
445 }
446 }
447
448 async fn next_frame(
460 reader: &mut futures_util::stream::SplitStream<WebSocketStream<TcpStream>>,
461 writer: &WsSink,
462 idle_timeout: Option<Duration>,
463 ) -> Option<Result<WsMessage, tokio_tungstenite::tungstenite::Error>> {
464 let Some(idle) = idle_timeout else {
465 return reader.next().await;
466 };
467
468 let mut pinged = false;
484 loop {
485 let idle_for = writer.activity.idle_for();
486 if idle_for >= idle {
487 trace_debug!("WebSocket connection idle past its budget; closing");
488 return None;
489 }
490 let half = idle / 2;
491 let (wait, ping_now) = if pinged || idle_for >= half {
492 (idle.saturating_sub(idle_for), false)
493 } else {
494 (half.saturating_sub(idle_for), true)
495 };
496
497 if let Ok(frame) = tokio::time::timeout(wait, reader.next()).await {
500 return frame;
501 }
502 if ping_now {
506 let mut w = writer.sink.lock().await;
507 let _ = w.send(WsMessage::Ping(Vec::new().into())).await;
510 drop(w);
511 pinged = true;
515 }
516 }
517 }
518}
519
520fn extract_upgrade_headers(req: &WsUpgradeRequest) -> HashMap<String, String> {
525 let mut map: HashMap<String, String> = req
526 .headers()
527 .iter()
528 .filter_map(|(k, v)| {
529 v.to_str()
530 .ok()
531 .map(|val| (k.as_str().to_lowercase(), val.to_owned()))
532 })
533 .collect();
534 map.insert(":path".to_owned(), req.uri().path().to_owned());
538 map
539}
540
541#[allow(clippy::result_large_err)]
549fn check_a2a_version(req: &WsUpgradeRequest, require: bool) -> Result<(), ErrorResponse> {
550 let value = req
551 .headers()
552 .get(a2a_protocol_types::A2A_VERSION_HEADER)
553 .and_then(|v| v.to_str().ok());
554 let v = value.unwrap_or("").trim();
555 if v.is_empty() {
556 if !require {
557 return Ok(());
558 }
559 } else {
561 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
562 if major == Some(1) {
563 return Ok(());
564 }
565 }
566 let a2a_err = a2a_protocol_types::error::A2aError::version_not_supported(if v.is_empty() {
570 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)"
571 .to_owned()
572 } else {
573 format!("unsupported A2A version: {v}; this server supports 1.x")
574 });
575 let mut error_obj = serde_json::json!({
576 "error": {
577 "code": a2a_err.code.http_status(),
578 "status": a2a_err.code.grpc_status(),
579 "message": a2a_err.message,
580 }
581 });
582 let details = a2a_err.error_info_data(None);
583 if !details.is_null() {
584 error_obj["error"]["details"] = details;
585 }
586 let body = error_obj.to_string();
587 let resp = tokio_tungstenite::tungstenite::http::Response::builder()
588 .status(400)
589 .header("content-type", "application/json")
590 .body(Some(body))
591 .unwrap_or_else(|_| {
592 let mut r = ErrorResponse::new(Some(String::new()));
593 *r.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::BAD_REQUEST;
594 r
595 });
596 Err(resp)
597}
598
599fn best_effort_request_id(text: &str) -> JsonRpcId {
602 let v: serde_json::Value = serde_json::from_str(text).ok()?;
603 match v.get("id") {
604 Some(serde_json::Value::Null) | None => None,
605 Some(id) => Some(id.clone()),
606 }
607}
608
609#[derive(Debug)]
611enum WsError {
612 Handshake(tokio_tungstenite::tungstenite::Error),
613 HandshakeTimeout,
614}
615
616impl std::fmt::Display for WsError {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 match self {
619 Self::Handshake(e) => write!(f, "WebSocket handshake failed: {e}"),
620 Self::HandshakeTimeout => write!(f, "WebSocket handshake timed out"),
621 }
622 }
623}
624
625struct Connection {
635 sink: tokio::sync::Mutex<SplitSink<WebSocketStream<TcpStream>, WsMessage>>,
636 activity: ActivityClock,
637}
638
639type WsSink = Arc<Connection>;
640
641struct ActivityClock {
649 start: std::time::Instant,
650 last_ms: std::sync::atomic::AtomicU64,
651}
652
653impl ActivityClock {
654 fn new() -> Self {
655 Self {
656 start: std::time::Instant::now(),
657 last_ms: std::sync::atomic::AtomicU64::new(0),
658 }
659 }
660
661 fn now_ms(&self) -> u64 {
665 u64::try_from(self.start.elapsed().as_millis()).unwrap_or(u64::MAX)
666 }
667
668 fn touch(&self) {
669 self.last_ms
670 .store(self.now_ms(), std::sync::atomic::Ordering::Relaxed);
671 }
672
673 fn idle_for(&self) -> Duration {
674 Duration::from_millis(
675 self.now_ms()
676 .saturating_sub(self.last_ms.load(std::sync::atomic::Ordering::Relaxed)),
677 )
678 }
679}
680
681#[allow(clippy::too_many_lines)]
687async fn process_ws_message(
688 handler: &RequestHandler,
689 text: &str,
690 writer: WsSink,
691 headers: &HashMap<String, String>,
692) {
693 let rpc_req: JsonRpcRequest = match serde_json::from_str(text) {
694 Ok(req) => req,
695 Err(e) => {
696 let err_resp = JsonRpcErrorResponse::new(
697 None,
698 JsonRpcError::new(-32700, format!("parse error: {e}")),
699 );
700 send_json(&writer, &err_resp).await;
701 return;
702 }
703 };
704
705 let id = rpc_req.id.to_response_id();
706
707 match rpc_req.method.as_str() {
708 "SendMessage" => {
709 dispatch_send_message(handler, &rpc_req, false, headers, id, &writer).await;
710 }
711 "SendStreamingMessage" | "message/stream" => {
712 dispatch_send_message(handler, &rpc_req, true, headers, id, &writer).await;
713 }
714 "GetTask" => {
715 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
716 Box::pin(async move {
717 let params: a2a_protocol_types::params::TaskQueryParams =
718 serde_json::from_value(p).map_err(|e| {
719 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
720 })?;
721 h.on_get_task(params, Some(hdr))
722 .await
723 .map(|r| serde_json::to_value(&r).unwrap_or_default())
724 .map_err(|e| e.to_a2a_error())
725 })
726 })
727 .await;
728 }
729 "ListTasks" => {
730 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
731 Box::pin(async move {
732 let params: a2a_protocol_types::params::ListTasksParams =
733 serde_json::from_value(p).map_err(|e| {
734 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
735 })?;
736 h.on_list_tasks(params, Some(hdr))
737 .await
738 .map(|r| serde_json::to_value(&r).unwrap_or_default())
739 .map_err(|e| e.to_a2a_error())
740 })
741 })
742 .await;
743 }
744 "CancelTask" => {
745 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
746 Box::pin(async move {
747 let params: a2a_protocol_types::params::CancelTaskParams =
748 serde_json::from_value(p).map_err(|e| {
749 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
750 })?;
751 h.on_cancel_task(params, Some(hdr))
752 .await
753 .map(|r| serde_json::to_value(&r).unwrap_or_default())
754 .map_err(|e| e.to_a2a_error())
755 })
756 })
757 .await;
758 }
759 "SubscribeToTask" => {
760 let params = match parse_params::<a2a_protocol_types::params::TaskIdParams>(
761 rpc_req.params.as_ref(),
762 ) {
763 Ok(p) => p,
764 Err(e) => {
765 send_error(&writer, id, &e).await;
766 return;
767 }
768 };
769 match handler.on_resubscribe(params, Some(headers)).await {
770 Ok(reader) => {
771 stream_events(&writer, reader, id).await;
772 }
773 Err(e) => {
774 send_error(&writer, id, &e).await;
775 }
776 }
777 }
778 "CreateTaskPushNotificationConfig" => {
779 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
780 Box::pin(async move {
781 let params: a2a_protocol_types::push::TaskPushNotificationConfig =
782 serde_json::from_value(p).map_err(|e| {
783 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
784 })?;
785 h.on_set_push_config(params, Some(hdr))
786 .await
787 .map(|r| serde_json::to_value(&r).unwrap_or_default())
788 .map_err(|e| e.to_a2a_error())
789 })
790 })
791 .await;
792 }
793 "GetTaskPushNotificationConfig" => {
794 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
795 Box::pin(async move {
796 let params: a2a_protocol_types::params::GetPushConfigParams =
797 serde_json::from_value(p).map_err(|e| {
798 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
799 })?;
800 h.on_get_push_config(params, Some(hdr))
801 .await
802 .map(|r| serde_json::to_value(&r).unwrap_or_default())
803 .map_err(|e| e.to_a2a_error())
804 })
805 })
806 .await;
807 }
808 "ListTaskPushNotificationConfigs" => {
809 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
810 Box::pin(async move {
811 let params: a2a_protocol_types::params::ListPushConfigsParams =
812 serde_json::from_value(p).map_err(|e| {
813 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
814 })?;
815 h.on_list_push_configs(¶ms.task_id, params.tenant.as_deref(), Some(hdr))
816 .await
817 .map(|configs| {
818 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
819 configs,
820 next_page_token: None,
821 };
822 serde_json::to_value(&resp).unwrap_or_default()
823 })
824 .map_err(|e| e.to_a2a_error())
825 })
826 })
827 .await;
828 }
829 "DeleteTaskPushNotificationConfig" => {
830 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
831 Box::pin(async move {
832 let params: a2a_protocol_types::params::DeletePushConfigParams =
833 serde_json::from_value(p).map_err(|e| {
834 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
835 })?;
836 h.on_delete_push_config(params, Some(hdr))
837 .await
838 .map(|()| serde_json::json!({}))
839 .map_err(|e| e.to_a2a_error())
840 })
841 })
842 .await;
843 }
844 "GetExtendedAgentCard" => {
845 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, _p, hdr| {
846 Box::pin(async move {
847 h.on_get_extended_agent_card(Some(hdr))
848 .await
849 .map(|r| serde_json::to_value(&r).unwrap_or_default())
850 .map_err(|e| e.to_a2a_error())
851 })
852 })
853 .await;
854 }
855 other => {
856 let err = ServerError::MethodNotFound(other.to_owned());
857 send_error(&writer, id, &err).await;
858 }
859 }
860}
861
862async fn dispatch_send_message(
864 handler: &RequestHandler,
865 rpc_req: &JsonRpcRequest,
866 streaming: bool,
867 headers: &HashMap<String, String>,
868 id: JsonRpcId,
869 writer: &WsSink,
870) {
871 let params = match parse_params::<a2a_protocol_types::params::MessageSendParams>(
872 rpc_req.params.as_ref(),
873 ) {
874 Ok(p) => p,
875 Err(e) => {
876 send_error(writer, id, &e).await;
877 return;
878 }
879 };
880
881 match handler
882 .on_send_message(params, streaming, Some(headers))
883 .await
884 {
885 Ok(SendMessageResult::Response(resp)) => {
886 let result = serde_json::to_value(&resp).unwrap_or(serde_json::Value::Null);
887 let success = JsonRpcSuccessResponse {
888 jsonrpc: JsonRpcVersion,
889 id,
890 result,
891 };
892 send_json(writer, &success).await;
893 }
894 Ok(SendMessageResult::Stream(reader)) => {
895 stream_events(writer, reader, id).await;
896 }
897 Err(e) => {
898 send_error(writer, id, &e).await;
899 }
900 }
901}
902
903async fn stream_events(
905 writer: &WsSink,
906 mut reader: crate::streaming::InMemoryQueueReader,
907 id: JsonRpcId,
908) {
909 while let Some(event) = reader.read().await {
910 match event {
911 Ok(stream_resp) => {
912 let envelope = JsonRpcSuccessResponse {
915 jsonrpc: JsonRpcVersion,
916 id: id.clone(),
917 result: stream_resp,
918 };
919 let json = serde_json::to_string(&envelope).unwrap_or_default();
920 let mut w = writer.sink.lock().await;
921 if w.send(WsMessage::Text(json.into())).await.is_err() {
922 return; }
924 drop(w);
925 writer.activity.touch();
928 }
929 Err(e) => {
930 let err_resp =
931 JsonRpcErrorResponse::new(id.clone(), JsonRpcError::new(-32000, e.to_string()));
932 send_json(writer, &err_resp).await;
933 return;
934 }
935 }
936 }
937
938 let success = JsonRpcSuccessResponse {
940 jsonrpc: JsonRpcVersion,
941 id,
942 result: serde_json::json!({"status": "stream_complete"}),
943 };
944 send_json(writer, &success).await;
945}
946
947async fn dispatch_simple<'a, F>(
949 handler: &'a RequestHandler,
950 rpc_req: &JsonRpcRequest,
951 id: JsonRpcId,
952 headers: &'a HashMap<String, String>,
953 writer: &WsSink,
954 f: F,
955) where
956 F: FnOnce(
957 &'a RequestHandler,
958 serde_json::Value,
959 &'a HashMap<String, String>,
960 ) -> std::pin::Pin<
961 Box<
962 dyn std::future::Future<
963 Output = Result<serde_json::Value, a2a_protocol_types::error::A2aError>,
964 > + Send
965 + 'a,
966 >,
967 >,
968{
969 let params = rpc_req.params.clone().unwrap_or(serde_json::Value::Null);
970 match f(handler, params, headers).await {
971 Ok(result) => {
972 let success = JsonRpcSuccessResponse {
973 jsonrpc: JsonRpcVersion,
974 id,
975 result,
976 };
977 send_json(writer, &success).await;
978 }
979 Err(e) => {
980 let err_resp =
981 JsonRpcErrorResponse::new(id, JsonRpcError::new(e.code.as_i32(), e.message));
982 send_json(writer, &err_resp).await;
983 }
984 }
985}
986
987async fn send_json<T: serde::Serialize + Sync>(writer: &WsSink, value: &T) {
989 let json = serde_json::to_string(value).unwrap_or_default();
990 let mut w = writer.sink.lock().await;
991 let _ = w.send(WsMessage::Text(json.into())).await;
992 drop(w);
993 writer.activity.touch();
994}
995
996async fn send_error(writer: &WsSink, id: JsonRpcId, err: &ServerError) {
998 let a2a_err = err.to_a2a_error();
999 let resp = JsonRpcErrorResponse::new(
1000 id,
1001 JsonRpcError::new(a2a_err.code.as_i32(), a2a_err.message),
1002 );
1003 send_json(writer, &resp).await;
1004}
1005
1006fn parse_params<T: serde::de::DeserializeOwned>(
1008 params: Option<&serde_json::Value>,
1009) -> Result<T, ServerError> {
1010 let value = params.cloned().unwrap_or(serde_json::Value::Null);
1011 serde_json::from_value(value)
1012 .map_err(|e| ServerError::InvalidParams(format!("invalid params: {e}")))
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018
1019 #[test]
1020 fn parse_params_with_valid_json() {
1021 let value = Some(serde_json::json!({"id": "task-1"}));
1022 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1023 parse_params(value.as_ref());
1024 assert!(result.is_ok());
1025 assert_eq!(result.unwrap().id, "task-1");
1026 }
1027
1028 #[test]
1029 fn parse_params_with_none_returns_error() {
1030 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> = parse_params(None);
1031 assert!(result.is_err());
1032 }
1033
1034 #[test]
1035 fn parse_params_with_wrong_type_returns_error() {
1036 let value = Some(serde_json::json!("not an object"));
1037 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1038 parse_params(value.as_ref());
1039 assert!(result.is_err());
1040 }
1041
1042 #[test]
1044 fn ws_error_display_contains_message() {
1045 let err = WsError::Handshake(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
1046 let s = err.to_string();
1047 assert!(s.contains("WebSocket handshake failed"));
1048 }
1049
1050 #[test]
1051 fn ws_error_display_handshake_timeout() {
1052 let s = WsError::HandshakeTimeout.to_string();
1053 assert!(s.contains("timed out"), "got: {s}");
1054 }
1055
1056 #[test]
1059 fn best_effort_request_id_extracts_string_and_number() {
1060 assert_eq!(
1061 best_effort_request_id(r#"{"jsonrpc":"2.0","id":"req-1","method":"GetTask"}"#),
1062 Some(serde_json::json!("req-1"))
1063 );
1064 assert_eq!(
1065 best_effort_request_id(r#"{"jsonrpc":"2.0","id":7,"method":"GetTask"}"#),
1066 Some(serde_json::json!(7))
1067 );
1068 }
1069
1070 #[test]
1071 fn best_effort_request_id_none_for_missing_null_or_invalid() {
1072 assert_eq!(best_effort_request_id(r#"{"jsonrpc":"2.0"}"#), None);
1073 assert_eq!(best_effort_request_id(r#"{"id":null}"#), None);
1074 assert_eq!(best_effort_request_id("not json {{"), None);
1075 }
1076
1077 #[test]
1079 fn websocket_dispatcher_new() {
1080 use crate::RequestHandlerBuilder;
1081 use crate::agent_executor;
1082 use std::sync::Arc;
1083 struct DummyExec;
1084 agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
1085 let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
1086 let _dispatcher = WebSocketDispatcher::new(handler);
1087 }
1088
1089 use crate::RequestHandlerBuilder;
1092 use crate::agent_executor;
1093 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
1094 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
1095 use futures_util::{SinkExt, StreamExt};
1096
1097 struct EchoExec;
1098 agent_executor!(EchoExec, |ctx, queue| async {
1099 queue
1100 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1101 task_id: ctx.task_id.clone(),
1102 context_id: ContextId::new(ctx.context_id.clone()),
1103 status: TaskStatus::new(TaskState::Working),
1104 metadata: None,
1105 }))
1106 .await?;
1107 queue
1108 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1109 task_id: ctx.task_id.clone(),
1110 context_id: ContextId::new(ctx.context_id.clone()),
1111 status: TaskStatus::new(TaskState::Completed),
1112 metadata: None,
1113 }))
1114 .await?;
1115 Ok(())
1116 });
1117
1118 async fn spawn_ws_server() -> std::net::SocketAddr {
1119 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1120 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1121 dispatcher
1122 .serve_with_addr("127.0.0.1:0")
1123 .await
1124 .expect("bind to port 0")
1125 }
1126
1127 #[tokio::test(flavor = "multi_thread")]
1151 async fn a_silent_peer_is_closed_once_its_idle_budget_is_spent() {
1152 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1153 let mut ws = ws_connect(addr).await;
1154
1155 tokio::time::sleep(Duration::from_secs(4)).await;
1156
1157 let ended = tokio::time::timeout(Duration::from_secs(5), async {
1160 loop {
1161 match ws.next().await {
1162 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1163 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1164 Some(Ok(other)) => panic!("unexpected frame on an idle connection: {other:?}"),
1165 }
1166 }
1167 })
1168 .await;
1169
1170 assert!(
1171 ended.is_ok(),
1172 "a connection idle past two 2s budgets must be closed; it was still open"
1173 );
1174 }
1175
1176 #[tokio::test(flavor = "multi_thread")]
1189 async fn a_peer_that_answers_the_keepalive_keeps_its_connection() {
1190 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1191 let mut ws = ws_connect(addr).await;
1192
1193 let died = tokio::time::timeout(Duration::from_secs(6), async {
1196 loop {
1197 match ws.next().await {
1198 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1199 other => return other,
1200 }
1201 }
1202 })
1203 .await;
1204 assert!(
1205 died.is_err(),
1206 "a responsive peer must not be closed; got {died:?}"
1207 );
1208
1209 ws.send(WsMessage::Text(send_message_json("keepalive").into()))
1211 .await
1212 .expect("send after three idle budgets");
1213 let text = tokio::time::timeout(Duration::from_secs(5), async {
1214 loop {
1215 match ws.next().await {
1216 Some(Ok(WsMessage::Text(t))) => return t,
1217 Some(Ok(_)) => {}
1218 other => panic!("connection died mid-request: {other:?}"),
1219 }
1220 }
1221 })
1222 .await
1223 .expect("a response within 5s");
1224 assert!(
1225 text.contains("\"result\""),
1226 "expected a JSON-RPC result, got: {text}"
1227 );
1228 }
1229
1230 #[tokio::test(flavor = "multi_thread")]
1243 async fn the_defaults_are_no_idle_timeout_and_no_connection_ceiling() {
1244 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1245 let default = WebSocketDispatcher::new(handler);
1246 assert!(
1247 default.idle_timeout.is_none(),
1248 "the idle timeout is opt-in: a WebSocket subscription may be legitimately \
1249 silent for hours, and a default that closes it is a knob nobody enables"
1250 );
1251 assert!(
1252 default.max_connections.is_none(),
1253 "unbounded by default, matching ServeConfig::max_connections"
1254 );
1255
1256 let addr = spawn_ws_server().await;
1257 let mut ws = ws_connect(addr).await;
1258 let died = tokio::time::timeout(Duration::from_secs(3), ws.next()).await;
1259 assert!(
1260 died.is_err(),
1261 "with no idle timeout nothing should arrive or close: {died:?}"
1262 );
1263 ws.send(WsMessage::Text(send_message_json("still-here").into()))
1264 .await
1265 .expect("send after idling");
1266 let text = read_text(&mut ws).await;
1267 assert!(
1268 text.contains("\"result\""),
1269 "expected a result, got: {text}"
1270 );
1271 }
1272
1273 #[tokio::test(flavor = "multi_thread")]
1280 async fn max_connections_bounds_accepted_connections() {
1281 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1282
1283 let addr = spawn_ws_server_with(|d| d.with_max_connections(2)).await;
1284
1285 let first = ws_connect(addr).await;
1286 let second = ws_connect(addr).await;
1287
1288 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1289 req.headers_mut()
1290 .insert("a2a-version", "1.0".parse().expect("header"));
1291 let third = tokio::time::timeout(
1292 Duration::from_secs(2),
1293 tokio_tungstenite::connect_async(req),
1294 )
1295 .await;
1296 assert!(
1297 third.is_err(),
1298 "the third handshake must not complete against a ceiling of 2; got {:?}",
1299 third.map(|r| r.is_ok())
1300 );
1301
1302 drop(first);
1306 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1307 req.headers_mut()
1308 .insert("a2a-version", "1.0".parse().expect("header"));
1309 let replacement = tokio::time::timeout(
1310 Duration::from_secs(5),
1311 tokio_tungstenite::connect_async(req),
1312 )
1313 .await;
1314 assert!(
1315 matches!(replacement, Ok(Ok(_))),
1316 "a freed permit must admit the next peer"
1317 );
1318 drop(second);
1319 }
1320
1321 struct TrickleExec {
1325 count: usize,
1326 step: Duration,
1327 }
1328
1329 impl crate::executor::AgentExecutor for TrickleExec {
1330 fn execute<'a>(
1331 &'a self,
1332 ctx: &'a crate::request_context::RequestContext,
1333 queue: &'a dyn crate::streaming::EventQueueWriter,
1334 ) -> std::pin::Pin<
1335 Box<
1336 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1337 + Send
1338 + 'a,
1339 >,
1340 > {
1341 Box::pin(async move {
1342 let status = |state: TaskState| {
1343 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1344 task_id: ctx.task_id.clone(),
1345 context_id: ContextId::new(ctx.context_id.clone()),
1346 status: TaskStatus::new(state),
1347 metadata: None,
1348 })
1349 };
1350 for _ in 0..self.count {
1351 tokio::time::sleep(self.step).await;
1352 queue.write(status(TaskState::Working)).await?;
1353 }
1354 queue.write(status(TaskState::Completed)).await?;
1355 Ok(())
1356 })
1357 }
1358 }
1359
1360 #[tokio::test(flavor = "multi_thread")]
1371 async fn a_stream_to_a_silent_consumer_is_pinged_once() {
1372 let idle = Duration::from_millis(1_000);
1373 let executor = TrickleExec {
1376 count: 12,
1377 step: Duration::from_millis(250),
1378 };
1379 let addr = spawn_ws_server_with_executor(executor, |d| d.with_idle_timeout(idle)).await;
1380 let mut ws = ws_connect(addr).await;
1381
1382 ws.send(WsMessage::Text(streaming_message_json("trickle").into()))
1383 .await
1384 .expect("send the streaming request");
1385
1386 tokio::time::sleep(Duration::from_millis(3_000) + 2 * idle).await;
1389
1390 let mut pings = 0;
1391 let mut texts = 0;
1392 let closed = tokio::time::timeout(Duration::from_secs(5), async {
1393 loop {
1394 match ws.next().await {
1395 Some(Ok(WsMessage::Ping(_))) => pings += 1,
1396 Some(Ok(WsMessage::Text(_))) => texts += 1,
1397 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1398 Some(Ok(other)) => panic!("unexpected frame: {other:?}"),
1399 }
1400 }
1401 })
1402 .await;
1403 assert!(
1404 closed.is_ok(),
1405 "a consumer that never reads must be closed once the stream ends and the idle budget passes"
1406 );
1407 assert!(
1408 texts >= 12,
1409 "the whole stream was written while the consumer read nothing; got {texts} text frames"
1410 );
1411 assert_eq!(
1412 pings, 1,
1413 "one Ping at the half-budget, and no more without an inbound frame"
1414 );
1415 }
1416
1417 fn streaming_message_json(id: &str) -> String {
1418 serde_json::json!({
1419 "jsonrpc": "2.0",
1420 "method": "SendStreamingMessage",
1421 "id": id,
1422 "params": {
1423 "message": {
1424 "messageId": "msg-stream",
1425 "role": "ROLE_USER",
1426 "parts": [{"text": "stream"}]
1427 }
1428 }
1429 })
1430 .to_string()
1431 }
1432
1433 async fn spawn_ws_server_with(
1435 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1436 ) -> std::net::SocketAddr {
1437 spawn_ws_server_with_executor(EchoExec, configure).await
1438 }
1439
1440 async fn spawn_ws_server_with_executor(
1442 executor: impl crate::executor::AgentExecutor + 'static,
1443 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1444 ) -> std::net::SocketAddr {
1445 let handler = Arc::new(RequestHandlerBuilder::new(executor).build().unwrap());
1446 let dispatcher = Arc::new(configure(WebSocketDispatcher::new(handler)));
1447 dispatcher
1448 .serve_with_addr("127.0.0.1:0")
1449 .await
1450 .expect("bind to port 0")
1451 }
1452
1453 async fn ws_connect(
1454 addr: std::net::SocketAddr,
1455 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
1456 {
1457 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1458 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1459 req.headers_mut()
1460 .insert("a2a-version", "1.0".parse().expect("header"));
1461 let (ws, _) = tokio_tungstenite::connect_async(req)
1462 .await
1463 .expect("ws connect");
1464 ws
1465 }
1466
1467 async fn read_text(
1469 ws: &mut tokio_tungstenite::WebSocketStream<
1470 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1471 >,
1472 ) -> String {
1473 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1474 .await
1475 .expect("timeout waiting for WS frame")
1476 .expect("stream ended")
1477 .expect("ws error");
1478 msg.into_text()
1479 .expect("not a text frame")
1480 .as_str()
1481 .to_owned()
1482 }
1483
1484 fn send_message_json(id: &str) -> String {
1485 serde_json::json!({
1486 "jsonrpc": "2.0",
1487 "method": "SendMessage",
1488 "id": id,
1489 "params": {
1490 "message": {
1491 "messageId": "msg-1",
1492 "role": "ROLE_USER",
1493 "parts": [{"text": "hello"}]
1494 }
1495 }
1496 })
1497 .to_string()
1498 }
1499
1500 #[tokio::test]
1502 async fn ws_send_message_success() {
1503 let addr = spawn_ws_server().await;
1504 let mut ws = ws_connect(addr).await;
1505
1506 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
1507 .await
1508 .unwrap();
1509
1510 let text = read_text(&mut ws).await;
1511 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1512 assert_eq!(v["id"], "sm-1");
1513 assert!(v.get("result").is_some(), "expected result key: {text}");
1515 }
1516
1517 #[tokio::test]
1519 async fn ws_get_task_not_found() {
1520 let addr = spawn_ws_server().await;
1521 let mut ws = ws_connect(addr).await;
1522
1523 let req = serde_json::json!({
1524 "jsonrpc": "2.0",
1525 "method": "GetTask",
1526 "id": "gt-1",
1527 "params": {"id": "nonexistent"}
1528 })
1529 .to_string();
1530 ws.send(WsMessage::Text(req.into())).await.unwrap();
1531
1532 let text = read_text(&mut ws).await;
1533 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1534 assert!(v.get("error").is_some(), "expected error: {text}");
1535 }
1536
1537 #[tokio::test]
1539 async fn ws_list_tasks_success() {
1540 let addr = spawn_ws_server().await;
1541 let mut ws = ws_connect(addr).await;
1542
1543 let req = serde_json::json!({
1544 "jsonrpc": "2.0",
1545 "method": "ListTasks",
1546 "id": "lt-1",
1547 "params": {}
1548 })
1549 .to_string();
1550 ws.send(WsMessage::Text(req.into())).await.unwrap();
1551
1552 let text = read_text(&mut ws).await;
1553 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1554 assert_eq!(v["id"], "lt-1");
1555 assert!(v.get("result").is_some(), "expected result: {text}");
1556 }
1557
1558 #[tokio::test]
1560 async fn ws_cancel_task_not_found() {
1561 let addr = spawn_ws_server().await;
1562 let mut ws = ws_connect(addr).await;
1563
1564 let req = serde_json::json!({
1565 "jsonrpc": "2.0",
1566 "method": "CancelTask",
1567 "id": "ct-1",
1568 "params": {"id": "nonexistent"}
1569 })
1570 .to_string();
1571 ws.send(WsMessage::Text(req.into())).await.unwrap();
1572
1573 let text = read_text(&mut ws).await;
1574 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1575 assert!(v.get("error").is_some(), "expected error: {text}");
1576 }
1577
1578 #[tokio::test]
1580 async fn ws_subscribe_task_not_found() {
1581 let addr = spawn_ws_server().await;
1582 let mut ws = ws_connect(addr).await;
1583
1584 let req = serde_json::json!({
1585 "jsonrpc": "2.0",
1586 "method": "SubscribeToTask",
1587 "id": "sub-1",
1588 "params": {"id": "nonexistent"}
1589 })
1590 .to_string();
1591 ws.send(WsMessage::Text(req.into())).await.unwrap();
1592
1593 let text = read_text(&mut ws).await;
1594 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1595 assert!(v.get("error").is_some(), "expected error: {text}");
1596 }
1597
1598 #[tokio::test]
1600 async fn ws_unknown_method_error() {
1601 let addr = spawn_ws_server().await;
1602 let mut ws = ws_connect(addr).await;
1603
1604 let req = serde_json::json!({
1605 "jsonrpc": "2.0",
1606 "method": "FooBar",
1607 "id": "unk-1",
1608 "params": {}
1609 })
1610 .to_string();
1611 ws.send(WsMessage::Text(req.into())).await.unwrap();
1612
1613 let text = read_text(&mut ws).await;
1614 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1615 assert!(v.get("error").is_some(), "expected error: {text}");
1616 let msg = v["error"]["message"].as_str().unwrap_or("");
1617 assert!(
1618 msg.to_lowercase().contains("method")
1619 || msg.to_lowercase().contains("not found")
1620 || msg.to_lowercase().contains("unsupported"),
1621 "error message should mention method not found: {msg}"
1622 );
1623 }
1624
1625 #[tokio::test]
1627 async fn ws_invalid_json_parse_error() {
1628 let addr = spawn_ws_server().await;
1629 let mut ws = ws_connect(addr).await;
1630
1631 ws.send(WsMessage::Text("this is not json {{".into()))
1632 .await
1633 .unwrap();
1634
1635 let text = read_text(&mut ws).await;
1636 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1637 assert_eq!(v["error"]["code"], -32700, "expected parse error code");
1638 }
1639
1640 #[tokio::test]
1648 async fn ws_oversized_message_rejected() {
1649 let addr = spawn_ws_server().await;
1650 let mut ws = ws_connect(addr).await;
1651
1652 let big = "x".repeat(4 * 1024 * 1024 + 1);
1654 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1658 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1661 .await
1662 .expect("server should react to the oversized message");
1663 match outcome {
1664 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1665 Some(Ok(frame)) => panic!(
1666 "server must not answer an oversized message with a frame, got: {frame:?}"
1667 ),
1668 }
1669 }
1670 }
1671
1672 #[tokio::test]
1676 async fn ws_large_message_under_cap_still_processed() {
1677 let addr = spawn_ws_server().await;
1678 let mut ws = ws_connect(addr).await;
1679
1680 let big = "x".repeat(3 * 1024 * 1024);
1681 ws.send(WsMessage::Text(big.into())).await.unwrap();
1682
1683 let text = read_text(&mut ws).await;
1684 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1685 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1686 }
1687
1688 #[tokio::test]
1690 async fn ws_ping_pong_response() {
1691 let addr = spawn_ws_server().await;
1692 let mut ws = ws_connect(addr).await;
1693
1694 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1695
1696 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1697 loop {
1698 let msg = ws.next().await.unwrap().unwrap();
1699 if let WsMessage::Pong(data) = msg {
1700 return data;
1701 }
1702 }
1703 })
1704 .await
1705 .expect("should get pong within 3s");
1706 assert_eq!(pong, vec![42, 43]);
1707 }
1708
1709 #[tokio::test]
1711 async fn ws_get_task_invalid_params() {
1712 let addr = spawn_ws_server().await;
1713 let mut ws = ws_connect(addr).await;
1714
1715 let req = serde_json::json!({
1717 "jsonrpc": "2.0",
1718 "method": "GetTask",
1719 "id": "gti-1",
1720 "params": {"wrong_field": 123}
1721 })
1722 .to_string();
1723 ws.send(WsMessage::Text(req.into())).await.unwrap();
1724
1725 let text = read_text(&mut ws).await;
1726 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1727 assert!(
1728 v.get("error").is_some(),
1729 "expected error for bad params: {text}"
1730 );
1731 }
1732
1733 #[tokio::test]
1735 async fn ws_send_streaming_message_events() {
1736 let addr = spawn_ws_server().await;
1737 let mut ws = ws_connect(addr).await;
1738
1739 let req = serde_json::json!({
1740 "jsonrpc": "2.0",
1741 "method": "SendStreamingMessage",
1742 "id": "ssm-1",
1743 "params": {
1744 "message": {
1745 "messageId": "msg-stream-1",
1746 "role": "ROLE_USER",
1747 "parts": [{"text": "stream me"}]
1748 }
1749 }
1750 })
1751 .to_string();
1752 ws.send(WsMessage::Text(req.into())).await.unwrap();
1753
1754 let mut frames = Vec::new();
1756 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1757 loop {
1758 let msg = ws.next().await.unwrap().unwrap();
1759 let text = msg.into_text().unwrap();
1760 let done = text.contains("stream_complete");
1761 frames.push(text);
1762 if done {
1763 break;
1764 }
1765 }
1766 });
1767 timeout.await.expect("streaming should complete within 5s");
1768
1769 assert!(
1771 frames.len() >= 3,
1772 "expected >= 3 frames, got {}: {:?}",
1773 frames.len(),
1774 frames
1775 );
1776 assert!(frames.last().unwrap().contains("stream_complete"));
1778 }
1779
1780 #[tokio::test]
1782 async fn ws_send_message_invalid_params() {
1783 let addr = spawn_ws_server().await;
1784 let mut ws = ws_connect(addr).await;
1785
1786 let req = serde_json::json!({
1787 "jsonrpc": "2.0",
1788 "method": "SendMessage",
1789 "id": "smi-1",
1790 "params": {"not_message": true}
1791 })
1792 .to_string();
1793 ws.send(WsMessage::Text(req.into())).await.unwrap();
1794
1795 let text = read_text(&mut ws).await;
1796 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1797 assert!(
1798 v.get("error").is_some(),
1799 "expected error for bad send params: {text}"
1800 );
1801 }
1802
1803 #[tokio::test]
1805 async fn ws_subscribe_invalid_params() {
1806 let addr = spawn_ws_server().await;
1807 let mut ws = ws_connect(addr).await;
1808
1809 let req = serde_json::json!({
1810 "jsonrpc": "2.0",
1811 "method": "SubscribeToTask",
1812 "id": "subi-1",
1813 "params": {}
1814 })
1815 .to_string();
1816 ws.send(WsMessage::Text(req.into())).await.unwrap();
1817
1818 let text = read_text(&mut ws).await;
1819 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1820 assert!(
1821 v.get("error").is_some(),
1822 "expected error for bad subscribe params: {text}"
1823 );
1824 }
1825
1826 #[tokio::test]
1828 async fn ws_cancel_task_invalid_params() {
1829 let addr = spawn_ws_server().await;
1830 let mut ws = ws_connect(addr).await;
1831
1832 let req = serde_json::json!({
1833 "jsonrpc": "2.0",
1834 "method": "CancelTask",
1835 "id": "cti-1",
1836 "params": {"wrong": 1}
1837 })
1838 .to_string();
1839 ws.send(WsMessage::Text(req.into())).await.unwrap();
1840
1841 let text = read_text(&mut ws).await;
1842 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1843 assert!(v.get("error").is_some(), "expected error: {text}");
1844 }
1845
1846 #[tokio::test]
1848 async fn ws_list_tasks_with_filters() {
1849 let addr = spawn_ws_server().await;
1850 let mut ws = ws_connect(addr).await;
1851
1852 let req = serde_json::json!({
1853 "jsonrpc": "2.0",
1854 "method": "ListTasks",
1855 "id": "ltf-1",
1856 "params": {
1857 "contextId": "ctx-1",
1858 "pageSize": 10
1859 }
1860 })
1861 .to_string();
1862 ws.send(WsMessage::Text(req.into())).await.unwrap();
1863
1864 let text = read_text(&mut ws).await;
1865 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1866 assert_eq!(v["id"], "ltf-1");
1867 assert!(v.get("result").is_some(), "expected result: {text}");
1868 }
1869
1870 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
1873
1874 async fn ws_call(
1876 ws: &mut tokio_tungstenite::WebSocketStream<
1877 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1878 >,
1879 req: serde_json::Value,
1880 ) -> serde_json::Value {
1881 ws.send(WsMessage::Text(req.to_string().into()))
1882 .await
1883 .expect("send");
1884 let text = read_text(ws).await;
1885 serde_json::from_str(&text).expect("response should be JSON")
1886 }
1887
1888 #[tokio::test]
1893 async fn ws_legacy_method_names_rejected() {
1894 let addr = spawn_ws_server().await;
1895 let mut ws = ws_connect(addr).await;
1896
1897 for legacy in ["message/send", "tasks/list", "tasks/get"] {
1898 let v = ws_call(
1899 &mut ws,
1900 serde_json::json!({
1901 "jsonrpc": "2.0",
1902 "method": legacy,
1903 "id": format!("legacy-{legacy}"),
1904 "params": {}
1905 }),
1906 )
1907 .await;
1908 assert_eq!(
1909 v["error"]["code"].as_i64(),
1910 Some(-32601),
1911 "v0.3-style name {legacy} must be MethodNotFound: {v}"
1912 );
1913 }
1914 }
1915
1916 #[tokio::test]
1919 #[allow(clippy::too_many_lines)]
1920 async fn ws_push_config_methods_routed() {
1921 use crate::push::PushSender;
1922 use a2a_protocol_types::push::TaskPushNotificationConfig;
1923
1924 struct NoopSender;
1925 impl PushSender for NoopSender {
1926 fn send<'a>(
1927 &'a self,
1928 _url: &'a str,
1929 _event: &'a StreamResponse,
1930 _config: &'a TaskPushNotificationConfig,
1931 ) -> std::pin::Pin<
1932 Box<
1933 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1934 + Send
1935 + 'a,
1936 >,
1937 > {
1938 Box::pin(async { Ok(()) })
1939 }
1940 fn allows_private_urls(&self) -> bool {
1941 true
1942 }
1943 }
1944
1945 let handler = Arc::new(
1946 RequestHandlerBuilder::new(EchoExec)
1947 .with_push_sender(NoopSender)
1948 .build()
1949 .unwrap(),
1950 );
1951 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1952 let addr = dispatcher
1953 .serve_with_addr("127.0.0.1:0")
1954 .await
1955 .expect("bind");
1956 let mut ws = ws_connect(addr).await;
1957
1958 let v = ws_call(
1960 &mut ws,
1961 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
1962 )
1963 .await;
1964 let task_id = v["result"]["task"]["id"]
1965 .as_str()
1966 .expect("task id in send result")
1967 .to_owned();
1968
1969 let v = ws_call(
1971 &mut ws,
1972 serde_json::json!({
1973 "jsonrpc": "2.0",
1974 "method": "CreateTaskPushNotificationConfig",
1975 "id": "pc-1",
1976 "params": {
1977 "taskId": task_id,
1978 "url": "https://example.com/hook"
1979 }
1980 }),
1981 )
1982 .await;
1983 assert!(v.get("result").is_some(), "set push config failed: {v}");
1984 let config_id = v["result"]["id"]
1985 .as_str()
1986 .expect("server-assigned config id")
1987 .to_owned();
1988
1989 let v = ws_call(
1991 &mut ws,
1992 serde_json::json!({
1993 "jsonrpc": "2.0",
1994 "method": "GetTaskPushNotificationConfig",
1995 "id": "pc-2",
1996 "params": {"taskId": task_id, "id": config_id}
1997 }),
1998 )
1999 .await;
2000 assert!(v.get("result").is_some(), "get push config failed: {v}");
2001
2002 let v = ws_call(
2004 &mut ws,
2005 serde_json::json!({
2006 "jsonrpc": "2.0",
2007 "method": "ListTaskPushNotificationConfigs",
2008 "id": "pc-3",
2009 "params": {"taskId": task_id}
2010 }),
2011 )
2012 .await;
2013 assert!(v.get("result").is_some(), "list push configs failed: {v}");
2014 assert!(
2015 v["result"]["configs"].is_array(),
2016 "expected configs array: {v}"
2017 );
2018
2019 let v = ws_call(
2021 &mut ws,
2022 serde_json::json!({
2023 "jsonrpc": "2.0",
2024 "method": "DeleteTaskPushNotificationConfig",
2025 "id": "pc-4",
2026 "params": {"taskId": task_id, "id": config_id}
2027 }),
2028 )
2029 .await;
2030 assert!(v.get("result").is_some(), "delete push config failed: {v}");
2031 }
2032
2033 #[tokio::test]
2036 async fn ws_get_extended_agent_card_routed() {
2037 let addr = spawn_ws_server().await;
2038 let mut ws = ws_connect(addr).await;
2039
2040 let v = ws_call(
2041 &mut ws,
2042 serde_json::json!({
2043 "jsonrpc": "2.0",
2044 "method": "GetExtendedAgentCard",
2045 "id": "card-1",
2046 "params": {}
2047 }),
2048 )
2049 .await;
2050 let err = v.get("error").expect("expected an error response");
2053 assert_ne!(
2054 err["code"], -32601,
2055 "GetExtendedAgentCard must be routed, got: {v}"
2056 );
2057 }
2058
2059 #[tokio::test]
2063 async fn ws_upgrade_headers_drive_tenant_resolution() {
2064 use crate::tenant_resolver::HeaderTenantResolver;
2065
2066 let handler = Arc::new(
2067 RequestHandlerBuilder::new(EchoExec)
2068 .with_tenant_resolver(HeaderTenantResolver::default())
2069 .require_resolved_tenant()
2070 .build()
2071 .unwrap(),
2072 );
2073 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
2074 let addr = dispatcher
2075 .serve_with_addr("127.0.0.1:0")
2076 .await
2077 .expect("bind");
2078
2079 let mut ws = ws_connect(addr).await;
2081 let v = ws_call(
2082 &mut ws,
2083 serde_json::json!({
2084 "jsonrpc": "2.0",
2085 "method": "ListTasks",
2086 "id": "t-1",
2087 "params": {}
2088 }),
2089 )
2090 .await;
2091 let err = v.get("error").expect("headerless request must be rejected");
2092 let msg = err["message"].as_str().unwrap_or("");
2093 assert!(
2094 msg.contains("tenant"),
2095 "expected strict-tenancy rejection, got: {v}"
2096 );
2097
2098 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2100 req.headers_mut()
2101 .insert("a2a-version", "1.0".parse().unwrap());
2102 req.headers_mut()
2103 .insert("x-tenant-id", "acme".parse().unwrap());
2104 let (mut ws, _) = tokio_tungstenite::connect_async(req)
2105 .await
2106 .expect("connect");
2107 let v = ws_call(
2108 &mut ws,
2109 serde_json::json!({
2110 "jsonrpc": "2.0",
2111 "method": "ListTasks",
2112 "id": "t-2",
2113 "params": {}
2114 }),
2115 )
2116 .await;
2117 assert!(
2118 v.get("result").is_some(),
2119 "tenant header on the upgrade request must reach the resolver: {v}"
2120 );
2121 }
2122
2123 #[tokio::test]
2125 async fn ws_version_mismatch_rejects_handshake() {
2126 let addr = spawn_ws_server().await;
2127
2128 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2129 req.headers_mut()
2130 .insert("a2a-version", "2.0".parse().unwrap());
2131 let outcome = tokio_tungstenite::connect_async(req).await;
2132 assert!(
2133 outcome.is_err(),
2134 "handshake with A2A-Version 2.0 must be rejected"
2135 );
2136
2137 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2139 req.headers_mut()
2140 .insert("a2a-version", "1.0".parse().unwrap());
2141 assert!(
2142 tokio_tungstenite::connect_async(req).await.is_ok(),
2143 "handshake with A2A-Version 1.0 must succeed"
2144 );
2145 }
2146
2147 #[tokio::test]
2158 async fn ws_missing_version_header_rejected_by_default() {
2159 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2160
2161 let addr = spawn_ws_server().await;
2162 let req = format!("ws://{addr}").into_client_request().unwrap();
2164 assert!(
2165 tokio_tungstenite::connect_async(req).await.is_err(),
2166 "a handshake with no A2A-Version header must be rejected by default \
2167 (§3.6.2 reads it as 0.3)"
2168 );
2169 }
2170
2171 #[tokio::test]
2172 async fn ws_missing_version_header_accepted_with_optout() {
2173 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2174
2175 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2176 let dispatcher =
2177 Arc::new(WebSocketDispatcher::new(handler).accept_missing_version_header());
2178 let addr = dispatcher
2179 .serve_with_addr("127.0.0.1:0")
2180 .await
2181 .expect("bind to port 0");
2182
2183 let req = format!("ws://{addr}").into_client_request().unwrap();
2184 assert!(
2185 tokio_tungstenite::connect_async(req).await.is_ok(),
2186 "accept_missing_version_header() must restore the tolerant behaviour"
2187 );
2188 }
2189
2190 #[tokio::test]
2199 async fn ws_version_rejection_body_carries_error_details() {
2200 use tokio_tungstenite::tungstenite::Error as WsError;
2201 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2202
2203 let addr = spawn_ws_server().await;
2204 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2205 req.headers_mut()
2206 .insert("a2a-version", "2.0".parse().unwrap());
2207
2208 match tokio_tungstenite::connect_async(req).await {
2209 Err(WsError::Http(resp)) => {
2210 assert_eq!(resp.status(), 400, "version rejection is HTTP 400");
2211 let body = resp.body().as_ref().expect("rejection carries a body");
2212 let text = String::from_utf8_lossy(body);
2213 let json: serde_json::Value =
2214 serde_json::from_str(&text).expect("rejection body is JSON");
2215 assert!(
2216 !json["error"]["details"].is_null(),
2217 "the AIP-193 details block must be present, got: {text}"
2218 );
2219 }
2220 other => panic!("expected an HTTP 400 rejection, got: {other:?}"),
2221 }
2222 }
2223
2224 #[tokio::test]
2238 async fn ws_lagged_stream_reports_server_error_code() {
2239 struct FloodExec;
2240 agent_executor!(FloodExec, |ctx, queue| async {
2241 for _ in 0..512 {
2242 queue
2243 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
2244 task_id: ctx.task_id.clone(),
2245 context_id: ContextId::new(ctx.context_id.clone()),
2246 status: TaskStatus::new(TaskState::Working),
2247 metadata: None,
2248 }))
2249 .await?;
2250 }
2251 Ok(())
2252 });
2253
2254 let handler = Arc::new(
2255 RequestHandlerBuilder::new(FloodExec)
2256 .with_event_queue_capacity(1)
2257 .build()
2258 .unwrap(),
2259 );
2260 let addr = Arc::new(WebSocketDispatcher::new(handler))
2261 .serve_with_addr("127.0.0.1:0")
2262 .await
2263 .expect("bind to port 0");
2264 let mut ws = ws_connect(addr).await;
2265
2266 let req = serde_json::json!({
2267 "jsonrpc": "2.0",
2268 "method": "SendStreamingMessage",
2269 "id": "lag-1",
2270 "params": {
2271 "message": {
2272 "messageId": "msg-lag-1",
2273 "role": "ROLE_USER",
2274 "parts": [{"text": "flood"}]
2275 }
2276 }
2277 })
2278 .to_string();
2279 ws.send(WsMessage::Text(req.into())).await.unwrap();
2280
2281 let found = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2282 while let Some(Ok(msg)) = ws.next().await {
2283 let Ok(text) = msg.into_text() else { continue };
2284 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2285 continue;
2286 };
2287 if let Some(code) = v["error"]["code"].as_i64() {
2288 return Some(code);
2289 }
2290 }
2291 None
2292 })
2293 .await
2294 .expect("the lagged stream must produce an error frame within 10s");
2295
2296 assert_eq!(
2297 found,
2298 Some(-32000),
2299 "a lagged stream must be reported with the JSON-RPC server-error \
2300 code -32000, not a positive code"
2301 );
2302 }
2303
2304 #[tokio::test]
2315 async fn ws_over_concurrency_limit_is_rejected_with_server_error_code() {
2316 struct BlockingExec;
2317 agent_executor!(BlockingExec, |_ctx, _queue| async {
2318 std::future::pending::<()>().await;
2320 Ok(())
2321 });
2322
2323 let handler = Arc::new(RequestHandlerBuilder::new(BlockingExec).build().unwrap());
2324 let addr = Arc::new(WebSocketDispatcher::new(handler))
2325 .serve_with_addr("127.0.0.1:0")
2326 .await
2327 .expect("bind to port 0");
2328 let mut ws = ws_connect(addr).await;
2329
2330 for i in 0..65 {
2332 let req = serde_json::json!({
2333 "jsonrpc": "2.0",
2334 "method": "SendMessage",
2335 "id": format!("busy-{i}"),
2336 "params": {
2337 "message": {
2338 "messageId": format!("msg-busy-{i}"),
2339 "role": "ROLE_USER",
2340 "parts": [{"text": "block"}]
2341 }
2342 }
2343 })
2344 .to_string();
2345 ws.send(WsMessage::Text(req.into())).await.unwrap();
2346 }
2347
2348 let code = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2350 while let Some(Ok(msg)) = ws.next().await {
2351 let Ok(text) = msg.into_text() else { continue };
2352 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2353 continue;
2354 };
2355 if let Some(c) = v["error"]["code"].as_i64() {
2356 assert!(
2357 v["error"]["message"]
2358 .as_str()
2359 .is_some_and(|m| m.contains("server busy")),
2360 "expected the back-pressure rejection, got: {v}"
2361 );
2362 return Some(c);
2363 }
2364 }
2365 None
2366 })
2367 .await
2368 .expect("the over-limit request must be answered within 10s");
2369
2370 assert_eq!(
2371 code,
2372 Some(-32000),
2373 "back-pressure must be reported with the JSON-RPC server-error \
2374 code -32000, not a positive code"
2375 );
2376 }
2377
2378 #[tokio::test]
2381 async fn ws_handshake_timeout_disconnects_stalled_peer() {
2382 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2383 let dispatcher = Arc::new(
2384 WebSocketDispatcher::new(handler)
2385 .with_handshake_timeout(std::time::Duration::from_millis(200)),
2386 );
2387 let addr = dispatcher
2388 .serve_with_addr("127.0.0.1:0")
2389 .await
2390 .expect("bind");
2391
2392 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
2394 let mut buf = [0u8; 16];
2395 let read = tokio::time::timeout(
2398 std::time::Duration::from_secs(5),
2399 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
2400 )
2401 .await
2402 .expect("server should close the stalled connection");
2403 assert!(
2404 matches!(read, Ok(0) | Err(_)),
2405 "expected EOF/reset from server, got: {read:?}"
2406 );
2407 }
2408
2409 #[tokio::test]
2411 async fn ws_binary_frame_gets_error_response() {
2412 let addr = spawn_ws_server().await;
2413 let mut ws = ws_connect(addr).await;
2414
2415 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
2416 .await
2417 .unwrap();
2418
2419 let text = read_text(&mut ws).await;
2420 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2421 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
2422 assert!(
2423 v["error"]["message"]
2424 .as_str()
2425 .unwrap_or("")
2426 .contains("binary"),
2427 "error should explain binary frames are unsupported: {v}"
2428 );
2429 }
2430}