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.map(|e| e.event) {
911 Ok(stream_resp) => {
912 let envelope = JsonRpcSuccessResponse {
920 jsonrpc: JsonRpcVersion,
921 id: id.clone(),
922 result: stream_resp,
923 };
924 let json = serde_json::to_string(&envelope).unwrap_or_default();
925 let mut w = writer.sink.lock().await;
926 if w.send(WsMessage::Text(json.into())).await.is_err() {
927 return; }
929 drop(w);
930 writer.activity.touch();
933 }
934 Err(e) => {
935 let err_resp =
936 JsonRpcErrorResponse::new(id.clone(), JsonRpcError::new(-32000, e.to_string()));
937 send_json(writer, &err_resp).await;
938 return;
939 }
940 }
941 }
942
943 let success = JsonRpcSuccessResponse {
945 jsonrpc: JsonRpcVersion,
946 id,
947 result: serde_json::json!({"status": "stream_complete"}),
948 };
949 send_json(writer, &success).await;
950}
951
952async fn dispatch_simple<'a, F>(
954 handler: &'a RequestHandler,
955 rpc_req: &JsonRpcRequest,
956 id: JsonRpcId,
957 headers: &'a HashMap<String, String>,
958 writer: &WsSink,
959 f: F,
960) where
961 F: FnOnce(
962 &'a RequestHandler,
963 serde_json::Value,
964 &'a HashMap<String, String>,
965 ) -> std::pin::Pin<
966 Box<
967 dyn std::future::Future<
968 Output = Result<serde_json::Value, a2a_protocol_types::error::A2aError>,
969 > + Send
970 + 'a,
971 >,
972 >,
973{
974 let params = rpc_req.params.clone().unwrap_or(serde_json::Value::Null);
975 match f(handler, params, headers).await {
976 Ok(result) => {
977 let success = JsonRpcSuccessResponse {
978 jsonrpc: JsonRpcVersion,
979 id,
980 result,
981 };
982 send_json(writer, &success).await;
983 }
984 Err(e) => {
985 let err_resp =
986 JsonRpcErrorResponse::new(id, JsonRpcError::new(e.code.as_i32(), e.message));
987 send_json(writer, &err_resp).await;
988 }
989 }
990}
991
992async fn send_json<T: serde::Serialize + Sync>(writer: &WsSink, value: &T) {
994 let json = serde_json::to_string(value).unwrap_or_default();
995 let mut w = writer.sink.lock().await;
996 let _ = w.send(WsMessage::Text(json.into())).await;
997 drop(w);
998 writer.activity.touch();
999}
1000
1001async fn send_error(writer: &WsSink, id: JsonRpcId, err: &ServerError) {
1003 let a2a_err = err.to_a2a_error();
1004 let resp = JsonRpcErrorResponse::new(
1005 id,
1006 JsonRpcError::new(a2a_err.code.as_i32(), a2a_err.message),
1007 );
1008 send_json(writer, &resp).await;
1009}
1010
1011fn parse_params<T: serde::de::DeserializeOwned>(
1013 params: Option<&serde_json::Value>,
1014) -> Result<T, ServerError> {
1015 let value = params.cloned().unwrap_or(serde_json::Value::Null);
1016 serde_json::from_value(value)
1017 .map_err(|e| ServerError::InvalidParams(format!("invalid params: {e}")))
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::*;
1023
1024 #[test]
1025 fn parse_params_with_valid_json() {
1026 let value = Some(serde_json::json!({"id": "task-1"}));
1027 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1028 parse_params(value.as_ref());
1029 assert!(result.is_ok());
1030 assert_eq!(result.unwrap().id, "task-1");
1031 }
1032
1033 #[test]
1034 fn parse_params_with_none_returns_error() {
1035 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> = parse_params(None);
1036 assert!(result.is_err());
1037 }
1038
1039 #[test]
1040 fn parse_params_with_wrong_type_returns_error() {
1041 let value = Some(serde_json::json!("not an object"));
1042 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
1043 parse_params(value.as_ref());
1044 assert!(result.is_err());
1045 }
1046
1047 #[test]
1049 fn ws_error_display_contains_message() {
1050 let err = WsError::Handshake(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
1051 let s = err.to_string();
1052 assert!(s.contains("WebSocket handshake failed"));
1053 }
1054
1055 #[test]
1056 fn ws_error_display_handshake_timeout() {
1057 let s = WsError::HandshakeTimeout.to_string();
1058 assert!(s.contains("timed out"), "got: {s}");
1059 }
1060
1061 #[test]
1064 fn best_effort_request_id_extracts_string_and_number() {
1065 assert_eq!(
1066 best_effort_request_id(r#"{"jsonrpc":"2.0","id":"req-1","method":"GetTask"}"#),
1067 Some(serde_json::json!("req-1"))
1068 );
1069 assert_eq!(
1070 best_effort_request_id(r#"{"jsonrpc":"2.0","id":7,"method":"GetTask"}"#),
1071 Some(serde_json::json!(7))
1072 );
1073 }
1074
1075 #[test]
1076 fn best_effort_request_id_none_for_missing_null_or_invalid() {
1077 assert_eq!(best_effort_request_id(r#"{"jsonrpc":"2.0"}"#), None);
1078 assert_eq!(best_effort_request_id(r#"{"id":null}"#), None);
1079 assert_eq!(best_effort_request_id("not json {{"), None);
1080 }
1081
1082 #[test]
1084 fn websocket_dispatcher_new() {
1085 use crate::RequestHandlerBuilder;
1086 use crate::agent_executor;
1087 use std::sync::Arc;
1088 struct DummyExec;
1089 agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
1090 let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
1091 let _dispatcher = WebSocketDispatcher::new(handler);
1092 }
1093
1094 use crate::RequestHandlerBuilder;
1097 use crate::agent_executor;
1098 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
1099 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
1100 use futures_util::{SinkExt, StreamExt};
1101
1102 struct EchoExec;
1103 agent_executor!(EchoExec, |ctx, queue| async {
1104 queue
1105 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1106 task_id: ctx.task_id.clone(),
1107 context_id: ContextId::new(ctx.context_id.clone()),
1108 status: TaskStatus::new(TaskState::Working),
1109 metadata: None,
1110 }))
1111 .await?;
1112 queue
1113 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1114 task_id: ctx.task_id.clone(),
1115 context_id: ContextId::new(ctx.context_id.clone()),
1116 status: TaskStatus::new(TaskState::Completed),
1117 metadata: None,
1118 }))
1119 .await?;
1120 Ok(())
1121 });
1122
1123 async fn spawn_ws_server() -> std::net::SocketAddr {
1124 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1125 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1126 dispatcher
1127 .serve_with_addr("127.0.0.1:0")
1128 .await
1129 .expect("bind to port 0")
1130 }
1131
1132 #[tokio::test(flavor = "multi_thread")]
1156 async fn a_silent_peer_is_closed_once_its_idle_budget_is_spent() {
1157 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1158 let mut ws = ws_connect(addr).await;
1159
1160 tokio::time::sleep(Duration::from_secs(4)).await;
1161
1162 let ended = tokio::time::timeout(Duration::from_secs(5), async {
1165 loop {
1166 match ws.next().await {
1167 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1168 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1169 Some(Ok(other)) => panic!("unexpected frame on an idle connection: {other:?}"),
1170 }
1171 }
1172 })
1173 .await;
1174
1175 assert!(
1176 ended.is_ok(),
1177 "a connection idle past two 2s budgets must be closed; it was still open"
1178 );
1179 }
1180
1181 #[tokio::test(flavor = "multi_thread")]
1194 async fn a_peer_that_answers_the_keepalive_keeps_its_connection() {
1195 let addr = spawn_ws_server_with(|d| d.with_idle_timeout(Duration::from_secs(2))).await;
1196 let mut ws = ws_connect(addr).await;
1197
1198 let died = tokio::time::timeout(Duration::from_secs(6), async {
1201 loop {
1202 match ws.next().await {
1203 Some(Ok(WsMessage::Ping(_) | WsMessage::Pong(_))) => {}
1204 other => return other,
1205 }
1206 }
1207 })
1208 .await;
1209 assert!(
1210 died.is_err(),
1211 "a responsive peer must not be closed; got {died:?}"
1212 );
1213
1214 ws.send(WsMessage::Text(send_message_json("keepalive").into()))
1216 .await
1217 .expect("send after three idle budgets");
1218 let text = tokio::time::timeout(Duration::from_secs(5), async {
1219 loop {
1220 match ws.next().await {
1221 Some(Ok(WsMessage::Text(t))) => return t,
1222 Some(Ok(_)) => {}
1223 other => panic!("connection died mid-request: {other:?}"),
1224 }
1225 }
1226 })
1227 .await
1228 .expect("a response within 5s");
1229 assert!(
1230 text.contains("\"result\""),
1231 "expected a JSON-RPC result, got: {text}"
1232 );
1233 }
1234
1235 #[tokio::test(flavor = "multi_thread")]
1248 async fn the_defaults_are_no_idle_timeout_and_no_connection_ceiling() {
1249 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1250 let default = WebSocketDispatcher::new(handler);
1251 assert!(
1252 default.idle_timeout.is_none(),
1253 "the idle timeout is opt-in: a WebSocket subscription may be legitimately \
1254 silent for hours, and a default that closes it is a knob nobody enables"
1255 );
1256 assert!(
1257 default.max_connections.is_none(),
1258 "unbounded by default, matching ServeConfig::max_connections"
1259 );
1260
1261 let addr = spawn_ws_server().await;
1262 let mut ws = ws_connect(addr).await;
1263 let died = tokio::time::timeout(Duration::from_secs(3), ws.next()).await;
1264 assert!(
1265 died.is_err(),
1266 "with no idle timeout nothing should arrive or close: {died:?}"
1267 );
1268 ws.send(WsMessage::Text(send_message_json("still-here").into()))
1269 .await
1270 .expect("send after idling");
1271 let text = read_text(&mut ws).await;
1272 assert!(
1273 text.contains("\"result\""),
1274 "expected a result, got: {text}"
1275 );
1276 }
1277
1278 #[tokio::test(flavor = "multi_thread")]
1285 async fn max_connections_bounds_accepted_connections() {
1286 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1287
1288 let addr = spawn_ws_server_with(|d| d.with_max_connections(2)).await;
1289
1290 let first = ws_connect(addr).await;
1291 let second = ws_connect(addr).await;
1292
1293 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1294 req.headers_mut()
1295 .insert("a2a-version", "1.0".parse().expect("header"));
1296 let third = tokio::time::timeout(
1297 Duration::from_secs(2),
1298 tokio_tungstenite::connect_async(req),
1299 )
1300 .await;
1301 assert!(
1302 third.is_err(),
1303 "the third handshake must not complete against a ceiling of 2; got {:?}",
1304 third.map(|r| r.is_ok())
1305 );
1306
1307 drop(first);
1311 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1312 req.headers_mut()
1313 .insert("a2a-version", "1.0".parse().expect("header"));
1314 let replacement = tokio::time::timeout(
1315 Duration::from_secs(5),
1316 tokio_tungstenite::connect_async(req),
1317 )
1318 .await;
1319 assert!(
1320 matches!(replacement, Ok(Ok(_))),
1321 "a freed permit must admit the next peer"
1322 );
1323 drop(second);
1324 }
1325
1326 struct TrickleExec {
1330 count: usize,
1331 step: Duration,
1332 }
1333
1334 impl crate::executor::AgentExecutor for TrickleExec {
1335 fn execute<'a>(
1336 &'a self,
1337 ctx: &'a crate::request_context::RequestContext,
1338 queue: &'a dyn crate::streaming::EventQueueWriter,
1339 ) -> std::pin::Pin<
1340 Box<
1341 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1342 + Send
1343 + 'a,
1344 >,
1345 > {
1346 Box::pin(async move {
1347 let status = |state: TaskState| {
1348 StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1349 task_id: ctx.task_id.clone(),
1350 context_id: ContextId::new(ctx.context_id.clone()),
1351 status: TaskStatus::new(state),
1352 metadata: None,
1353 })
1354 };
1355 for _ in 0..self.count {
1356 tokio::time::sleep(self.step).await;
1357 queue.write(status(TaskState::Working)).await?;
1358 }
1359 queue.write(status(TaskState::Completed)).await?;
1360 Ok(())
1361 })
1362 }
1363 }
1364
1365 #[tokio::test(flavor = "multi_thread")]
1376 async fn a_stream_to_a_silent_consumer_is_pinged_once() {
1377 let idle = Duration::from_millis(1_000);
1378 let executor = TrickleExec {
1381 count: 12,
1382 step: Duration::from_millis(250),
1383 };
1384 let addr = spawn_ws_server_with_executor(executor, |d| d.with_idle_timeout(idle)).await;
1385 let mut ws = ws_connect(addr).await;
1386
1387 ws.send(WsMessage::Text(streaming_message_json("trickle").into()))
1388 .await
1389 .expect("send the streaming request");
1390
1391 tokio::time::sleep(Duration::from_millis(3_000) + 2 * idle).await;
1394
1395 let mut pings = 0;
1396 let mut texts = 0;
1397 let closed = tokio::time::timeout(Duration::from_secs(5), async {
1398 loop {
1399 match ws.next().await {
1400 Some(Ok(WsMessage::Ping(_))) => pings += 1,
1401 Some(Ok(WsMessage::Text(_))) => texts += 1,
1402 Some(Ok(WsMessage::Close(_)) | Err(_)) | None => return,
1403 Some(Ok(other)) => panic!("unexpected frame: {other:?}"),
1404 }
1405 }
1406 })
1407 .await;
1408 assert!(
1409 closed.is_ok(),
1410 "a consumer that never reads must be closed once the stream ends and the idle budget passes"
1411 );
1412 assert!(
1413 texts >= 12,
1414 "the whole stream was written while the consumer read nothing; got {texts} text frames"
1415 );
1416 assert_eq!(
1417 pings, 1,
1418 "one Ping at the half-budget, and no more without an inbound frame"
1419 );
1420 }
1421
1422 fn streaming_message_json(id: &str) -> String {
1423 serde_json::json!({
1424 "jsonrpc": "2.0",
1425 "method": "SendStreamingMessage",
1426 "id": id,
1427 "params": {
1428 "message": {
1429 "messageId": "msg-stream",
1430 "role": "ROLE_USER",
1431 "parts": [{"text": "stream"}]
1432 }
1433 }
1434 })
1435 .to_string()
1436 }
1437
1438 async fn spawn_ws_server_with(
1440 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1441 ) -> std::net::SocketAddr {
1442 spawn_ws_server_with_executor(EchoExec, configure).await
1443 }
1444
1445 async fn spawn_ws_server_with_executor(
1447 executor: impl crate::executor::AgentExecutor + 'static,
1448 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1449 ) -> std::net::SocketAddr {
1450 let handler = Arc::new(RequestHandlerBuilder::new(executor).build().unwrap());
1451 let dispatcher = Arc::new(configure(WebSocketDispatcher::new(handler)));
1452 dispatcher
1453 .serve_with_addr("127.0.0.1:0")
1454 .await
1455 .expect("bind to port 0")
1456 }
1457
1458 async fn ws_connect(
1459 addr: std::net::SocketAddr,
1460 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
1461 {
1462 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1463 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1464 req.headers_mut()
1465 .insert("a2a-version", "1.0".parse().expect("header"));
1466 let (ws, _) = tokio_tungstenite::connect_async(req)
1467 .await
1468 .expect("ws connect");
1469 ws
1470 }
1471
1472 async fn read_text(
1474 ws: &mut tokio_tungstenite::WebSocketStream<
1475 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1476 >,
1477 ) -> String {
1478 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1479 .await
1480 .expect("timeout waiting for WS frame")
1481 .expect("stream ended")
1482 .expect("ws error");
1483 msg.into_text()
1484 .expect("not a text frame")
1485 .as_str()
1486 .to_owned()
1487 }
1488
1489 fn send_message_json(id: &str) -> String {
1490 serde_json::json!({
1491 "jsonrpc": "2.0",
1492 "method": "SendMessage",
1493 "id": id,
1494 "params": {
1495 "message": {
1496 "messageId": "msg-1",
1497 "role": "ROLE_USER",
1498 "parts": [{"text": "hello"}]
1499 }
1500 }
1501 })
1502 .to_string()
1503 }
1504
1505 #[tokio::test]
1507 async fn ws_send_message_success() {
1508 let addr = spawn_ws_server().await;
1509 let mut ws = ws_connect(addr).await;
1510
1511 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
1512 .await
1513 .unwrap();
1514
1515 let text = read_text(&mut ws).await;
1516 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1517 assert_eq!(v["id"], "sm-1");
1518 assert!(v.get("result").is_some(), "expected result key: {text}");
1520 }
1521
1522 #[tokio::test]
1524 async fn ws_get_task_not_found() {
1525 let addr = spawn_ws_server().await;
1526 let mut ws = ws_connect(addr).await;
1527
1528 let req = serde_json::json!({
1529 "jsonrpc": "2.0",
1530 "method": "GetTask",
1531 "id": "gt-1",
1532 "params": {"id": "nonexistent"}
1533 })
1534 .to_string();
1535 ws.send(WsMessage::Text(req.into())).await.unwrap();
1536
1537 let text = read_text(&mut ws).await;
1538 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1539 assert!(v.get("error").is_some(), "expected error: {text}");
1540 }
1541
1542 #[tokio::test]
1544 async fn ws_list_tasks_success() {
1545 let addr = spawn_ws_server().await;
1546 let mut ws = ws_connect(addr).await;
1547
1548 let req = serde_json::json!({
1549 "jsonrpc": "2.0",
1550 "method": "ListTasks",
1551 "id": "lt-1",
1552 "params": {}
1553 })
1554 .to_string();
1555 ws.send(WsMessage::Text(req.into())).await.unwrap();
1556
1557 let text = read_text(&mut ws).await;
1558 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1559 assert_eq!(v["id"], "lt-1");
1560 assert!(v.get("result").is_some(), "expected result: {text}");
1561 }
1562
1563 #[tokio::test]
1565 async fn ws_cancel_task_not_found() {
1566 let addr = spawn_ws_server().await;
1567 let mut ws = ws_connect(addr).await;
1568
1569 let req = serde_json::json!({
1570 "jsonrpc": "2.0",
1571 "method": "CancelTask",
1572 "id": "ct-1",
1573 "params": {"id": "nonexistent"}
1574 })
1575 .to_string();
1576 ws.send(WsMessage::Text(req.into())).await.unwrap();
1577
1578 let text = read_text(&mut ws).await;
1579 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1580 assert!(v.get("error").is_some(), "expected error: {text}");
1581 }
1582
1583 #[tokio::test]
1585 async fn ws_subscribe_task_not_found() {
1586 let addr = spawn_ws_server().await;
1587 let mut ws = ws_connect(addr).await;
1588
1589 let req = serde_json::json!({
1590 "jsonrpc": "2.0",
1591 "method": "SubscribeToTask",
1592 "id": "sub-1",
1593 "params": {"id": "nonexistent"}
1594 })
1595 .to_string();
1596 ws.send(WsMessage::Text(req.into())).await.unwrap();
1597
1598 let text = read_text(&mut ws).await;
1599 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1600 assert!(v.get("error").is_some(), "expected error: {text}");
1601 }
1602
1603 #[tokio::test]
1605 async fn ws_unknown_method_error() {
1606 let addr = spawn_ws_server().await;
1607 let mut ws = ws_connect(addr).await;
1608
1609 let req = serde_json::json!({
1610 "jsonrpc": "2.0",
1611 "method": "FooBar",
1612 "id": "unk-1",
1613 "params": {}
1614 })
1615 .to_string();
1616 ws.send(WsMessage::Text(req.into())).await.unwrap();
1617
1618 let text = read_text(&mut ws).await;
1619 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1620 assert!(v.get("error").is_some(), "expected error: {text}");
1621 let msg = v["error"]["message"].as_str().unwrap_or("");
1622 assert!(
1623 msg.to_lowercase().contains("method")
1624 || msg.to_lowercase().contains("not found")
1625 || msg.to_lowercase().contains("unsupported"),
1626 "error message should mention method not found: {msg}"
1627 );
1628 }
1629
1630 #[tokio::test]
1632 async fn ws_invalid_json_parse_error() {
1633 let addr = spawn_ws_server().await;
1634 let mut ws = ws_connect(addr).await;
1635
1636 ws.send(WsMessage::Text("this is not json {{".into()))
1637 .await
1638 .unwrap();
1639
1640 let text = read_text(&mut ws).await;
1641 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1642 assert_eq!(v["error"]["code"], -32700, "expected parse error code");
1643 }
1644
1645 #[tokio::test]
1653 async fn ws_oversized_message_rejected() {
1654 let addr = spawn_ws_server().await;
1655 let mut ws = ws_connect(addr).await;
1656
1657 let big = "x".repeat(4 * 1024 * 1024 + 1);
1659 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1663 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1666 .await
1667 .expect("server should react to the oversized message");
1668 match outcome {
1669 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1670 Some(Ok(frame)) => panic!(
1671 "server must not answer an oversized message with a frame, got: {frame:?}"
1672 ),
1673 }
1674 }
1675 }
1676
1677 #[tokio::test]
1681 async fn ws_large_message_under_cap_still_processed() {
1682 let addr = spawn_ws_server().await;
1683 let mut ws = ws_connect(addr).await;
1684
1685 let big = "x".repeat(3 * 1024 * 1024);
1686 ws.send(WsMessage::Text(big.into())).await.unwrap();
1687
1688 let text = read_text(&mut ws).await;
1689 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1690 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1691 }
1692
1693 #[tokio::test]
1695 async fn ws_ping_pong_response() {
1696 let addr = spawn_ws_server().await;
1697 let mut ws = ws_connect(addr).await;
1698
1699 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1700
1701 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1702 loop {
1703 let msg = ws.next().await.unwrap().unwrap();
1704 if let WsMessage::Pong(data) = msg {
1705 return data;
1706 }
1707 }
1708 })
1709 .await
1710 .expect("should get pong within 3s");
1711 assert_eq!(pong, vec![42, 43]);
1712 }
1713
1714 #[tokio::test]
1716 async fn ws_get_task_invalid_params() {
1717 let addr = spawn_ws_server().await;
1718 let mut ws = ws_connect(addr).await;
1719
1720 let req = serde_json::json!({
1722 "jsonrpc": "2.0",
1723 "method": "GetTask",
1724 "id": "gti-1",
1725 "params": {"wrong_field": 123}
1726 })
1727 .to_string();
1728 ws.send(WsMessage::Text(req.into())).await.unwrap();
1729
1730 let text = read_text(&mut ws).await;
1731 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1732 assert!(
1733 v.get("error").is_some(),
1734 "expected error for bad params: {text}"
1735 );
1736 }
1737
1738 #[tokio::test]
1740 async fn ws_send_streaming_message_events() {
1741 let addr = spawn_ws_server().await;
1742 let mut ws = ws_connect(addr).await;
1743
1744 let req = serde_json::json!({
1745 "jsonrpc": "2.0",
1746 "method": "SendStreamingMessage",
1747 "id": "ssm-1",
1748 "params": {
1749 "message": {
1750 "messageId": "msg-stream-1",
1751 "role": "ROLE_USER",
1752 "parts": [{"text": "stream me"}]
1753 }
1754 }
1755 })
1756 .to_string();
1757 ws.send(WsMessage::Text(req.into())).await.unwrap();
1758
1759 let mut frames = Vec::new();
1761 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1762 loop {
1763 let msg = ws.next().await.unwrap().unwrap();
1764 let text = msg.into_text().unwrap();
1765 let done = text.contains("stream_complete");
1766 frames.push(text);
1767 if done {
1768 break;
1769 }
1770 }
1771 });
1772 timeout.await.expect("streaming should complete within 5s");
1773
1774 assert!(
1776 frames.len() >= 3,
1777 "expected >= 3 frames, got {}: {:?}",
1778 frames.len(),
1779 frames
1780 );
1781 assert!(frames.last().unwrap().contains("stream_complete"));
1783 }
1784
1785 #[tokio::test]
1787 async fn ws_send_message_invalid_params() {
1788 let addr = spawn_ws_server().await;
1789 let mut ws = ws_connect(addr).await;
1790
1791 let req = serde_json::json!({
1792 "jsonrpc": "2.0",
1793 "method": "SendMessage",
1794 "id": "smi-1",
1795 "params": {"not_message": true}
1796 })
1797 .to_string();
1798 ws.send(WsMessage::Text(req.into())).await.unwrap();
1799
1800 let text = read_text(&mut ws).await;
1801 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1802 assert!(
1803 v.get("error").is_some(),
1804 "expected error for bad send params: {text}"
1805 );
1806 }
1807
1808 #[tokio::test]
1810 async fn ws_subscribe_invalid_params() {
1811 let addr = spawn_ws_server().await;
1812 let mut ws = ws_connect(addr).await;
1813
1814 let req = serde_json::json!({
1815 "jsonrpc": "2.0",
1816 "method": "SubscribeToTask",
1817 "id": "subi-1",
1818 "params": {}
1819 })
1820 .to_string();
1821 ws.send(WsMessage::Text(req.into())).await.unwrap();
1822
1823 let text = read_text(&mut ws).await;
1824 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1825 assert!(
1826 v.get("error").is_some(),
1827 "expected error for bad subscribe params: {text}"
1828 );
1829 }
1830
1831 #[tokio::test]
1833 async fn ws_cancel_task_invalid_params() {
1834 let addr = spawn_ws_server().await;
1835 let mut ws = ws_connect(addr).await;
1836
1837 let req = serde_json::json!({
1838 "jsonrpc": "2.0",
1839 "method": "CancelTask",
1840 "id": "cti-1",
1841 "params": {"wrong": 1}
1842 })
1843 .to_string();
1844 ws.send(WsMessage::Text(req.into())).await.unwrap();
1845
1846 let text = read_text(&mut ws).await;
1847 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1848 assert!(v.get("error").is_some(), "expected error: {text}");
1849 }
1850
1851 #[tokio::test]
1853 async fn ws_list_tasks_with_filters() {
1854 let addr = spawn_ws_server().await;
1855 let mut ws = ws_connect(addr).await;
1856
1857 let req = serde_json::json!({
1858 "jsonrpc": "2.0",
1859 "method": "ListTasks",
1860 "id": "ltf-1",
1861 "params": {
1862 "contextId": "ctx-1",
1863 "pageSize": 10
1864 }
1865 })
1866 .to_string();
1867 ws.send(WsMessage::Text(req.into())).await.unwrap();
1868
1869 let text = read_text(&mut ws).await;
1870 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1871 assert_eq!(v["id"], "ltf-1");
1872 assert!(v.get("result").is_some(), "expected result: {text}");
1873 }
1874
1875 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
1878
1879 async fn ws_call(
1881 ws: &mut tokio_tungstenite::WebSocketStream<
1882 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1883 >,
1884 req: serde_json::Value,
1885 ) -> serde_json::Value {
1886 ws.send(WsMessage::Text(req.to_string().into()))
1887 .await
1888 .expect("send");
1889 let text = read_text(ws).await;
1890 serde_json::from_str(&text).expect("response should be JSON")
1891 }
1892
1893 #[tokio::test]
1898 async fn ws_legacy_method_names_rejected() {
1899 let addr = spawn_ws_server().await;
1900 let mut ws = ws_connect(addr).await;
1901
1902 for legacy in ["message/send", "tasks/list", "tasks/get"] {
1903 let v = ws_call(
1904 &mut ws,
1905 serde_json::json!({
1906 "jsonrpc": "2.0",
1907 "method": legacy,
1908 "id": format!("legacy-{legacy}"),
1909 "params": {}
1910 }),
1911 )
1912 .await;
1913 assert_eq!(
1914 v["error"]["code"].as_i64(),
1915 Some(-32601),
1916 "v0.3-style name {legacy} must be MethodNotFound: {v}"
1917 );
1918 }
1919 }
1920
1921 #[tokio::test]
1924 #[allow(clippy::too_many_lines)]
1925 async fn ws_push_config_methods_routed() {
1926 use crate::push::PushSender;
1927 use a2a_protocol_types::push::TaskPushNotificationConfig;
1928
1929 struct NoopSender;
1930 impl PushSender for NoopSender {
1931 fn send<'a>(
1932 &'a self,
1933 _url: &'a str,
1934 _event: &'a StreamResponse,
1935 _config: &'a TaskPushNotificationConfig,
1936 ) -> std::pin::Pin<
1937 Box<
1938 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1939 + Send
1940 + 'a,
1941 >,
1942 > {
1943 Box::pin(async { Ok(()) })
1944 }
1945 fn allows_private_urls(&self) -> bool {
1946 true
1947 }
1948 }
1949
1950 let handler = Arc::new(
1951 RequestHandlerBuilder::new(EchoExec)
1952 .with_push_sender(NoopSender)
1953 .build()
1954 .unwrap(),
1955 );
1956 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1957 let addr = dispatcher
1958 .serve_with_addr("127.0.0.1:0")
1959 .await
1960 .expect("bind");
1961 let mut ws = ws_connect(addr).await;
1962
1963 let v = ws_call(
1965 &mut ws,
1966 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
1967 )
1968 .await;
1969 let task_id = v["result"]["task"]["id"]
1970 .as_str()
1971 .expect("task id in send result")
1972 .to_owned();
1973
1974 let v = ws_call(
1976 &mut ws,
1977 serde_json::json!({
1978 "jsonrpc": "2.0",
1979 "method": "CreateTaskPushNotificationConfig",
1980 "id": "pc-1",
1981 "params": {
1982 "taskId": task_id,
1983 "url": "https://example.com/hook"
1984 }
1985 }),
1986 )
1987 .await;
1988 assert!(v.get("result").is_some(), "set push config failed: {v}");
1989 let config_id = v["result"]["id"]
1990 .as_str()
1991 .expect("server-assigned config id")
1992 .to_owned();
1993
1994 let v = ws_call(
1996 &mut ws,
1997 serde_json::json!({
1998 "jsonrpc": "2.0",
1999 "method": "GetTaskPushNotificationConfig",
2000 "id": "pc-2",
2001 "params": {"taskId": task_id, "id": config_id}
2002 }),
2003 )
2004 .await;
2005 assert!(v.get("result").is_some(), "get push config failed: {v}");
2006
2007 let v = ws_call(
2009 &mut ws,
2010 serde_json::json!({
2011 "jsonrpc": "2.0",
2012 "method": "ListTaskPushNotificationConfigs",
2013 "id": "pc-3",
2014 "params": {"taskId": task_id}
2015 }),
2016 )
2017 .await;
2018 assert!(v.get("result").is_some(), "list push configs failed: {v}");
2019 assert!(
2020 v["result"]["configs"].is_array(),
2021 "expected configs array: {v}"
2022 );
2023
2024 let v = ws_call(
2026 &mut ws,
2027 serde_json::json!({
2028 "jsonrpc": "2.0",
2029 "method": "DeleteTaskPushNotificationConfig",
2030 "id": "pc-4",
2031 "params": {"taskId": task_id, "id": config_id}
2032 }),
2033 )
2034 .await;
2035 assert!(v.get("result").is_some(), "delete push config failed: {v}");
2036 }
2037
2038 #[tokio::test]
2041 async fn ws_get_extended_agent_card_routed() {
2042 let addr = spawn_ws_server().await;
2043 let mut ws = ws_connect(addr).await;
2044
2045 let v = ws_call(
2046 &mut ws,
2047 serde_json::json!({
2048 "jsonrpc": "2.0",
2049 "method": "GetExtendedAgentCard",
2050 "id": "card-1",
2051 "params": {}
2052 }),
2053 )
2054 .await;
2055 let err = v.get("error").expect("expected an error response");
2058 assert_ne!(
2059 err["code"], -32601,
2060 "GetExtendedAgentCard must be routed, got: {v}"
2061 );
2062 }
2063
2064 #[tokio::test]
2068 async fn ws_upgrade_headers_drive_tenant_resolution() {
2069 use crate::tenant_resolver::HeaderTenantResolver;
2070
2071 let handler = Arc::new(
2072 RequestHandlerBuilder::new(EchoExec)
2073 .with_tenant_resolver(HeaderTenantResolver::default())
2074 .require_resolved_tenant()
2075 .build()
2076 .unwrap(),
2077 );
2078 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
2079 let addr = dispatcher
2080 .serve_with_addr("127.0.0.1:0")
2081 .await
2082 .expect("bind");
2083
2084 let mut ws = ws_connect(addr).await;
2086 let v = ws_call(
2087 &mut ws,
2088 serde_json::json!({
2089 "jsonrpc": "2.0",
2090 "method": "ListTasks",
2091 "id": "t-1",
2092 "params": {}
2093 }),
2094 )
2095 .await;
2096 let err = v.get("error").expect("headerless request must be rejected");
2097 let msg = err["message"].as_str().unwrap_or("");
2098 assert!(
2099 msg.contains("tenant"),
2100 "expected strict-tenancy rejection, got: {v}"
2101 );
2102
2103 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2105 req.headers_mut()
2106 .insert("a2a-version", "1.0".parse().unwrap());
2107 req.headers_mut()
2108 .insert("x-tenant-id", "acme".parse().unwrap());
2109 let (mut ws, _) = tokio_tungstenite::connect_async(req)
2110 .await
2111 .expect("connect");
2112 let v = ws_call(
2113 &mut ws,
2114 serde_json::json!({
2115 "jsonrpc": "2.0",
2116 "method": "ListTasks",
2117 "id": "t-2",
2118 "params": {}
2119 }),
2120 )
2121 .await;
2122 assert!(
2123 v.get("result").is_some(),
2124 "tenant header on the upgrade request must reach the resolver: {v}"
2125 );
2126 }
2127
2128 #[tokio::test]
2130 async fn ws_version_mismatch_rejects_handshake() {
2131 let addr = spawn_ws_server().await;
2132
2133 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2134 req.headers_mut()
2135 .insert("a2a-version", "2.0".parse().unwrap());
2136 let outcome = tokio_tungstenite::connect_async(req).await;
2137 assert!(
2138 outcome.is_err(),
2139 "handshake with A2A-Version 2.0 must be rejected"
2140 );
2141
2142 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2144 req.headers_mut()
2145 .insert("a2a-version", "1.0".parse().unwrap());
2146 assert!(
2147 tokio_tungstenite::connect_async(req).await.is_ok(),
2148 "handshake with A2A-Version 1.0 must succeed"
2149 );
2150 }
2151
2152 #[tokio::test]
2163 async fn ws_missing_version_header_rejected_by_default() {
2164 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2165
2166 let addr = spawn_ws_server().await;
2167 let req = format!("ws://{addr}").into_client_request().unwrap();
2169 assert!(
2170 tokio_tungstenite::connect_async(req).await.is_err(),
2171 "a handshake with no A2A-Version header must be rejected by default \
2172 (§3.6.2 reads it as 0.3)"
2173 );
2174 }
2175
2176 #[tokio::test]
2177 async fn ws_missing_version_header_accepted_with_optout() {
2178 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2179
2180 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2181 let dispatcher =
2182 Arc::new(WebSocketDispatcher::new(handler).accept_missing_version_header());
2183 let addr = dispatcher
2184 .serve_with_addr("127.0.0.1:0")
2185 .await
2186 .expect("bind to port 0");
2187
2188 let req = format!("ws://{addr}").into_client_request().unwrap();
2189 assert!(
2190 tokio_tungstenite::connect_async(req).await.is_ok(),
2191 "accept_missing_version_header() must restore the tolerant behaviour"
2192 );
2193 }
2194
2195 #[tokio::test]
2204 async fn ws_version_rejection_body_carries_error_details() {
2205 use tokio_tungstenite::tungstenite::Error as WsError;
2206 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2207
2208 let addr = spawn_ws_server().await;
2209 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2210 req.headers_mut()
2211 .insert("a2a-version", "2.0".parse().unwrap());
2212
2213 match tokio_tungstenite::connect_async(req).await {
2214 Err(WsError::Http(resp)) => {
2215 assert_eq!(resp.status(), 400, "version rejection is HTTP 400");
2216 let body = resp.body().as_ref().expect("rejection carries a body");
2217 let text = String::from_utf8_lossy(body);
2218 let json: serde_json::Value =
2219 serde_json::from_str(&text).expect("rejection body is JSON");
2220 assert!(
2221 !json["error"]["details"].is_null(),
2222 "the AIP-193 details block must be present, got: {text}"
2223 );
2224 }
2225 other => panic!("expected an HTTP 400 rejection, got: {other:?}"),
2226 }
2227 }
2228
2229 #[tokio::test]
2243 async fn ws_lagged_stream_reports_server_error_code() {
2244 struct FloodExec;
2245 agent_executor!(FloodExec, |ctx, queue| async {
2246 for _ in 0..512 {
2247 queue
2248 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
2249 task_id: ctx.task_id.clone(),
2250 context_id: ContextId::new(ctx.context_id.clone()),
2251 status: TaskStatus::new(TaskState::Working),
2252 metadata: None,
2253 }))
2254 .await?;
2255 }
2256 Ok(())
2257 });
2258
2259 let handler = Arc::new(
2260 RequestHandlerBuilder::new(FloodExec)
2261 .with_event_queue_capacity(1)
2262 .build()
2263 .unwrap(),
2264 );
2265 let addr = Arc::new(WebSocketDispatcher::new(handler))
2266 .serve_with_addr("127.0.0.1:0")
2267 .await
2268 .expect("bind to port 0");
2269 let mut ws = ws_connect(addr).await;
2270
2271 let req = serde_json::json!({
2272 "jsonrpc": "2.0",
2273 "method": "SendStreamingMessage",
2274 "id": "lag-1",
2275 "params": {
2276 "message": {
2277 "messageId": "msg-lag-1",
2278 "role": "ROLE_USER",
2279 "parts": [{"text": "flood"}]
2280 }
2281 }
2282 })
2283 .to_string();
2284 ws.send(WsMessage::Text(req.into())).await.unwrap();
2285
2286 let found = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2287 while let Some(Ok(msg)) = ws.next().await {
2288 let Ok(text) = msg.into_text() else { continue };
2289 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2290 continue;
2291 };
2292 if let Some(code) = v["error"]["code"].as_i64() {
2293 return Some(code);
2294 }
2295 }
2296 None
2297 })
2298 .await
2299 .expect("the lagged stream must produce an error frame within 10s");
2300
2301 assert_eq!(
2302 found,
2303 Some(-32000),
2304 "a lagged stream must be reported with the JSON-RPC server-error \
2305 code -32000, not a positive code"
2306 );
2307 }
2308
2309 #[tokio::test]
2320 async fn ws_over_concurrency_limit_is_rejected_with_server_error_code() {
2321 struct BlockingExec;
2322 agent_executor!(BlockingExec, |_ctx, _queue| async {
2323 std::future::pending::<()>().await;
2325 Ok(())
2326 });
2327
2328 let handler = Arc::new(RequestHandlerBuilder::new(BlockingExec).build().unwrap());
2329 let addr = Arc::new(WebSocketDispatcher::new(handler))
2330 .serve_with_addr("127.0.0.1:0")
2331 .await
2332 .expect("bind to port 0");
2333 let mut ws = ws_connect(addr).await;
2334
2335 for i in 0..65 {
2337 let req = serde_json::json!({
2338 "jsonrpc": "2.0",
2339 "method": "SendMessage",
2340 "id": format!("busy-{i}"),
2341 "params": {
2342 "message": {
2343 "messageId": format!("msg-busy-{i}"),
2344 "role": "ROLE_USER",
2345 "parts": [{"text": "block"}]
2346 }
2347 }
2348 })
2349 .to_string();
2350 ws.send(WsMessage::Text(req.into())).await.unwrap();
2351 }
2352
2353 let code = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2355 while let Some(Ok(msg)) = ws.next().await {
2356 let Ok(text) = msg.into_text() else { continue };
2357 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2358 continue;
2359 };
2360 if let Some(c) = v["error"]["code"].as_i64() {
2361 assert!(
2362 v["error"]["message"]
2363 .as_str()
2364 .is_some_and(|m| m.contains("server busy")),
2365 "expected the back-pressure rejection, got: {v}"
2366 );
2367 return Some(c);
2368 }
2369 }
2370 None
2371 })
2372 .await
2373 .expect("the over-limit request must be answered within 10s");
2374
2375 assert_eq!(
2376 code,
2377 Some(-32000),
2378 "back-pressure must be reported with the JSON-RPC server-error \
2379 code -32000, not a positive code"
2380 );
2381 }
2382
2383 #[tokio::test]
2386 async fn ws_handshake_timeout_disconnects_stalled_peer() {
2387 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2388 let dispatcher = Arc::new(
2389 WebSocketDispatcher::new(handler)
2390 .with_handshake_timeout(std::time::Duration::from_millis(200)),
2391 );
2392 let addr = dispatcher
2393 .serve_with_addr("127.0.0.1:0")
2394 .await
2395 .expect("bind");
2396
2397 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
2399 let mut buf = [0u8; 16];
2400 let read = tokio::time::timeout(
2403 std::time::Duration::from_secs(5),
2404 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
2405 )
2406 .await
2407 .expect("server should close the stalled connection");
2408 assert!(
2409 matches!(read, Ok(0) | Err(_)),
2410 "expected EOF/reset from server, got: {read:?}"
2411 );
2412 }
2413
2414 #[tokio::test]
2416 async fn ws_binary_frame_gets_error_response() {
2417 let addr = spawn_ws_server().await;
2418 let mut ws = ws_connect(addr).await;
2419
2420 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
2421 .await
2422 .unwrap();
2423
2424 let text = read_text(&mut ws).await;
2425 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2426 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
2427 assert!(
2428 v["error"]["message"]
2429 .as_str()
2430 .unwrap_or("")
2431 .contains("binary"),
2432 "error should explain binary frames are unsupported: {v}"
2433 );
2434 }
2435}