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 struct WebSocketDispatcher {
97 handler: Arc<RequestHandler>,
98 handshake_timeout: Duration,
99 require_version_header: bool,
100}
101
102impl WebSocketDispatcher {
103 #[must_use]
105 pub const fn new(handler: Arc<RequestHandler>) -> Self {
106 Self {
107 handler,
108 handshake_timeout: DEFAULT_HANDSHAKE_TIMEOUT,
109 require_version_header: true,
110 }
111 }
112
113 #[must_use]
120 pub const fn accept_missing_version_header(mut self) -> Self {
121 self.require_version_header = false;
122 self
123 }
124
125 #[must_use]
130 pub const fn with_handshake_timeout(mut self, timeout: Duration) -> Self {
131 self.handshake_timeout = timeout;
132 self
133 }
134
135 pub async fn serve(
145 self: Arc<Self>,
146 addr: impl tokio::net::ToSocketAddrs,
147 ) -> std::io::Result<()> {
148 let listener = TcpListener::bind(addr).await?;
149
150 trace_info!(
151 addr = %listener.local_addr().unwrap_or_else(|_| SocketAddr::from(([0, 0, 0, 0], 0))),
152 "A2A WebSocket server listening"
153 );
154
155 self.accept_loop(listener).await;
156 Ok(())
157 }
158
159 pub async fn serve_with_addr(
167 self: Arc<Self>,
168 addr: impl tokio::net::ToSocketAddrs,
169 ) -> std::io::Result<SocketAddr> {
170 let listener = TcpListener::bind(addr).await?;
171 let local_addr = listener.local_addr()?;
172
173 trace_info!(%local_addr, "A2A WebSocket server listening");
174
175 tokio::spawn(async move {
176 self.accept_loop(listener).await;
177 });
178
179 Ok(local_addr)
180 }
181
182 async fn accept_loop(self: Arc<Self>, listener: TcpListener) {
184 loop {
185 let (stream, _peer) = match listener.accept().await {
186 Ok(pair) => pair,
187 Err(e) => {
188 trace_warn!(error = %e, "accept() failed; retrying");
192 let backoff = crate::serve::accept_retry_backoff(&e);
193 tokio::time::sleep(backoff).await;
197 continue;
198 }
199 };
200 let dispatcher = Arc::clone(&self);
201 tokio::spawn(async move {
202 trace_debug!("WebSocket connection accepted");
203 if let Err(_e) = dispatcher.handle_connection(stream).await {
204 trace_warn!(error = %_e, "WebSocket connection error");
205 }
206 });
207 }
208 }
209
210 #[allow(clippy::result_large_err)]
214 async fn handle_connection(&self, stream: TcpStream) -> Result<(), WsError> {
215 let _ = stream.set_nodelay(true);
218
219 let ws_config = tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
222 .max_message_size(Some(MAX_WS_MESSAGE_SIZE))
223 .max_frame_size(Some(MAX_WS_MESSAGE_SIZE));
224
225 let mut upgrade_headers: Option<HashMap<String, String>> = None;
229 let require_version = self.require_version_header;
230 let callback = |req: &WsUpgradeRequest, resp: WsUpgradeResponse| {
231 check_a2a_version(req, require_version)?;
232 upgrade_headers = Some(extract_upgrade_headers(req));
233 Ok(resp)
234 };
235
236 let ws_stream = tokio::time::timeout(
239 self.handshake_timeout,
240 tokio_tungstenite::accept_hdr_async_with_config(stream, callback, Some(ws_config)),
241 )
242 .await
243 .map_err(|_| WsError::HandshakeTimeout)?
244 .map_err(WsError::Handshake)?;
245
246 let headers = Arc::new(upgrade_headers.unwrap_or_default());
247
248 let (writer, reader) = ws_stream.split();
249 let writer = Arc::new(tokio::sync::Mutex::new(writer));
250
251 self.read_loop(reader, &writer, &headers).await;
252
253 let mut w = writer.lock().await;
256 let _ = w.close().await;
257 drop(w);
258
259 Ok(())
260 }
261
262 async fn read_loop(
264 &self,
265 mut reader: futures_util::stream::SplitStream<WebSocketStream<TcpStream>>,
266 writer: &WsSink,
267 headers: &Arc<HashMap<String, String>>,
268 ) {
269 let semaphore = Arc::new(tokio::sync::Semaphore::new(64));
271
272 while let Some(msg) = reader.next().await {
273 match msg {
274 Ok(WsMessage::Text(text)) => {
275 if text.len() > MAX_WS_MESSAGE_SIZE {
278 let err_resp = JsonRpcErrorResponse::new(
279 best_effort_request_id(&text),
280 JsonRpcError::new(-32000, "message too large".to_string()),
281 );
282 send_json(writer, &err_resp).await;
283 continue;
284 }
285
286 let Ok(permit) = semaphore.clone().try_acquire_owned() else {
288 let err_resp = JsonRpcErrorResponse::new(
293 best_effort_request_id(&text),
294 JsonRpcError::new(
295 -32000,
296 "server busy: too many concurrent requests".to_string(),
297 ),
298 );
299 send_json(writer, &err_resp).await;
300 continue;
301 };
302
303 let writer = Arc::clone(writer);
304 let handler = Arc::clone(&self.handler);
305 let headers = Arc::clone(headers);
306 tokio::spawn(async move {
307 process_ws_message(&handler, &text, writer, &headers).await;
308 drop(permit); });
310 }
311 Ok(WsMessage::Binary(_)) => {
312 let err_resp = JsonRpcErrorResponse::new(
316 None,
317 JsonRpcError::new(
318 -32700,
319 "binary frames are not supported; send JSON-RPC as text frames"
320 .to_string(),
321 ),
322 );
323 send_json(writer, &err_resp).await;
324 }
325 Ok(WsMessage::Close(_)) | Err(_) => break,
326 Ok(_) => {}
331 }
332 }
333 }
334}
335
336fn extract_upgrade_headers(req: &WsUpgradeRequest) -> HashMap<String, String> {
341 let mut map: HashMap<String, String> = req
342 .headers()
343 .iter()
344 .filter_map(|(k, v)| {
345 v.to_str()
346 .ok()
347 .map(|val| (k.as_str().to_lowercase(), val.to_owned()))
348 })
349 .collect();
350 map.insert(":path".to_owned(), req.uri().path().to_owned());
354 map
355}
356
357#[allow(clippy::result_large_err)]
365fn check_a2a_version(req: &WsUpgradeRequest, require: bool) -> Result<(), ErrorResponse> {
366 let value = req
367 .headers()
368 .get(a2a_protocol_types::A2A_VERSION_HEADER)
369 .and_then(|v| v.to_str().ok());
370 let v = value.unwrap_or("").trim();
371 if v.is_empty() {
372 if !require {
373 return Ok(());
374 }
375 } else {
377 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
378 if major == Some(1) {
379 return Ok(());
380 }
381 }
382 let a2a_err = a2a_protocol_types::error::A2aError::version_not_supported(if v.is_empty() {
386 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)"
387 .to_owned()
388 } else {
389 format!("unsupported A2A version: {v}; this server supports 1.x")
390 });
391 let mut error_obj = serde_json::json!({
392 "error": {
393 "code": a2a_err.code.http_status(),
394 "status": a2a_err.code.grpc_status(),
395 "message": a2a_err.message,
396 }
397 });
398 let details = a2a_err.error_info_data(None);
399 if !details.is_null() {
400 error_obj["error"]["details"] = details;
401 }
402 let body = error_obj.to_string();
403 let resp = tokio_tungstenite::tungstenite::http::Response::builder()
404 .status(400)
405 .header("content-type", "application/json")
406 .body(Some(body))
407 .unwrap_or_else(|_| {
408 let mut r = ErrorResponse::new(Some(String::new()));
409 *r.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::BAD_REQUEST;
410 r
411 });
412 Err(resp)
413}
414
415fn best_effort_request_id(text: &str) -> JsonRpcId {
418 let v: serde_json::Value = serde_json::from_str(text).ok()?;
419 match v.get("id") {
420 Some(serde_json::Value::Null) | None => None,
421 Some(id) => Some(id.clone()),
422 }
423}
424
425#[derive(Debug)]
427enum WsError {
428 Handshake(tokio_tungstenite::tungstenite::Error),
429 HandshakeTimeout,
430}
431
432impl std::fmt::Display for WsError {
433 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
434 match self {
435 Self::Handshake(e) => write!(f, "WebSocket handshake failed: {e}"),
436 Self::HandshakeTimeout => write!(f, "WebSocket handshake timed out"),
437 }
438 }
439}
440
441type WsSink = Arc<tokio::sync::Mutex<SplitSink<WebSocketStream<TcpStream>, WsMessage>>>;
442
443#[allow(clippy::too_many_lines)]
449async fn process_ws_message(
450 handler: &RequestHandler,
451 text: &str,
452 writer: WsSink,
453 headers: &HashMap<String, String>,
454) {
455 let rpc_req: JsonRpcRequest = match serde_json::from_str(text) {
456 Ok(req) => req,
457 Err(e) => {
458 let err_resp = JsonRpcErrorResponse::new(
459 None,
460 JsonRpcError::new(-32700, format!("parse error: {e}")),
461 );
462 send_json(&writer, &err_resp).await;
463 return;
464 }
465 };
466
467 let id = rpc_req.id.to_response_id();
468
469 match rpc_req.method.as_str() {
470 "SendMessage" => {
471 dispatch_send_message(handler, &rpc_req, false, headers, id, &writer).await;
472 }
473 "SendStreamingMessage" | "message/stream" => {
474 dispatch_send_message(handler, &rpc_req, true, headers, id, &writer).await;
475 }
476 "GetTask" => {
477 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
478 Box::pin(async move {
479 let params: a2a_protocol_types::params::TaskQueryParams =
480 serde_json::from_value(p).map_err(|e| {
481 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
482 })?;
483 h.on_get_task(params, Some(hdr))
484 .await
485 .map(|r| serde_json::to_value(&r).unwrap_or_default())
486 .map_err(|e| e.to_a2a_error())
487 })
488 })
489 .await;
490 }
491 "ListTasks" => {
492 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
493 Box::pin(async move {
494 let params: a2a_protocol_types::params::ListTasksParams =
495 serde_json::from_value(p).map_err(|e| {
496 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
497 })?;
498 h.on_list_tasks(params, Some(hdr))
499 .await
500 .map(|r| serde_json::to_value(&r).unwrap_or_default())
501 .map_err(|e| e.to_a2a_error())
502 })
503 })
504 .await;
505 }
506 "CancelTask" => {
507 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
508 Box::pin(async move {
509 let params: a2a_protocol_types::params::CancelTaskParams =
510 serde_json::from_value(p).map_err(|e| {
511 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
512 })?;
513 h.on_cancel_task(params, Some(hdr))
514 .await
515 .map(|r| serde_json::to_value(&r).unwrap_or_default())
516 .map_err(|e| e.to_a2a_error())
517 })
518 })
519 .await;
520 }
521 "SubscribeToTask" => {
522 let params = match parse_params::<a2a_protocol_types::params::TaskIdParams>(
523 rpc_req.params.as_ref(),
524 ) {
525 Ok(p) => p,
526 Err(e) => {
527 send_error(&writer, id, &e).await;
528 return;
529 }
530 };
531 match handler.on_resubscribe(params, Some(headers)).await {
532 Ok(reader) => {
533 stream_events(&writer, reader, id).await;
534 }
535 Err(e) => {
536 send_error(&writer, id, &e).await;
537 }
538 }
539 }
540 "CreateTaskPushNotificationConfig" => {
541 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
542 Box::pin(async move {
543 let params: a2a_protocol_types::push::TaskPushNotificationConfig =
544 serde_json::from_value(p).map_err(|e| {
545 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
546 })?;
547 h.on_set_push_config(params, Some(hdr))
548 .await
549 .map(|r| serde_json::to_value(&r).unwrap_or_default())
550 .map_err(|e| e.to_a2a_error())
551 })
552 })
553 .await;
554 }
555 "GetTaskPushNotificationConfig" => {
556 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
557 Box::pin(async move {
558 let params: a2a_protocol_types::params::GetPushConfigParams =
559 serde_json::from_value(p).map_err(|e| {
560 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
561 })?;
562 h.on_get_push_config(params, Some(hdr))
563 .await
564 .map(|r| serde_json::to_value(&r).unwrap_or_default())
565 .map_err(|e| e.to_a2a_error())
566 })
567 })
568 .await;
569 }
570 "ListTaskPushNotificationConfigs" => {
571 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
572 Box::pin(async move {
573 let params: a2a_protocol_types::params::ListPushConfigsParams =
574 serde_json::from_value(p).map_err(|e| {
575 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
576 })?;
577 h.on_list_push_configs(¶ms.task_id, params.tenant.as_deref(), Some(hdr))
578 .await
579 .map(|configs| {
580 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
581 configs,
582 next_page_token: None,
583 };
584 serde_json::to_value(&resp).unwrap_or_default()
585 })
586 .map_err(|e| e.to_a2a_error())
587 })
588 })
589 .await;
590 }
591 "DeleteTaskPushNotificationConfig" => {
592 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
593 Box::pin(async move {
594 let params: a2a_protocol_types::params::DeletePushConfigParams =
595 serde_json::from_value(p).map_err(|e| {
596 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
597 })?;
598 h.on_delete_push_config(params, Some(hdr))
599 .await
600 .map(|()| serde_json::json!({}))
601 .map_err(|e| e.to_a2a_error())
602 })
603 })
604 .await;
605 }
606 "GetExtendedAgentCard" => {
607 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, _p, hdr| {
608 Box::pin(async move {
609 h.on_get_extended_agent_card(Some(hdr))
610 .await
611 .map(|r| serde_json::to_value(&r).unwrap_or_default())
612 .map_err(|e| e.to_a2a_error())
613 })
614 })
615 .await;
616 }
617 other => {
618 let err = ServerError::MethodNotFound(other.to_owned());
619 send_error(&writer, id, &err).await;
620 }
621 }
622}
623
624async fn dispatch_send_message(
626 handler: &RequestHandler,
627 rpc_req: &JsonRpcRequest,
628 streaming: bool,
629 headers: &HashMap<String, String>,
630 id: JsonRpcId,
631 writer: &WsSink,
632) {
633 let params = match parse_params::<a2a_protocol_types::params::MessageSendParams>(
634 rpc_req.params.as_ref(),
635 ) {
636 Ok(p) => p,
637 Err(e) => {
638 send_error(writer, id, &e).await;
639 return;
640 }
641 };
642
643 match handler
644 .on_send_message(params, streaming, Some(headers))
645 .await
646 {
647 Ok(SendMessageResult::Response(resp)) => {
648 let result = serde_json::to_value(&resp).unwrap_or(serde_json::Value::Null);
649 let success = JsonRpcSuccessResponse {
650 jsonrpc: JsonRpcVersion,
651 id,
652 result,
653 };
654 send_json(writer, &success).await;
655 }
656 Ok(SendMessageResult::Stream(reader)) => {
657 stream_events(writer, reader, id).await;
658 }
659 Err(e) => {
660 send_error(writer, id, &e).await;
661 }
662 }
663}
664
665async fn stream_events(
667 writer: &WsSink,
668 mut reader: crate::streaming::InMemoryQueueReader,
669 id: JsonRpcId,
670) {
671 while let Some(event) = reader.read().await {
672 match event {
673 Ok(stream_resp) => {
674 let envelope = JsonRpcSuccessResponse {
677 jsonrpc: JsonRpcVersion,
678 id: id.clone(),
679 result: stream_resp,
680 };
681 let json = serde_json::to_string(&envelope).unwrap_or_default();
682 let mut w = writer.lock().await;
683 if w.send(WsMessage::Text(json.into())).await.is_err() {
684 return; }
686 drop(w);
687 }
688 Err(e) => {
689 let err_resp =
690 JsonRpcErrorResponse::new(id.clone(), JsonRpcError::new(-32000, e.to_string()));
691 send_json(writer, &err_resp).await;
692 return;
693 }
694 }
695 }
696
697 let success = JsonRpcSuccessResponse {
699 jsonrpc: JsonRpcVersion,
700 id,
701 result: serde_json::json!({"status": "stream_complete"}),
702 };
703 send_json(writer, &success).await;
704}
705
706async fn dispatch_simple<'a, F>(
708 handler: &'a RequestHandler,
709 rpc_req: &JsonRpcRequest,
710 id: JsonRpcId,
711 headers: &'a HashMap<String, String>,
712 writer: &WsSink,
713 f: F,
714) where
715 F: FnOnce(
716 &'a RequestHandler,
717 serde_json::Value,
718 &'a HashMap<String, String>,
719 ) -> std::pin::Pin<
720 Box<
721 dyn std::future::Future<
722 Output = Result<serde_json::Value, a2a_protocol_types::error::A2aError>,
723 > + Send
724 + 'a,
725 >,
726 >,
727{
728 let params = rpc_req.params.clone().unwrap_or(serde_json::Value::Null);
729 match f(handler, params, headers).await {
730 Ok(result) => {
731 let success = JsonRpcSuccessResponse {
732 jsonrpc: JsonRpcVersion,
733 id,
734 result,
735 };
736 send_json(writer, &success).await;
737 }
738 Err(e) => {
739 let err_resp =
740 JsonRpcErrorResponse::new(id, JsonRpcError::new(e.code.as_i32(), e.message));
741 send_json(writer, &err_resp).await;
742 }
743 }
744}
745
746async fn send_json<T: serde::Serialize + Sync>(writer: &WsSink, value: &T) {
748 let json = serde_json::to_string(value).unwrap_or_default();
749 let mut w = writer.lock().await;
750 let _ = w.send(WsMessage::Text(json.into())).await;
751 drop(w);
752}
753
754async fn send_error(writer: &WsSink, id: JsonRpcId, err: &ServerError) {
756 let a2a_err = err.to_a2a_error();
757 let resp = JsonRpcErrorResponse::new(
758 id,
759 JsonRpcError::new(a2a_err.code.as_i32(), a2a_err.message),
760 );
761 send_json(writer, &resp).await;
762}
763
764fn parse_params<T: serde::de::DeserializeOwned>(
766 params: Option<&serde_json::Value>,
767) -> Result<T, ServerError> {
768 let value = params.cloned().unwrap_or(serde_json::Value::Null);
769 serde_json::from_value(value)
770 .map_err(|e| ServerError::InvalidParams(format!("invalid params: {e}")))
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776
777 #[test]
778 fn parse_params_with_valid_json() {
779 let value = Some(serde_json::json!({"id": "task-1"}));
780 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
781 parse_params(value.as_ref());
782 assert!(result.is_ok());
783 assert_eq!(result.unwrap().id, "task-1");
784 }
785
786 #[test]
787 fn parse_params_with_none_returns_error() {
788 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> = parse_params(None);
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn parse_params_with_wrong_type_returns_error() {
794 let value = Some(serde_json::json!("not an object"));
795 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
796 parse_params(value.as_ref());
797 assert!(result.is_err());
798 }
799
800 #[test]
802 fn ws_error_display_contains_message() {
803 let err = WsError::Handshake(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
804 let s = err.to_string();
805 assert!(s.contains("WebSocket handshake failed"));
806 }
807
808 #[test]
809 fn ws_error_display_handshake_timeout() {
810 let s = WsError::HandshakeTimeout.to_string();
811 assert!(s.contains("timed out"), "got: {s}");
812 }
813
814 #[test]
817 fn best_effort_request_id_extracts_string_and_number() {
818 assert_eq!(
819 best_effort_request_id(r#"{"jsonrpc":"2.0","id":"req-1","method":"GetTask"}"#),
820 Some(serde_json::json!("req-1"))
821 );
822 assert_eq!(
823 best_effort_request_id(r#"{"jsonrpc":"2.0","id":7,"method":"GetTask"}"#),
824 Some(serde_json::json!(7))
825 );
826 }
827
828 #[test]
829 fn best_effort_request_id_none_for_missing_null_or_invalid() {
830 assert_eq!(best_effort_request_id(r#"{"jsonrpc":"2.0"}"#), None);
831 assert_eq!(best_effort_request_id(r#"{"id":null}"#), None);
832 assert_eq!(best_effort_request_id("not json {{"), None);
833 }
834
835 #[test]
837 fn websocket_dispatcher_new() {
838 use crate::agent_executor;
839 use crate::RequestHandlerBuilder;
840 use std::sync::Arc;
841 struct DummyExec;
842 agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
843 let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
844 let _dispatcher = WebSocketDispatcher::new(handler);
845 }
846
847 use crate::agent_executor;
850 use crate::RequestHandlerBuilder;
851 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
852 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
853 use futures_util::{SinkExt, StreamExt};
854
855 struct EchoExec;
856 agent_executor!(EchoExec, |ctx, queue| async {
857 queue
858 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
859 task_id: ctx.task_id.clone(),
860 context_id: ContextId::new(ctx.context_id.clone()),
861 status: TaskStatus::new(TaskState::Working),
862 metadata: None,
863 }))
864 .await?;
865 queue
866 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
867 task_id: ctx.task_id.clone(),
868 context_id: ContextId::new(ctx.context_id.clone()),
869 status: TaskStatus::new(TaskState::Completed),
870 metadata: None,
871 }))
872 .await?;
873 Ok(())
874 });
875
876 async fn spawn_ws_server() -> std::net::SocketAddr {
877 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
878 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
879 dispatcher
880 .serve_with_addr("127.0.0.1:0")
881 .await
882 .expect("bind to port 0")
883 }
884
885 async fn ws_connect(
886 addr: std::net::SocketAddr,
887 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
888 {
889 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
890 let mut req = format!("ws://{addr}").into_client_request().expect("url");
891 req.headers_mut()
892 .insert("a2a-version", "1.0".parse().expect("header"));
893 let (ws, _) = tokio_tungstenite::connect_async(req)
894 .await
895 .expect("ws connect");
896 ws
897 }
898
899 async fn read_text(
901 ws: &mut tokio_tungstenite::WebSocketStream<
902 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
903 >,
904 ) -> String {
905 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
906 .await
907 .expect("timeout waiting for WS frame")
908 .expect("stream ended")
909 .expect("ws error");
910 msg.into_text()
911 .expect("not a text frame")
912 .as_str()
913 .to_owned()
914 }
915
916 fn send_message_json(id: &str) -> String {
917 serde_json::json!({
918 "jsonrpc": "2.0",
919 "method": "SendMessage",
920 "id": id,
921 "params": {
922 "message": {
923 "messageId": "msg-1",
924 "role": "ROLE_USER",
925 "parts": [{"text": "hello"}]
926 }
927 }
928 })
929 .to_string()
930 }
931
932 #[tokio::test]
934 async fn ws_send_message_success() {
935 let addr = spawn_ws_server().await;
936 let mut ws = ws_connect(addr).await;
937
938 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
939 .await
940 .unwrap();
941
942 let text = read_text(&mut ws).await;
943 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
944 assert_eq!(v["id"], "sm-1");
945 assert!(v.get("result").is_some(), "expected result key: {text}");
947 }
948
949 #[tokio::test]
951 async fn ws_get_task_not_found() {
952 let addr = spawn_ws_server().await;
953 let mut ws = ws_connect(addr).await;
954
955 let req = serde_json::json!({
956 "jsonrpc": "2.0",
957 "method": "GetTask",
958 "id": "gt-1",
959 "params": {"id": "nonexistent"}
960 })
961 .to_string();
962 ws.send(WsMessage::Text(req.into())).await.unwrap();
963
964 let text = read_text(&mut ws).await;
965 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
966 assert!(v.get("error").is_some(), "expected error: {text}");
967 }
968
969 #[tokio::test]
971 async fn ws_list_tasks_success() {
972 let addr = spawn_ws_server().await;
973 let mut ws = ws_connect(addr).await;
974
975 let req = serde_json::json!({
976 "jsonrpc": "2.0",
977 "method": "ListTasks",
978 "id": "lt-1",
979 "params": {}
980 })
981 .to_string();
982 ws.send(WsMessage::Text(req.into())).await.unwrap();
983
984 let text = read_text(&mut ws).await;
985 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
986 assert_eq!(v["id"], "lt-1");
987 assert!(v.get("result").is_some(), "expected result: {text}");
988 }
989
990 #[tokio::test]
992 async fn ws_cancel_task_not_found() {
993 let addr = spawn_ws_server().await;
994 let mut ws = ws_connect(addr).await;
995
996 let req = serde_json::json!({
997 "jsonrpc": "2.0",
998 "method": "CancelTask",
999 "id": "ct-1",
1000 "params": {"id": "nonexistent"}
1001 })
1002 .to_string();
1003 ws.send(WsMessage::Text(req.into())).await.unwrap();
1004
1005 let text = read_text(&mut ws).await;
1006 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1007 assert!(v.get("error").is_some(), "expected error: {text}");
1008 }
1009
1010 #[tokio::test]
1012 async fn ws_subscribe_task_not_found() {
1013 let addr = spawn_ws_server().await;
1014 let mut ws = ws_connect(addr).await;
1015
1016 let req = serde_json::json!({
1017 "jsonrpc": "2.0",
1018 "method": "SubscribeToTask",
1019 "id": "sub-1",
1020 "params": {"id": "nonexistent"}
1021 })
1022 .to_string();
1023 ws.send(WsMessage::Text(req.into())).await.unwrap();
1024
1025 let text = read_text(&mut ws).await;
1026 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1027 assert!(v.get("error").is_some(), "expected error: {text}");
1028 }
1029
1030 #[tokio::test]
1032 async fn ws_unknown_method_error() {
1033 let addr = spawn_ws_server().await;
1034 let mut ws = ws_connect(addr).await;
1035
1036 let req = serde_json::json!({
1037 "jsonrpc": "2.0",
1038 "method": "FooBar",
1039 "id": "unk-1",
1040 "params": {}
1041 })
1042 .to_string();
1043 ws.send(WsMessage::Text(req.into())).await.unwrap();
1044
1045 let text = read_text(&mut ws).await;
1046 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1047 assert!(v.get("error").is_some(), "expected error: {text}");
1048 let msg = v["error"]["message"].as_str().unwrap_or("");
1049 assert!(
1050 msg.to_lowercase().contains("method")
1051 || msg.to_lowercase().contains("not found")
1052 || msg.to_lowercase().contains("unsupported"),
1053 "error message should mention method not found: {msg}"
1054 );
1055 }
1056
1057 #[tokio::test]
1059 async fn ws_invalid_json_parse_error() {
1060 let addr = spawn_ws_server().await;
1061 let mut ws = ws_connect(addr).await;
1062
1063 ws.send(WsMessage::Text("this is not json {{".into()))
1064 .await
1065 .unwrap();
1066
1067 let text = read_text(&mut ws).await;
1068 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1069 assert_eq!(v["error"]["code"], -32700, "expected parse error code");
1070 }
1071
1072 #[tokio::test]
1080 async fn ws_oversized_message_rejected() {
1081 let addr = spawn_ws_server().await;
1082 let mut ws = ws_connect(addr).await;
1083
1084 let big = "x".repeat(4 * 1024 * 1024 + 1);
1086 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1090 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1093 .await
1094 .expect("server should react to the oversized message");
1095 match outcome {
1096 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1097 Some(Ok(frame)) => panic!(
1098 "server must not answer an oversized message with a frame, got: {frame:?}"
1099 ),
1100 }
1101 }
1102 }
1103
1104 #[tokio::test]
1108 async fn ws_large_message_under_cap_still_processed() {
1109 let addr = spawn_ws_server().await;
1110 let mut ws = ws_connect(addr).await;
1111
1112 let big = "x".repeat(3 * 1024 * 1024);
1113 ws.send(WsMessage::Text(big.into())).await.unwrap();
1114
1115 let text = read_text(&mut ws).await;
1116 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1117 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1118 }
1119
1120 #[tokio::test]
1122 async fn ws_ping_pong_response() {
1123 let addr = spawn_ws_server().await;
1124 let mut ws = ws_connect(addr).await;
1125
1126 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1127
1128 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1129 loop {
1130 let msg = ws.next().await.unwrap().unwrap();
1131 if let WsMessage::Pong(data) = msg {
1132 return data;
1133 }
1134 }
1135 })
1136 .await
1137 .expect("should get pong within 3s");
1138 assert_eq!(pong, vec![42, 43]);
1139 }
1140
1141 #[tokio::test]
1143 async fn ws_get_task_invalid_params() {
1144 let addr = spawn_ws_server().await;
1145 let mut ws = ws_connect(addr).await;
1146
1147 let req = serde_json::json!({
1149 "jsonrpc": "2.0",
1150 "method": "GetTask",
1151 "id": "gti-1",
1152 "params": {"wrong_field": 123}
1153 })
1154 .to_string();
1155 ws.send(WsMessage::Text(req.into())).await.unwrap();
1156
1157 let text = read_text(&mut ws).await;
1158 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1159 assert!(
1160 v.get("error").is_some(),
1161 "expected error for bad params: {text}"
1162 );
1163 }
1164
1165 #[tokio::test]
1167 async fn ws_send_streaming_message_events() {
1168 let addr = spawn_ws_server().await;
1169 let mut ws = ws_connect(addr).await;
1170
1171 let req = serde_json::json!({
1172 "jsonrpc": "2.0",
1173 "method": "SendStreamingMessage",
1174 "id": "ssm-1",
1175 "params": {
1176 "message": {
1177 "messageId": "msg-stream-1",
1178 "role": "ROLE_USER",
1179 "parts": [{"text": "stream me"}]
1180 }
1181 }
1182 })
1183 .to_string();
1184 ws.send(WsMessage::Text(req.into())).await.unwrap();
1185
1186 let mut frames = Vec::new();
1188 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1189 loop {
1190 let msg = ws.next().await.unwrap().unwrap();
1191 let text = msg.into_text().unwrap();
1192 let done = text.contains("stream_complete");
1193 frames.push(text);
1194 if done {
1195 break;
1196 }
1197 }
1198 });
1199 timeout.await.expect("streaming should complete within 5s");
1200
1201 assert!(
1203 frames.len() >= 3,
1204 "expected >= 3 frames, got {}: {:?}",
1205 frames.len(),
1206 frames
1207 );
1208 assert!(frames.last().unwrap().contains("stream_complete"));
1210 }
1211
1212 #[tokio::test]
1214 async fn ws_send_message_invalid_params() {
1215 let addr = spawn_ws_server().await;
1216 let mut ws = ws_connect(addr).await;
1217
1218 let req = serde_json::json!({
1219 "jsonrpc": "2.0",
1220 "method": "SendMessage",
1221 "id": "smi-1",
1222 "params": {"not_message": true}
1223 })
1224 .to_string();
1225 ws.send(WsMessage::Text(req.into())).await.unwrap();
1226
1227 let text = read_text(&mut ws).await;
1228 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1229 assert!(
1230 v.get("error").is_some(),
1231 "expected error for bad send params: {text}"
1232 );
1233 }
1234
1235 #[tokio::test]
1237 async fn ws_subscribe_invalid_params() {
1238 let addr = spawn_ws_server().await;
1239 let mut ws = ws_connect(addr).await;
1240
1241 let req = serde_json::json!({
1242 "jsonrpc": "2.0",
1243 "method": "SubscribeToTask",
1244 "id": "subi-1",
1245 "params": {}
1246 })
1247 .to_string();
1248 ws.send(WsMessage::Text(req.into())).await.unwrap();
1249
1250 let text = read_text(&mut ws).await;
1251 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1252 assert!(
1253 v.get("error").is_some(),
1254 "expected error for bad subscribe params: {text}"
1255 );
1256 }
1257
1258 #[tokio::test]
1260 async fn ws_cancel_task_invalid_params() {
1261 let addr = spawn_ws_server().await;
1262 let mut ws = ws_connect(addr).await;
1263
1264 let req = serde_json::json!({
1265 "jsonrpc": "2.0",
1266 "method": "CancelTask",
1267 "id": "cti-1",
1268 "params": {"wrong": 1}
1269 })
1270 .to_string();
1271 ws.send(WsMessage::Text(req.into())).await.unwrap();
1272
1273 let text = read_text(&mut ws).await;
1274 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1275 assert!(v.get("error").is_some(), "expected error: {text}");
1276 }
1277
1278 #[tokio::test]
1280 async fn ws_list_tasks_with_filters() {
1281 let addr = spawn_ws_server().await;
1282 let mut ws = ws_connect(addr).await;
1283
1284 let req = serde_json::json!({
1285 "jsonrpc": "2.0",
1286 "method": "ListTasks",
1287 "id": "ltf-1",
1288 "params": {
1289 "contextId": "ctx-1",
1290 "pageSize": 10
1291 }
1292 })
1293 .to_string();
1294 ws.send(WsMessage::Text(req.into())).await.unwrap();
1295
1296 let text = read_text(&mut ws).await;
1297 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1298 assert_eq!(v["id"], "ltf-1");
1299 assert!(v.get("result").is_some(), "expected result: {text}");
1300 }
1301
1302 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
1305
1306 async fn ws_call(
1308 ws: &mut tokio_tungstenite::WebSocketStream<
1309 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1310 >,
1311 req: serde_json::Value,
1312 ) -> serde_json::Value {
1313 ws.send(WsMessage::Text(req.to_string().into()))
1314 .await
1315 .expect("send");
1316 let text = read_text(ws).await;
1317 serde_json::from_str(&text).expect("response should be JSON")
1318 }
1319
1320 #[tokio::test]
1325 async fn ws_legacy_method_names_rejected() {
1326 let addr = spawn_ws_server().await;
1327 let mut ws = ws_connect(addr).await;
1328
1329 for legacy in ["message/send", "tasks/list", "tasks/get"] {
1330 let v = ws_call(
1331 &mut ws,
1332 serde_json::json!({
1333 "jsonrpc": "2.0",
1334 "method": legacy,
1335 "id": format!("legacy-{legacy}"),
1336 "params": {}
1337 }),
1338 )
1339 .await;
1340 assert_eq!(
1341 v["error"]["code"].as_i64(),
1342 Some(-32601),
1343 "v0.3-style name {legacy} must be MethodNotFound: {v}"
1344 );
1345 }
1346 }
1347
1348 #[tokio::test]
1351 #[allow(clippy::too_many_lines)]
1352 async fn ws_push_config_methods_routed() {
1353 use crate::push::PushSender;
1354 use a2a_protocol_types::push::TaskPushNotificationConfig;
1355
1356 struct NoopSender;
1357 impl PushSender for NoopSender {
1358 fn send<'a>(
1359 &'a self,
1360 _url: &'a str,
1361 _event: &'a StreamResponse,
1362 _config: &'a TaskPushNotificationConfig,
1363 ) -> std::pin::Pin<
1364 Box<
1365 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1366 + Send
1367 + 'a,
1368 >,
1369 > {
1370 Box::pin(async { Ok(()) })
1371 }
1372 fn allows_private_urls(&self) -> bool {
1373 true
1374 }
1375 }
1376
1377 let handler = Arc::new(
1378 RequestHandlerBuilder::new(EchoExec)
1379 .with_push_sender(NoopSender)
1380 .build()
1381 .unwrap(),
1382 );
1383 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1384 let addr = dispatcher
1385 .serve_with_addr("127.0.0.1:0")
1386 .await
1387 .expect("bind");
1388 let mut ws = ws_connect(addr).await;
1389
1390 let v = ws_call(
1392 &mut ws,
1393 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
1394 )
1395 .await;
1396 let task_id = v["result"]["task"]["id"]
1397 .as_str()
1398 .expect("task id in send result")
1399 .to_owned();
1400
1401 let v = ws_call(
1403 &mut ws,
1404 serde_json::json!({
1405 "jsonrpc": "2.0",
1406 "method": "CreateTaskPushNotificationConfig",
1407 "id": "pc-1",
1408 "params": {
1409 "taskId": task_id,
1410 "url": "https://example.com/hook"
1411 }
1412 }),
1413 )
1414 .await;
1415 assert!(v.get("result").is_some(), "set push config failed: {v}");
1416 let config_id = v["result"]["id"]
1417 .as_str()
1418 .expect("server-assigned config id")
1419 .to_owned();
1420
1421 let v = ws_call(
1423 &mut ws,
1424 serde_json::json!({
1425 "jsonrpc": "2.0",
1426 "method": "GetTaskPushNotificationConfig",
1427 "id": "pc-2",
1428 "params": {"taskId": task_id, "id": config_id}
1429 }),
1430 )
1431 .await;
1432 assert!(v.get("result").is_some(), "get push config failed: {v}");
1433
1434 let v = ws_call(
1436 &mut ws,
1437 serde_json::json!({
1438 "jsonrpc": "2.0",
1439 "method": "ListTaskPushNotificationConfigs",
1440 "id": "pc-3",
1441 "params": {"taskId": task_id}
1442 }),
1443 )
1444 .await;
1445 assert!(v.get("result").is_some(), "list push configs failed: {v}");
1446 assert!(
1447 v["result"]["configs"].is_array(),
1448 "expected configs array: {v}"
1449 );
1450
1451 let v = ws_call(
1453 &mut ws,
1454 serde_json::json!({
1455 "jsonrpc": "2.0",
1456 "method": "DeleteTaskPushNotificationConfig",
1457 "id": "pc-4",
1458 "params": {"taskId": task_id, "id": config_id}
1459 }),
1460 )
1461 .await;
1462 assert!(v.get("result").is_some(), "delete push config failed: {v}");
1463 }
1464
1465 #[tokio::test]
1468 async fn ws_get_extended_agent_card_routed() {
1469 let addr = spawn_ws_server().await;
1470 let mut ws = ws_connect(addr).await;
1471
1472 let v = ws_call(
1473 &mut ws,
1474 serde_json::json!({
1475 "jsonrpc": "2.0",
1476 "method": "GetExtendedAgentCard",
1477 "id": "card-1",
1478 "params": {}
1479 }),
1480 )
1481 .await;
1482 let err = v.get("error").expect("expected an error response");
1485 assert_ne!(
1486 err["code"], -32601,
1487 "GetExtendedAgentCard must be routed, got: {v}"
1488 );
1489 }
1490
1491 #[tokio::test]
1495 async fn ws_upgrade_headers_drive_tenant_resolution() {
1496 use crate::tenant_resolver::HeaderTenantResolver;
1497
1498 let handler = Arc::new(
1499 RequestHandlerBuilder::new(EchoExec)
1500 .with_tenant_resolver(HeaderTenantResolver::default())
1501 .require_resolved_tenant()
1502 .build()
1503 .unwrap(),
1504 );
1505 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1506 let addr = dispatcher
1507 .serve_with_addr("127.0.0.1:0")
1508 .await
1509 .expect("bind");
1510
1511 let mut ws = ws_connect(addr).await;
1513 let v = ws_call(
1514 &mut ws,
1515 serde_json::json!({
1516 "jsonrpc": "2.0",
1517 "method": "ListTasks",
1518 "id": "t-1",
1519 "params": {}
1520 }),
1521 )
1522 .await;
1523 let err = v.get("error").expect("headerless request must be rejected");
1524 let msg = err["message"].as_str().unwrap_or("");
1525 assert!(
1526 msg.contains("tenant"),
1527 "expected strict-tenancy rejection, got: {v}"
1528 );
1529
1530 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1532 req.headers_mut()
1533 .insert("a2a-version", "1.0".parse().unwrap());
1534 req.headers_mut()
1535 .insert("x-tenant-id", "acme".parse().unwrap());
1536 let (mut ws, _) = tokio_tungstenite::connect_async(req)
1537 .await
1538 .expect("connect");
1539 let v = ws_call(
1540 &mut ws,
1541 serde_json::json!({
1542 "jsonrpc": "2.0",
1543 "method": "ListTasks",
1544 "id": "t-2",
1545 "params": {}
1546 }),
1547 )
1548 .await;
1549 assert!(
1550 v.get("result").is_some(),
1551 "tenant header on the upgrade request must reach the resolver: {v}"
1552 );
1553 }
1554
1555 #[tokio::test]
1557 async fn ws_version_mismatch_rejects_handshake() {
1558 let addr = spawn_ws_server().await;
1559
1560 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1561 req.headers_mut()
1562 .insert("a2a-version", "2.0".parse().unwrap());
1563 let outcome = tokio_tungstenite::connect_async(req).await;
1564 assert!(
1565 outcome.is_err(),
1566 "handshake with A2A-Version 2.0 must be rejected"
1567 );
1568
1569 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1571 req.headers_mut()
1572 .insert("a2a-version", "1.0".parse().unwrap());
1573 assert!(
1574 tokio_tungstenite::connect_async(req).await.is_ok(),
1575 "handshake with A2A-Version 1.0 must succeed"
1576 );
1577 }
1578
1579 #[tokio::test]
1582 async fn ws_handshake_timeout_disconnects_stalled_peer() {
1583 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1584 let dispatcher = Arc::new(
1585 WebSocketDispatcher::new(handler)
1586 .with_handshake_timeout(std::time::Duration::from_millis(200)),
1587 );
1588 let addr = dispatcher
1589 .serve_with_addr("127.0.0.1:0")
1590 .await
1591 .expect("bind");
1592
1593 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
1595 let mut buf = [0u8; 16];
1596 let read = tokio::time::timeout(
1599 std::time::Duration::from_secs(5),
1600 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
1601 )
1602 .await
1603 .expect("server should close the stalled connection");
1604 assert!(
1605 matches!(read, Ok(0) | Err(_)),
1606 "expected EOF/reset from server, got: {read:?}"
1607 );
1608 }
1609
1610 #[tokio::test]
1612 async fn ws_binary_frame_gets_error_response() {
1613 let addr = spawn_ws_server().await;
1614 let mut ws = ws_connect(addr).await;
1615
1616 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
1617 .await
1618 .unwrap();
1619
1620 let text = read_text(&mut ws).await;
1621 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1622 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
1623 assert!(
1624 v["error"]["message"]
1625 .as_str()
1626 .unwrap_or("")
1627 .contains("binary"),
1628 "error should explain binary frames are unsupported: {v}"
1629 );
1630 }
1631}