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::tungstenite::handshake::server::{
45 ErrorResponse, Request as WsUpgradeRequest, Response as WsUpgradeResponse,
46};
47use tokio_tungstenite::tungstenite::Message as WsMessage;
48use tokio_tungstenite::WebSocketStream;
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::agent_executor;
1081 use crate::RequestHandlerBuilder;
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::agent_executor;
1092 use crate::RequestHandlerBuilder;
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 async fn spawn_ws_server_with(
1323 configure: impl FnOnce(WebSocketDispatcher) -> WebSocketDispatcher,
1324 ) -> std::net::SocketAddr {
1325 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1326 let dispatcher = Arc::new(configure(WebSocketDispatcher::new(handler)));
1327 dispatcher
1328 .serve_with_addr("127.0.0.1:0")
1329 .await
1330 .expect("bind to port 0")
1331 }
1332
1333 async fn ws_connect(
1334 addr: std::net::SocketAddr,
1335 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
1336 {
1337 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1338 let mut req = format!("ws://{addr}").into_client_request().expect("url");
1339 req.headers_mut()
1340 .insert("a2a-version", "1.0".parse().expect("header"));
1341 let (ws, _) = tokio_tungstenite::connect_async(req)
1342 .await
1343 .expect("ws connect");
1344 ws
1345 }
1346
1347 async fn read_text(
1349 ws: &mut tokio_tungstenite::WebSocketStream<
1350 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1351 >,
1352 ) -> String {
1353 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1354 .await
1355 .expect("timeout waiting for WS frame")
1356 .expect("stream ended")
1357 .expect("ws error");
1358 msg.into_text()
1359 .expect("not a text frame")
1360 .as_str()
1361 .to_owned()
1362 }
1363
1364 fn send_message_json(id: &str) -> String {
1365 serde_json::json!({
1366 "jsonrpc": "2.0",
1367 "method": "SendMessage",
1368 "id": id,
1369 "params": {
1370 "message": {
1371 "messageId": "msg-1",
1372 "role": "ROLE_USER",
1373 "parts": [{"text": "hello"}]
1374 }
1375 }
1376 })
1377 .to_string()
1378 }
1379
1380 #[tokio::test]
1382 async fn ws_send_message_success() {
1383 let addr = spawn_ws_server().await;
1384 let mut ws = ws_connect(addr).await;
1385
1386 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
1387 .await
1388 .unwrap();
1389
1390 let text = read_text(&mut ws).await;
1391 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1392 assert_eq!(v["id"], "sm-1");
1393 assert!(v.get("result").is_some(), "expected result key: {text}");
1395 }
1396
1397 #[tokio::test]
1399 async fn ws_get_task_not_found() {
1400 let addr = spawn_ws_server().await;
1401 let mut ws = ws_connect(addr).await;
1402
1403 let req = serde_json::json!({
1404 "jsonrpc": "2.0",
1405 "method": "GetTask",
1406 "id": "gt-1",
1407 "params": {"id": "nonexistent"}
1408 })
1409 .to_string();
1410 ws.send(WsMessage::Text(req.into())).await.unwrap();
1411
1412 let text = read_text(&mut ws).await;
1413 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1414 assert!(v.get("error").is_some(), "expected error: {text}");
1415 }
1416
1417 #[tokio::test]
1419 async fn ws_list_tasks_success() {
1420 let addr = spawn_ws_server().await;
1421 let mut ws = ws_connect(addr).await;
1422
1423 let req = serde_json::json!({
1424 "jsonrpc": "2.0",
1425 "method": "ListTasks",
1426 "id": "lt-1",
1427 "params": {}
1428 })
1429 .to_string();
1430 ws.send(WsMessage::Text(req.into())).await.unwrap();
1431
1432 let text = read_text(&mut ws).await;
1433 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1434 assert_eq!(v["id"], "lt-1");
1435 assert!(v.get("result").is_some(), "expected result: {text}");
1436 }
1437
1438 #[tokio::test]
1440 async fn ws_cancel_task_not_found() {
1441 let addr = spawn_ws_server().await;
1442 let mut ws = ws_connect(addr).await;
1443
1444 let req = serde_json::json!({
1445 "jsonrpc": "2.0",
1446 "method": "CancelTask",
1447 "id": "ct-1",
1448 "params": {"id": "nonexistent"}
1449 })
1450 .to_string();
1451 ws.send(WsMessage::Text(req.into())).await.unwrap();
1452
1453 let text = read_text(&mut ws).await;
1454 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1455 assert!(v.get("error").is_some(), "expected error: {text}");
1456 }
1457
1458 #[tokio::test]
1460 async fn ws_subscribe_task_not_found() {
1461 let addr = spawn_ws_server().await;
1462 let mut ws = ws_connect(addr).await;
1463
1464 let req = serde_json::json!({
1465 "jsonrpc": "2.0",
1466 "method": "SubscribeToTask",
1467 "id": "sub-1",
1468 "params": {"id": "nonexistent"}
1469 })
1470 .to_string();
1471 ws.send(WsMessage::Text(req.into())).await.unwrap();
1472
1473 let text = read_text(&mut ws).await;
1474 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1475 assert!(v.get("error").is_some(), "expected error: {text}");
1476 }
1477
1478 #[tokio::test]
1480 async fn ws_unknown_method_error() {
1481 let addr = spawn_ws_server().await;
1482 let mut ws = ws_connect(addr).await;
1483
1484 let req = serde_json::json!({
1485 "jsonrpc": "2.0",
1486 "method": "FooBar",
1487 "id": "unk-1",
1488 "params": {}
1489 })
1490 .to_string();
1491 ws.send(WsMessage::Text(req.into())).await.unwrap();
1492
1493 let text = read_text(&mut ws).await;
1494 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1495 assert!(v.get("error").is_some(), "expected error: {text}");
1496 let msg = v["error"]["message"].as_str().unwrap_or("");
1497 assert!(
1498 msg.to_lowercase().contains("method")
1499 || msg.to_lowercase().contains("not found")
1500 || msg.to_lowercase().contains("unsupported"),
1501 "error message should mention method not found: {msg}"
1502 );
1503 }
1504
1505 #[tokio::test]
1507 async fn ws_invalid_json_parse_error() {
1508 let addr = spawn_ws_server().await;
1509 let mut ws = ws_connect(addr).await;
1510
1511 ws.send(WsMessage::Text("this is not json {{".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["error"]["code"], -32700, "expected parse error code");
1518 }
1519
1520 #[tokio::test]
1528 async fn ws_oversized_message_rejected() {
1529 let addr = spawn_ws_server().await;
1530 let mut ws = ws_connect(addr).await;
1531
1532 let big = "x".repeat(4 * 1024 * 1024 + 1);
1534 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1538 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1541 .await
1542 .expect("server should react to the oversized message");
1543 match outcome {
1544 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1545 Some(Ok(frame)) => panic!(
1546 "server must not answer an oversized message with a frame, got: {frame:?}"
1547 ),
1548 }
1549 }
1550 }
1551
1552 #[tokio::test]
1556 async fn ws_large_message_under_cap_still_processed() {
1557 let addr = spawn_ws_server().await;
1558 let mut ws = ws_connect(addr).await;
1559
1560 let big = "x".repeat(3 * 1024 * 1024);
1561 ws.send(WsMessage::Text(big.into())).await.unwrap();
1562
1563 let text = read_text(&mut ws).await;
1564 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1565 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1566 }
1567
1568 #[tokio::test]
1570 async fn ws_ping_pong_response() {
1571 let addr = spawn_ws_server().await;
1572 let mut ws = ws_connect(addr).await;
1573
1574 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1575
1576 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1577 loop {
1578 let msg = ws.next().await.unwrap().unwrap();
1579 if let WsMessage::Pong(data) = msg {
1580 return data;
1581 }
1582 }
1583 })
1584 .await
1585 .expect("should get pong within 3s");
1586 assert_eq!(pong, vec![42, 43]);
1587 }
1588
1589 #[tokio::test]
1591 async fn ws_get_task_invalid_params() {
1592 let addr = spawn_ws_server().await;
1593 let mut ws = ws_connect(addr).await;
1594
1595 let req = serde_json::json!({
1597 "jsonrpc": "2.0",
1598 "method": "GetTask",
1599 "id": "gti-1",
1600 "params": {"wrong_field": 123}
1601 })
1602 .to_string();
1603 ws.send(WsMessage::Text(req.into())).await.unwrap();
1604
1605 let text = read_text(&mut ws).await;
1606 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1607 assert!(
1608 v.get("error").is_some(),
1609 "expected error for bad params: {text}"
1610 );
1611 }
1612
1613 #[tokio::test]
1615 async fn ws_send_streaming_message_events() {
1616 let addr = spawn_ws_server().await;
1617 let mut ws = ws_connect(addr).await;
1618
1619 let req = serde_json::json!({
1620 "jsonrpc": "2.0",
1621 "method": "SendStreamingMessage",
1622 "id": "ssm-1",
1623 "params": {
1624 "message": {
1625 "messageId": "msg-stream-1",
1626 "role": "ROLE_USER",
1627 "parts": [{"text": "stream me"}]
1628 }
1629 }
1630 })
1631 .to_string();
1632 ws.send(WsMessage::Text(req.into())).await.unwrap();
1633
1634 let mut frames = Vec::new();
1636 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1637 loop {
1638 let msg = ws.next().await.unwrap().unwrap();
1639 let text = msg.into_text().unwrap();
1640 let done = text.contains("stream_complete");
1641 frames.push(text);
1642 if done {
1643 break;
1644 }
1645 }
1646 });
1647 timeout.await.expect("streaming should complete within 5s");
1648
1649 assert!(
1651 frames.len() >= 3,
1652 "expected >= 3 frames, got {}: {:?}",
1653 frames.len(),
1654 frames
1655 );
1656 assert!(frames.last().unwrap().contains("stream_complete"));
1658 }
1659
1660 #[tokio::test]
1662 async fn ws_send_message_invalid_params() {
1663 let addr = spawn_ws_server().await;
1664 let mut ws = ws_connect(addr).await;
1665
1666 let req = serde_json::json!({
1667 "jsonrpc": "2.0",
1668 "method": "SendMessage",
1669 "id": "smi-1",
1670 "params": {"not_message": true}
1671 })
1672 .to_string();
1673 ws.send(WsMessage::Text(req.into())).await.unwrap();
1674
1675 let text = read_text(&mut ws).await;
1676 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1677 assert!(
1678 v.get("error").is_some(),
1679 "expected error for bad send params: {text}"
1680 );
1681 }
1682
1683 #[tokio::test]
1685 async fn ws_subscribe_invalid_params() {
1686 let addr = spawn_ws_server().await;
1687 let mut ws = ws_connect(addr).await;
1688
1689 let req = serde_json::json!({
1690 "jsonrpc": "2.0",
1691 "method": "SubscribeToTask",
1692 "id": "subi-1",
1693 "params": {}
1694 })
1695 .to_string();
1696 ws.send(WsMessage::Text(req.into())).await.unwrap();
1697
1698 let text = read_text(&mut ws).await;
1699 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1700 assert!(
1701 v.get("error").is_some(),
1702 "expected error for bad subscribe params: {text}"
1703 );
1704 }
1705
1706 #[tokio::test]
1708 async fn ws_cancel_task_invalid_params() {
1709 let addr = spawn_ws_server().await;
1710 let mut ws = ws_connect(addr).await;
1711
1712 let req = serde_json::json!({
1713 "jsonrpc": "2.0",
1714 "method": "CancelTask",
1715 "id": "cti-1",
1716 "params": {"wrong": 1}
1717 })
1718 .to_string();
1719 ws.send(WsMessage::Text(req.into())).await.unwrap();
1720
1721 let text = read_text(&mut ws).await;
1722 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1723 assert!(v.get("error").is_some(), "expected error: {text}");
1724 }
1725
1726 #[tokio::test]
1728 async fn ws_list_tasks_with_filters() {
1729 let addr = spawn_ws_server().await;
1730 let mut ws = ws_connect(addr).await;
1731
1732 let req = serde_json::json!({
1733 "jsonrpc": "2.0",
1734 "method": "ListTasks",
1735 "id": "ltf-1",
1736 "params": {
1737 "contextId": "ctx-1",
1738 "pageSize": 10
1739 }
1740 })
1741 .to_string();
1742 ws.send(WsMessage::Text(req.into())).await.unwrap();
1743
1744 let text = read_text(&mut ws).await;
1745 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1746 assert_eq!(v["id"], "ltf-1");
1747 assert!(v.get("result").is_some(), "expected result: {text}");
1748 }
1749
1750 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
1753
1754 async fn ws_call(
1756 ws: &mut tokio_tungstenite::WebSocketStream<
1757 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1758 >,
1759 req: serde_json::Value,
1760 ) -> serde_json::Value {
1761 ws.send(WsMessage::Text(req.to_string().into()))
1762 .await
1763 .expect("send");
1764 let text = read_text(ws).await;
1765 serde_json::from_str(&text).expect("response should be JSON")
1766 }
1767
1768 #[tokio::test]
1773 async fn ws_legacy_method_names_rejected() {
1774 let addr = spawn_ws_server().await;
1775 let mut ws = ws_connect(addr).await;
1776
1777 for legacy in ["message/send", "tasks/list", "tasks/get"] {
1778 let v = ws_call(
1779 &mut ws,
1780 serde_json::json!({
1781 "jsonrpc": "2.0",
1782 "method": legacy,
1783 "id": format!("legacy-{legacy}"),
1784 "params": {}
1785 }),
1786 )
1787 .await;
1788 assert_eq!(
1789 v["error"]["code"].as_i64(),
1790 Some(-32601),
1791 "v0.3-style name {legacy} must be MethodNotFound: {v}"
1792 );
1793 }
1794 }
1795
1796 #[tokio::test]
1799 #[allow(clippy::too_many_lines)]
1800 async fn ws_push_config_methods_routed() {
1801 use crate::push::PushSender;
1802 use a2a_protocol_types::push::TaskPushNotificationConfig;
1803
1804 struct NoopSender;
1805 impl PushSender for NoopSender {
1806 fn send<'a>(
1807 &'a self,
1808 _url: &'a str,
1809 _event: &'a StreamResponse,
1810 _config: &'a TaskPushNotificationConfig,
1811 ) -> std::pin::Pin<
1812 Box<
1813 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1814 + Send
1815 + 'a,
1816 >,
1817 > {
1818 Box::pin(async { Ok(()) })
1819 }
1820 fn allows_private_urls(&self) -> bool {
1821 true
1822 }
1823 }
1824
1825 let handler = Arc::new(
1826 RequestHandlerBuilder::new(EchoExec)
1827 .with_push_sender(NoopSender)
1828 .build()
1829 .unwrap(),
1830 );
1831 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1832 let addr = dispatcher
1833 .serve_with_addr("127.0.0.1:0")
1834 .await
1835 .expect("bind");
1836 let mut ws = ws_connect(addr).await;
1837
1838 let v = ws_call(
1840 &mut ws,
1841 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
1842 )
1843 .await;
1844 let task_id = v["result"]["task"]["id"]
1845 .as_str()
1846 .expect("task id in send result")
1847 .to_owned();
1848
1849 let v = ws_call(
1851 &mut ws,
1852 serde_json::json!({
1853 "jsonrpc": "2.0",
1854 "method": "CreateTaskPushNotificationConfig",
1855 "id": "pc-1",
1856 "params": {
1857 "taskId": task_id,
1858 "url": "https://example.com/hook"
1859 }
1860 }),
1861 )
1862 .await;
1863 assert!(v.get("result").is_some(), "set push config failed: {v}");
1864 let config_id = v["result"]["id"]
1865 .as_str()
1866 .expect("server-assigned config id")
1867 .to_owned();
1868
1869 let v = ws_call(
1871 &mut ws,
1872 serde_json::json!({
1873 "jsonrpc": "2.0",
1874 "method": "GetTaskPushNotificationConfig",
1875 "id": "pc-2",
1876 "params": {"taskId": task_id, "id": config_id}
1877 }),
1878 )
1879 .await;
1880 assert!(v.get("result").is_some(), "get push config failed: {v}");
1881
1882 let v = ws_call(
1884 &mut ws,
1885 serde_json::json!({
1886 "jsonrpc": "2.0",
1887 "method": "ListTaskPushNotificationConfigs",
1888 "id": "pc-3",
1889 "params": {"taskId": task_id}
1890 }),
1891 )
1892 .await;
1893 assert!(v.get("result").is_some(), "list push configs failed: {v}");
1894 assert!(
1895 v["result"]["configs"].is_array(),
1896 "expected configs array: {v}"
1897 );
1898
1899 let v = ws_call(
1901 &mut ws,
1902 serde_json::json!({
1903 "jsonrpc": "2.0",
1904 "method": "DeleteTaskPushNotificationConfig",
1905 "id": "pc-4",
1906 "params": {"taskId": task_id, "id": config_id}
1907 }),
1908 )
1909 .await;
1910 assert!(v.get("result").is_some(), "delete push config failed: {v}");
1911 }
1912
1913 #[tokio::test]
1916 async fn ws_get_extended_agent_card_routed() {
1917 let addr = spawn_ws_server().await;
1918 let mut ws = ws_connect(addr).await;
1919
1920 let v = ws_call(
1921 &mut ws,
1922 serde_json::json!({
1923 "jsonrpc": "2.0",
1924 "method": "GetExtendedAgentCard",
1925 "id": "card-1",
1926 "params": {}
1927 }),
1928 )
1929 .await;
1930 let err = v.get("error").expect("expected an error response");
1933 assert_ne!(
1934 err["code"], -32601,
1935 "GetExtendedAgentCard must be routed, got: {v}"
1936 );
1937 }
1938
1939 #[tokio::test]
1943 async fn ws_upgrade_headers_drive_tenant_resolution() {
1944 use crate::tenant_resolver::HeaderTenantResolver;
1945
1946 let handler = Arc::new(
1947 RequestHandlerBuilder::new(EchoExec)
1948 .with_tenant_resolver(HeaderTenantResolver::default())
1949 .require_resolved_tenant()
1950 .build()
1951 .unwrap(),
1952 );
1953 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1954 let addr = dispatcher
1955 .serve_with_addr("127.0.0.1:0")
1956 .await
1957 .expect("bind");
1958
1959 let mut ws = ws_connect(addr).await;
1961 let v = ws_call(
1962 &mut ws,
1963 serde_json::json!({
1964 "jsonrpc": "2.0",
1965 "method": "ListTasks",
1966 "id": "t-1",
1967 "params": {}
1968 }),
1969 )
1970 .await;
1971 let err = v.get("error").expect("headerless request must be rejected");
1972 let msg = err["message"].as_str().unwrap_or("");
1973 assert!(
1974 msg.contains("tenant"),
1975 "expected strict-tenancy rejection, got: {v}"
1976 );
1977
1978 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1980 req.headers_mut()
1981 .insert("a2a-version", "1.0".parse().unwrap());
1982 req.headers_mut()
1983 .insert("x-tenant-id", "acme".parse().unwrap());
1984 let (mut ws, _) = tokio_tungstenite::connect_async(req)
1985 .await
1986 .expect("connect");
1987 let v = ws_call(
1988 &mut ws,
1989 serde_json::json!({
1990 "jsonrpc": "2.0",
1991 "method": "ListTasks",
1992 "id": "t-2",
1993 "params": {}
1994 }),
1995 )
1996 .await;
1997 assert!(
1998 v.get("result").is_some(),
1999 "tenant header on the upgrade request must reach the resolver: {v}"
2000 );
2001 }
2002
2003 #[tokio::test]
2005 async fn ws_version_mismatch_rejects_handshake() {
2006 let addr = spawn_ws_server().await;
2007
2008 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2009 req.headers_mut()
2010 .insert("a2a-version", "2.0".parse().unwrap());
2011 let outcome = tokio_tungstenite::connect_async(req).await;
2012 assert!(
2013 outcome.is_err(),
2014 "handshake with A2A-Version 2.0 must be rejected"
2015 );
2016
2017 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2019 req.headers_mut()
2020 .insert("a2a-version", "1.0".parse().unwrap());
2021 assert!(
2022 tokio_tungstenite::connect_async(req).await.is_ok(),
2023 "handshake with A2A-Version 1.0 must succeed"
2024 );
2025 }
2026
2027 #[tokio::test]
2038 async fn ws_missing_version_header_rejected_by_default() {
2039 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2040
2041 let addr = spawn_ws_server().await;
2042 let req = format!("ws://{addr}").into_client_request().unwrap();
2044 assert!(
2045 tokio_tungstenite::connect_async(req).await.is_err(),
2046 "a handshake with no A2A-Version header must be rejected by default \
2047 (§3.6.2 reads it as 0.3)"
2048 );
2049 }
2050
2051 #[tokio::test]
2052 async fn ws_missing_version_header_accepted_with_optout() {
2053 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2054
2055 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2056 let dispatcher =
2057 Arc::new(WebSocketDispatcher::new(handler).accept_missing_version_header());
2058 let addr = dispatcher
2059 .serve_with_addr("127.0.0.1:0")
2060 .await
2061 .expect("bind to port 0");
2062
2063 let req = format!("ws://{addr}").into_client_request().unwrap();
2064 assert!(
2065 tokio_tungstenite::connect_async(req).await.is_ok(),
2066 "accept_missing_version_header() must restore the tolerant behaviour"
2067 );
2068 }
2069
2070 #[tokio::test]
2079 async fn ws_version_rejection_body_carries_error_details() {
2080 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
2081 use tokio_tungstenite::tungstenite::Error as WsError;
2082
2083 let addr = spawn_ws_server().await;
2084 let mut req = format!("ws://{addr}").into_client_request().unwrap();
2085 req.headers_mut()
2086 .insert("a2a-version", "2.0".parse().unwrap());
2087
2088 match tokio_tungstenite::connect_async(req).await {
2089 Err(WsError::Http(resp)) => {
2090 assert_eq!(resp.status(), 400, "version rejection is HTTP 400");
2091 let body = resp.body().as_ref().expect("rejection carries a body");
2092 let text = String::from_utf8_lossy(body);
2093 let json: serde_json::Value =
2094 serde_json::from_str(&text).expect("rejection body is JSON");
2095 assert!(
2096 !json["error"]["details"].is_null(),
2097 "the AIP-193 details block must be present, got: {text}"
2098 );
2099 }
2100 other => panic!("expected an HTTP 400 rejection, got: {other:?}"),
2101 }
2102 }
2103
2104 #[tokio::test]
2118 async fn ws_lagged_stream_reports_server_error_code() {
2119 struct FloodExec;
2120 agent_executor!(FloodExec, |ctx, queue| async {
2121 for _ in 0..512 {
2122 queue
2123 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
2124 task_id: ctx.task_id.clone(),
2125 context_id: ContextId::new(ctx.context_id.clone()),
2126 status: TaskStatus::new(TaskState::Working),
2127 metadata: None,
2128 }))
2129 .await?;
2130 }
2131 Ok(())
2132 });
2133
2134 let handler = Arc::new(
2135 RequestHandlerBuilder::new(FloodExec)
2136 .with_event_queue_capacity(1)
2137 .build()
2138 .unwrap(),
2139 );
2140 let addr = Arc::new(WebSocketDispatcher::new(handler))
2141 .serve_with_addr("127.0.0.1:0")
2142 .await
2143 .expect("bind to port 0");
2144 let mut ws = ws_connect(addr).await;
2145
2146 let req = serde_json::json!({
2147 "jsonrpc": "2.0",
2148 "method": "SendStreamingMessage",
2149 "id": "lag-1",
2150 "params": {
2151 "message": {
2152 "messageId": "msg-lag-1",
2153 "role": "ROLE_USER",
2154 "parts": [{"text": "flood"}]
2155 }
2156 }
2157 })
2158 .to_string();
2159 ws.send(WsMessage::Text(req.into())).await.unwrap();
2160
2161 let found = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2162 while let Some(Ok(msg)) = ws.next().await {
2163 let Ok(text) = msg.into_text() else { continue };
2164 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2165 continue;
2166 };
2167 if let Some(code) = v["error"]["code"].as_i64() {
2168 return Some(code);
2169 }
2170 }
2171 None
2172 })
2173 .await
2174 .expect("the lagged stream must produce an error frame within 10s");
2175
2176 assert_eq!(
2177 found,
2178 Some(-32000),
2179 "a lagged stream must be reported with the JSON-RPC server-error \
2180 code -32000, not a positive code"
2181 );
2182 }
2183
2184 #[tokio::test]
2195 async fn ws_over_concurrency_limit_is_rejected_with_server_error_code() {
2196 struct BlockingExec;
2197 agent_executor!(BlockingExec, |_ctx, _queue| async {
2198 std::future::pending::<()>().await;
2200 Ok(())
2201 });
2202
2203 let handler = Arc::new(RequestHandlerBuilder::new(BlockingExec).build().unwrap());
2204 let addr = Arc::new(WebSocketDispatcher::new(handler))
2205 .serve_with_addr("127.0.0.1:0")
2206 .await
2207 .expect("bind to port 0");
2208 let mut ws = ws_connect(addr).await;
2209
2210 for i in 0..65 {
2212 let req = serde_json::json!({
2213 "jsonrpc": "2.0",
2214 "method": "SendMessage",
2215 "id": format!("busy-{i}"),
2216 "params": {
2217 "message": {
2218 "messageId": format!("msg-busy-{i}"),
2219 "role": "ROLE_USER",
2220 "parts": [{"text": "block"}]
2221 }
2222 }
2223 })
2224 .to_string();
2225 ws.send(WsMessage::Text(req.into())).await.unwrap();
2226 }
2227
2228 let code = tokio::time::timeout(std::time::Duration::from_secs(10), async {
2230 while let Some(Ok(msg)) = ws.next().await {
2231 let Ok(text) = msg.into_text() else { continue };
2232 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
2233 continue;
2234 };
2235 if let Some(c) = v["error"]["code"].as_i64() {
2236 assert!(
2237 v["error"]["message"]
2238 .as_str()
2239 .is_some_and(|m| m.contains("server busy")),
2240 "expected the back-pressure rejection, got: {v}"
2241 );
2242 return Some(c);
2243 }
2244 }
2245 None
2246 })
2247 .await
2248 .expect("the over-limit request must be answered within 10s");
2249
2250 assert_eq!(
2251 code,
2252 Some(-32000),
2253 "back-pressure must be reported with the JSON-RPC server-error \
2254 code -32000, not a positive code"
2255 );
2256 }
2257
2258 #[tokio::test]
2261 async fn ws_handshake_timeout_disconnects_stalled_peer() {
2262 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
2263 let dispatcher = Arc::new(
2264 WebSocketDispatcher::new(handler)
2265 .with_handshake_timeout(std::time::Duration::from_millis(200)),
2266 );
2267 let addr = dispatcher
2268 .serve_with_addr("127.0.0.1:0")
2269 .await
2270 .expect("bind");
2271
2272 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
2274 let mut buf = [0u8; 16];
2275 let read = tokio::time::timeout(
2278 std::time::Duration::from_secs(5),
2279 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
2280 )
2281 .await
2282 .expect("server should close the stalled connection");
2283 assert!(
2284 matches!(read, Ok(0) | Err(_)),
2285 "expected EOF/reset from server, got: {read:?}"
2286 );
2287 }
2288
2289 #[tokio::test]
2291 async fn ws_binary_frame_gets_error_response() {
2292 let addr = spawn_ws_server().await;
2293 let mut ws = ws_connect(addr).await;
2294
2295 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
2296 .await
2297 .unwrap();
2298
2299 let text = read_text(&mut ws).await;
2300 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
2301 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
2302 assert!(
2303 v["error"]["message"]
2304 .as_str()
2305 .unwrap_or("")
2306 .contains("binary"),
2307 "error should explain binary frames are unsupported: {v}"
2308 );
2309 }
2310}