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 let Ok(permit) = semaphore.clone().try_acquire_owned() else {
296 let err_resp = JsonRpcErrorResponse::new(
301 best_effort_request_id(&text),
302 JsonRpcError::new(
303 -32000,
304 "server busy: too many concurrent requests".to_string(),
305 ),
306 );
307 send_json(writer, &err_resp).await;
308 continue;
309 };
310
311 let writer = Arc::clone(writer);
312 let handler = Arc::clone(&self.handler);
313 let headers = Arc::clone(headers);
314 tokio::spawn(async move {
315 Box::pin(process_ws_message(&handler, &text, writer, &headers)).await;
317 drop(permit); });
319 }
320 Ok(WsMessage::Binary(_)) => {
321 let err_resp = JsonRpcErrorResponse::new(
325 None,
326 JsonRpcError::new(
327 -32700,
328 "binary frames are not supported; send JSON-RPC as text frames"
329 .to_string(),
330 ),
331 );
332 send_json(writer, &err_resp).await;
333 }
334 Ok(WsMessage::Close(_)) | Err(_) => break,
335 Ok(_) => {}
340 }
341 }
342 }
343}
344
345fn extract_upgrade_headers(req: &WsUpgradeRequest) -> HashMap<String, String> {
350 let mut map: HashMap<String, String> = req
351 .headers()
352 .iter()
353 .filter_map(|(k, v)| {
354 v.to_str()
355 .ok()
356 .map(|val| (k.as_str().to_lowercase(), val.to_owned()))
357 })
358 .collect();
359 map.insert(":path".to_owned(), req.uri().path().to_owned());
363 map
364}
365
366#[allow(clippy::result_large_err)]
374fn check_a2a_version(req: &WsUpgradeRequest, require: bool) -> Result<(), ErrorResponse> {
375 let value = req
376 .headers()
377 .get(a2a_protocol_types::A2A_VERSION_HEADER)
378 .and_then(|v| v.to_str().ok());
379 let v = value.unwrap_or("").trim();
380 if v.is_empty() {
381 if !require {
382 return Ok(());
383 }
384 } else {
386 let major = v.split('.').next().and_then(|s| s.parse::<u32>().ok());
387 if major == Some(1) {
388 return Ok(());
389 }
390 }
391 let a2a_err = a2a_protocol_types::error::A2aError::version_not_supported(if v.is_empty() {
395 "A2A version '0.3' is not supported by this server; expected '1.0' (send the A2A-Version header)"
396 .to_owned()
397 } else {
398 format!("unsupported A2A version: {v}; this server supports 1.x")
399 });
400 let mut error_obj = serde_json::json!({
401 "error": {
402 "code": a2a_err.code.http_status(),
403 "status": a2a_err.code.grpc_status(),
404 "message": a2a_err.message,
405 }
406 });
407 let details = a2a_err.error_info_data(None);
408 if !details.is_null() {
409 error_obj["error"]["details"] = details;
410 }
411 let body = error_obj.to_string();
412 let resp = tokio_tungstenite::tungstenite::http::Response::builder()
413 .status(400)
414 .header("content-type", "application/json")
415 .body(Some(body))
416 .unwrap_or_else(|_| {
417 let mut r = ErrorResponse::new(Some(String::new()));
418 *r.status_mut() = tokio_tungstenite::tungstenite::http::StatusCode::BAD_REQUEST;
419 r
420 });
421 Err(resp)
422}
423
424fn best_effort_request_id(text: &str) -> JsonRpcId {
427 let v: serde_json::Value = serde_json::from_str(text).ok()?;
428 match v.get("id") {
429 Some(serde_json::Value::Null) | None => None,
430 Some(id) => Some(id.clone()),
431 }
432}
433
434#[derive(Debug)]
436enum WsError {
437 Handshake(tokio_tungstenite::tungstenite::Error),
438 HandshakeTimeout,
439}
440
441impl std::fmt::Display for WsError {
442 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
443 match self {
444 Self::Handshake(e) => write!(f, "WebSocket handshake failed: {e}"),
445 Self::HandshakeTimeout => write!(f, "WebSocket handshake timed out"),
446 }
447 }
448}
449
450type WsSink = Arc<tokio::sync::Mutex<SplitSink<WebSocketStream<TcpStream>, WsMessage>>>;
451
452#[allow(clippy::too_many_lines)]
458async fn process_ws_message(
459 handler: &RequestHandler,
460 text: &str,
461 writer: WsSink,
462 headers: &HashMap<String, String>,
463) {
464 let rpc_req: JsonRpcRequest = match serde_json::from_str(text) {
465 Ok(req) => req,
466 Err(e) => {
467 let err_resp = JsonRpcErrorResponse::new(
468 None,
469 JsonRpcError::new(-32700, format!("parse error: {e}")),
470 );
471 send_json(&writer, &err_resp).await;
472 return;
473 }
474 };
475
476 let id = rpc_req.id.to_response_id();
477
478 match rpc_req.method.as_str() {
479 "SendMessage" => {
480 dispatch_send_message(handler, &rpc_req, false, headers, id, &writer).await;
481 }
482 "SendStreamingMessage" | "message/stream" => {
483 dispatch_send_message(handler, &rpc_req, true, headers, id, &writer).await;
484 }
485 "GetTask" => {
486 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
487 Box::pin(async move {
488 let params: a2a_protocol_types::params::TaskQueryParams =
489 serde_json::from_value(p).map_err(|e| {
490 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
491 })?;
492 h.on_get_task(params, Some(hdr))
493 .await
494 .map(|r| serde_json::to_value(&r).unwrap_or_default())
495 .map_err(|e| e.to_a2a_error())
496 })
497 })
498 .await;
499 }
500 "ListTasks" => {
501 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
502 Box::pin(async move {
503 let params: a2a_protocol_types::params::ListTasksParams =
504 serde_json::from_value(p).map_err(|e| {
505 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
506 })?;
507 h.on_list_tasks(params, Some(hdr))
508 .await
509 .map(|r| serde_json::to_value(&r).unwrap_or_default())
510 .map_err(|e| e.to_a2a_error())
511 })
512 })
513 .await;
514 }
515 "CancelTask" => {
516 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
517 Box::pin(async move {
518 let params: a2a_protocol_types::params::CancelTaskParams =
519 serde_json::from_value(p).map_err(|e| {
520 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
521 })?;
522 h.on_cancel_task(params, Some(hdr))
523 .await
524 .map(|r| serde_json::to_value(&r).unwrap_or_default())
525 .map_err(|e| e.to_a2a_error())
526 })
527 })
528 .await;
529 }
530 "SubscribeToTask" => {
531 let params = match parse_params::<a2a_protocol_types::params::TaskIdParams>(
532 rpc_req.params.as_ref(),
533 ) {
534 Ok(p) => p,
535 Err(e) => {
536 send_error(&writer, id, &e).await;
537 return;
538 }
539 };
540 match handler.on_resubscribe(params, Some(headers)).await {
541 Ok(reader) => {
542 stream_events(&writer, reader, id).await;
543 }
544 Err(e) => {
545 send_error(&writer, id, &e).await;
546 }
547 }
548 }
549 "CreateTaskPushNotificationConfig" => {
550 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
551 Box::pin(async move {
552 let params: a2a_protocol_types::push::TaskPushNotificationConfig =
553 serde_json::from_value(p).map_err(|e| {
554 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
555 })?;
556 h.on_set_push_config(params, Some(hdr))
557 .await
558 .map(|r| serde_json::to_value(&r).unwrap_or_default())
559 .map_err(|e| e.to_a2a_error())
560 })
561 })
562 .await;
563 }
564 "GetTaskPushNotificationConfig" => {
565 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
566 Box::pin(async move {
567 let params: a2a_protocol_types::params::GetPushConfigParams =
568 serde_json::from_value(p).map_err(|e| {
569 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
570 })?;
571 h.on_get_push_config(params, Some(hdr))
572 .await
573 .map(|r| serde_json::to_value(&r).unwrap_or_default())
574 .map_err(|e| e.to_a2a_error())
575 })
576 })
577 .await;
578 }
579 "ListTaskPushNotificationConfigs" => {
580 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
581 Box::pin(async move {
582 let params: a2a_protocol_types::params::ListPushConfigsParams =
583 serde_json::from_value(p).map_err(|e| {
584 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
585 })?;
586 h.on_list_push_configs(¶ms.task_id, params.tenant.as_deref(), Some(hdr))
587 .await
588 .map(|configs| {
589 let resp = a2a_protocol_types::responses::ListPushConfigsResponse {
590 configs,
591 next_page_token: None,
592 };
593 serde_json::to_value(&resp).unwrap_or_default()
594 })
595 .map_err(|e| e.to_a2a_error())
596 })
597 })
598 .await;
599 }
600 "DeleteTaskPushNotificationConfig" => {
601 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, p, hdr| {
602 Box::pin(async move {
603 let params: a2a_protocol_types::params::DeletePushConfigParams =
604 serde_json::from_value(p).map_err(|e| {
605 a2a_protocol_types::error::A2aError::invalid_params(e.to_string())
606 })?;
607 h.on_delete_push_config(params, Some(hdr))
608 .await
609 .map(|()| serde_json::json!({}))
610 .map_err(|e| e.to_a2a_error())
611 })
612 })
613 .await;
614 }
615 "GetExtendedAgentCard" => {
616 dispatch_simple(handler, &rpc_req, id, headers, &writer, |h, _p, hdr| {
617 Box::pin(async move {
618 h.on_get_extended_agent_card(Some(hdr))
619 .await
620 .map(|r| serde_json::to_value(&r).unwrap_or_default())
621 .map_err(|e| e.to_a2a_error())
622 })
623 })
624 .await;
625 }
626 other => {
627 let err = ServerError::MethodNotFound(other.to_owned());
628 send_error(&writer, id, &err).await;
629 }
630 }
631}
632
633async fn dispatch_send_message(
635 handler: &RequestHandler,
636 rpc_req: &JsonRpcRequest,
637 streaming: bool,
638 headers: &HashMap<String, String>,
639 id: JsonRpcId,
640 writer: &WsSink,
641) {
642 let params = match parse_params::<a2a_protocol_types::params::MessageSendParams>(
643 rpc_req.params.as_ref(),
644 ) {
645 Ok(p) => p,
646 Err(e) => {
647 send_error(writer, id, &e).await;
648 return;
649 }
650 };
651
652 match handler
653 .on_send_message(params, streaming, Some(headers))
654 .await
655 {
656 Ok(SendMessageResult::Response(resp)) => {
657 let result = serde_json::to_value(&resp).unwrap_or(serde_json::Value::Null);
658 let success = JsonRpcSuccessResponse {
659 jsonrpc: JsonRpcVersion,
660 id,
661 result,
662 };
663 send_json(writer, &success).await;
664 }
665 Ok(SendMessageResult::Stream(reader)) => {
666 stream_events(writer, reader, id).await;
667 }
668 Err(e) => {
669 send_error(writer, id, &e).await;
670 }
671 }
672}
673
674async fn stream_events(
676 writer: &WsSink,
677 mut reader: crate::streaming::InMemoryQueueReader,
678 id: JsonRpcId,
679) {
680 while let Some(event) = reader.read().await {
681 match event {
682 Ok(stream_resp) => {
683 let envelope = JsonRpcSuccessResponse {
686 jsonrpc: JsonRpcVersion,
687 id: id.clone(),
688 result: stream_resp,
689 };
690 let json = serde_json::to_string(&envelope).unwrap_or_default();
691 let mut w = writer.lock().await;
692 if w.send(WsMessage::Text(json.into())).await.is_err() {
693 return; }
695 drop(w);
696 }
697 Err(e) => {
698 let err_resp =
699 JsonRpcErrorResponse::new(id.clone(), JsonRpcError::new(-32000, e.to_string()));
700 send_json(writer, &err_resp).await;
701 return;
702 }
703 }
704 }
705
706 let success = JsonRpcSuccessResponse {
708 jsonrpc: JsonRpcVersion,
709 id,
710 result: serde_json::json!({"status": "stream_complete"}),
711 };
712 send_json(writer, &success).await;
713}
714
715async fn dispatch_simple<'a, F>(
717 handler: &'a RequestHandler,
718 rpc_req: &JsonRpcRequest,
719 id: JsonRpcId,
720 headers: &'a HashMap<String, String>,
721 writer: &WsSink,
722 f: F,
723) where
724 F: FnOnce(
725 &'a RequestHandler,
726 serde_json::Value,
727 &'a HashMap<String, String>,
728 ) -> std::pin::Pin<
729 Box<
730 dyn std::future::Future<
731 Output = Result<serde_json::Value, a2a_protocol_types::error::A2aError>,
732 > + Send
733 + 'a,
734 >,
735 >,
736{
737 let params = rpc_req.params.clone().unwrap_or(serde_json::Value::Null);
738 match f(handler, params, headers).await {
739 Ok(result) => {
740 let success = JsonRpcSuccessResponse {
741 jsonrpc: JsonRpcVersion,
742 id,
743 result,
744 };
745 send_json(writer, &success).await;
746 }
747 Err(e) => {
748 let err_resp =
749 JsonRpcErrorResponse::new(id, JsonRpcError::new(e.code.as_i32(), e.message));
750 send_json(writer, &err_resp).await;
751 }
752 }
753}
754
755async fn send_json<T: serde::Serialize + Sync>(writer: &WsSink, value: &T) {
757 let json = serde_json::to_string(value).unwrap_or_default();
758 let mut w = writer.lock().await;
759 let _ = w.send(WsMessage::Text(json.into())).await;
760 drop(w);
761}
762
763async fn send_error(writer: &WsSink, id: JsonRpcId, err: &ServerError) {
765 let a2a_err = err.to_a2a_error();
766 let resp = JsonRpcErrorResponse::new(
767 id,
768 JsonRpcError::new(a2a_err.code.as_i32(), a2a_err.message),
769 );
770 send_json(writer, &resp).await;
771}
772
773fn parse_params<T: serde::de::DeserializeOwned>(
775 params: Option<&serde_json::Value>,
776) -> Result<T, ServerError> {
777 let value = params.cloned().unwrap_or(serde_json::Value::Null);
778 serde_json::from_value(value)
779 .map_err(|e| ServerError::InvalidParams(format!("invalid params: {e}")))
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785
786 #[test]
787 fn parse_params_with_valid_json() {
788 let value = Some(serde_json::json!({"id": "task-1"}));
789 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
790 parse_params(value.as_ref());
791 assert!(result.is_ok());
792 assert_eq!(result.unwrap().id, "task-1");
793 }
794
795 #[test]
796 fn parse_params_with_none_returns_error() {
797 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> = parse_params(None);
798 assert!(result.is_err());
799 }
800
801 #[test]
802 fn parse_params_with_wrong_type_returns_error() {
803 let value = Some(serde_json::json!("not an object"));
804 let result: Result<a2a_protocol_types::params::TaskQueryParams, _> =
805 parse_params(value.as_ref());
806 assert!(result.is_err());
807 }
808
809 #[test]
811 fn ws_error_display_contains_message() {
812 let err = WsError::Handshake(tokio_tungstenite::tungstenite::Error::ConnectionClosed);
813 let s = err.to_string();
814 assert!(s.contains("WebSocket handshake failed"));
815 }
816
817 #[test]
818 fn ws_error_display_handshake_timeout() {
819 let s = WsError::HandshakeTimeout.to_string();
820 assert!(s.contains("timed out"), "got: {s}");
821 }
822
823 #[test]
826 fn best_effort_request_id_extracts_string_and_number() {
827 assert_eq!(
828 best_effort_request_id(r#"{"jsonrpc":"2.0","id":"req-1","method":"GetTask"}"#),
829 Some(serde_json::json!("req-1"))
830 );
831 assert_eq!(
832 best_effort_request_id(r#"{"jsonrpc":"2.0","id":7,"method":"GetTask"}"#),
833 Some(serde_json::json!(7))
834 );
835 }
836
837 #[test]
838 fn best_effort_request_id_none_for_missing_null_or_invalid() {
839 assert_eq!(best_effort_request_id(r#"{"jsonrpc":"2.0"}"#), None);
840 assert_eq!(best_effort_request_id(r#"{"id":null}"#), None);
841 assert_eq!(best_effort_request_id("not json {{"), None);
842 }
843
844 #[test]
846 fn websocket_dispatcher_new() {
847 use crate::agent_executor;
848 use crate::RequestHandlerBuilder;
849 use std::sync::Arc;
850 struct DummyExec;
851 agent_executor!(DummyExec, |_ctx, _queue| async { Ok(()) });
852 let handler = Arc::new(RequestHandlerBuilder::new(DummyExec).build().unwrap());
853 let _dispatcher = WebSocketDispatcher::new(handler);
854 }
855
856 use crate::agent_executor;
859 use crate::RequestHandlerBuilder;
860 use a2a_protocol_types::events::{StreamResponse, TaskStatusUpdateEvent};
861 use a2a_protocol_types::task::{ContextId, TaskState, TaskStatus};
862 use futures_util::{SinkExt, StreamExt};
863
864 struct EchoExec;
865 agent_executor!(EchoExec, |ctx, queue| async {
866 queue
867 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
868 task_id: ctx.task_id.clone(),
869 context_id: ContextId::new(ctx.context_id.clone()),
870 status: TaskStatus::new(TaskState::Working),
871 metadata: None,
872 }))
873 .await?;
874 queue
875 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
876 task_id: ctx.task_id.clone(),
877 context_id: ContextId::new(ctx.context_id.clone()),
878 status: TaskStatus::new(TaskState::Completed),
879 metadata: None,
880 }))
881 .await?;
882 Ok(())
883 });
884
885 async fn spawn_ws_server() -> std::net::SocketAddr {
886 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
887 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
888 dispatcher
889 .serve_with_addr("127.0.0.1:0")
890 .await
891 .expect("bind to port 0")
892 }
893
894 async fn ws_connect(
895 addr: std::net::SocketAddr,
896 ) -> tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>
897 {
898 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
899 let mut req = format!("ws://{addr}").into_client_request().expect("url");
900 req.headers_mut()
901 .insert("a2a-version", "1.0".parse().expect("header"));
902 let (ws, _) = tokio_tungstenite::connect_async(req)
903 .await
904 .expect("ws connect");
905 ws
906 }
907
908 async fn read_text(
910 ws: &mut tokio_tungstenite::WebSocketStream<
911 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
912 >,
913 ) -> String {
914 let msg = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
915 .await
916 .expect("timeout waiting for WS frame")
917 .expect("stream ended")
918 .expect("ws error");
919 msg.into_text()
920 .expect("not a text frame")
921 .as_str()
922 .to_owned()
923 }
924
925 fn send_message_json(id: &str) -> String {
926 serde_json::json!({
927 "jsonrpc": "2.0",
928 "method": "SendMessage",
929 "id": id,
930 "params": {
931 "message": {
932 "messageId": "msg-1",
933 "role": "ROLE_USER",
934 "parts": [{"text": "hello"}]
935 }
936 }
937 })
938 .to_string()
939 }
940
941 #[tokio::test]
943 async fn ws_send_message_success() {
944 let addr = spawn_ws_server().await;
945 let mut ws = ws_connect(addr).await;
946
947 ws.send(WsMessage::Text(send_message_json("sm-1").into()))
948 .await
949 .unwrap();
950
951 let text = read_text(&mut ws).await;
952 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
953 assert_eq!(v["id"], "sm-1");
954 assert!(v.get("result").is_some(), "expected result key: {text}");
956 }
957
958 #[tokio::test]
960 async fn ws_get_task_not_found() {
961 let addr = spawn_ws_server().await;
962 let mut ws = ws_connect(addr).await;
963
964 let req = serde_json::json!({
965 "jsonrpc": "2.0",
966 "method": "GetTask",
967 "id": "gt-1",
968 "params": {"id": "nonexistent"}
969 })
970 .to_string();
971 ws.send(WsMessage::Text(req.into())).await.unwrap();
972
973 let text = read_text(&mut ws).await;
974 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
975 assert!(v.get("error").is_some(), "expected error: {text}");
976 }
977
978 #[tokio::test]
980 async fn ws_list_tasks_success() {
981 let addr = spawn_ws_server().await;
982 let mut ws = ws_connect(addr).await;
983
984 let req = serde_json::json!({
985 "jsonrpc": "2.0",
986 "method": "ListTasks",
987 "id": "lt-1",
988 "params": {}
989 })
990 .to_string();
991 ws.send(WsMessage::Text(req.into())).await.unwrap();
992
993 let text = read_text(&mut ws).await;
994 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
995 assert_eq!(v["id"], "lt-1");
996 assert!(v.get("result").is_some(), "expected result: {text}");
997 }
998
999 #[tokio::test]
1001 async fn ws_cancel_task_not_found() {
1002 let addr = spawn_ws_server().await;
1003 let mut ws = ws_connect(addr).await;
1004
1005 let req = serde_json::json!({
1006 "jsonrpc": "2.0",
1007 "method": "CancelTask",
1008 "id": "ct-1",
1009 "params": {"id": "nonexistent"}
1010 })
1011 .to_string();
1012 ws.send(WsMessage::Text(req.into())).await.unwrap();
1013
1014 let text = read_text(&mut ws).await;
1015 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1016 assert!(v.get("error").is_some(), "expected error: {text}");
1017 }
1018
1019 #[tokio::test]
1021 async fn ws_subscribe_task_not_found() {
1022 let addr = spawn_ws_server().await;
1023 let mut ws = ws_connect(addr).await;
1024
1025 let req = serde_json::json!({
1026 "jsonrpc": "2.0",
1027 "method": "SubscribeToTask",
1028 "id": "sub-1",
1029 "params": {"id": "nonexistent"}
1030 })
1031 .to_string();
1032 ws.send(WsMessage::Text(req.into())).await.unwrap();
1033
1034 let text = read_text(&mut ws).await;
1035 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1036 assert!(v.get("error").is_some(), "expected error: {text}");
1037 }
1038
1039 #[tokio::test]
1041 async fn ws_unknown_method_error() {
1042 let addr = spawn_ws_server().await;
1043 let mut ws = ws_connect(addr).await;
1044
1045 let req = serde_json::json!({
1046 "jsonrpc": "2.0",
1047 "method": "FooBar",
1048 "id": "unk-1",
1049 "params": {}
1050 })
1051 .to_string();
1052 ws.send(WsMessage::Text(req.into())).await.unwrap();
1053
1054 let text = read_text(&mut ws).await;
1055 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1056 assert!(v.get("error").is_some(), "expected error: {text}");
1057 let msg = v["error"]["message"].as_str().unwrap_or("");
1058 assert!(
1059 msg.to_lowercase().contains("method")
1060 || msg.to_lowercase().contains("not found")
1061 || msg.to_lowercase().contains("unsupported"),
1062 "error message should mention method not found: {msg}"
1063 );
1064 }
1065
1066 #[tokio::test]
1068 async fn ws_invalid_json_parse_error() {
1069 let addr = spawn_ws_server().await;
1070 let mut ws = ws_connect(addr).await;
1071
1072 ws.send(WsMessage::Text("this is not json {{".into()))
1073 .await
1074 .unwrap();
1075
1076 let text = read_text(&mut ws).await;
1077 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1078 assert_eq!(v["error"]["code"], -32700, "expected parse error code");
1079 }
1080
1081 #[tokio::test]
1089 async fn ws_oversized_message_rejected() {
1090 let addr = spawn_ws_server().await;
1091 let mut ws = ws_connect(addr).await;
1092
1093 let big = "x".repeat(4 * 1024 * 1024 + 1);
1095 if ws.send(WsMessage::Text(big.into())).await.is_ok() {
1099 let outcome = tokio::time::timeout(std::time::Duration::from_secs(5), ws.next())
1102 .await
1103 .expect("server should react to the oversized message");
1104 match outcome {
1105 None | Some(Err(_) | Ok(WsMessage::Close(_))) => {}
1106 Some(Ok(frame)) => panic!(
1107 "server must not answer an oversized message with a frame, got: {frame:?}"
1108 ),
1109 }
1110 }
1111 }
1112
1113 #[tokio::test]
1117 async fn ws_large_message_under_cap_still_processed() {
1118 let addr = spawn_ws_server().await;
1119 let mut ws = ws_connect(addr).await;
1120
1121 let big = "x".repeat(3 * 1024 * 1024);
1122 ws.send(WsMessage::Text(big.into())).await.unwrap();
1123
1124 let text = read_text(&mut ws).await;
1125 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1126 assert_eq!(v["error"]["code"], -32700, "expected parse error: {text}");
1127 }
1128
1129 #[tokio::test]
1131 async fn ws_ping_pong_response() {
1132 let addr = spawn_ws_server().await;
1133 let mut ws = ws_connect(addr).await;
1134
1135 ws.send(WsMessage::Ping(vec![42, 43].into())).await.unwrap();
1136
1137 let pong = tokio::time::timeout(std::time::Duration::from_secs(3), async {
1138 loop {
1139 let msg = ws.next().await.unwrap().unwrap();
1140 if let WsMessage::Pong(data) = msg {
1141 return data;
1142 }
1143 }
1144 })
1145 .await
1146 .expect("should get pong within 3s");
1147 assert_eq!(pong, vec![42, 43]);
1148 }
1149
1150 #[tokio::test]
1152 async fn ws_get_task_invalid_params() {
1153 let addr = spawn_ws_server().await;
1154 let mut ws = ws_connect(addr).await;
1155
1156 let req = serde_json::json!({
1158 "jsonrpc": "2.0",
1159 "method": "GetTask",
1160 "id": "gti-1",
1161 "params": {"wrong_field": 123}
1162 })
1163 .to_string();
1164 ws.send(WsMessage::Text(req.into())).await.unwrap();
1165
1166 let text = read_text(&mut ws).await;
1167 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1168 assert!(
1169 v.get("error").is_some(),
1170 "expected error for bad params: {text}"
1171 );
1172 }
1173
1174 #[tokio::test]
1176 async fn ws_send_streaming_message_events() {
1177 let addr = spawn_ws_server().await;
1178 let mut ws = ws_connect(addr).await;
1179
1180 let req = serde_json::json!({
1181 "jsonrpc": "2.0",
1182 "method": "SendStreamingMessage",
1183 "id": "ssm-1",
1184 "params": {
1185 "message": {
1186 "messageId": "msg-stream-1",
1187 "role": "ROLE_USER",
1188 "parts": [{"text": "stream me"}]
1189 }
1190 }
1191 })
1192 .to_string();
1193 ws.send(WsMessage::Text(req.into())).await.unwrap();
1194
1195 let mut frames = Vec::new();
1197 let timeout = tokio::time::timeout(std::time::Duration::from_secs(5), async {
1198 loop {
1199 let msg = ws.next().await.unwrap().unwrap();
1200 let text = msg.into_text().unwrap();
1201 let done = text.contains("stream_complete");
1202 frames.push(text);
1203 if done {
1204 break;
1205 }
1206 }
1207 });
1208 timeout.await.expect("streaming should complete within 5s");
1209
1210 assert!(
1212 frames.len() >= 3,
1213 "expected >= 3 frames, got {}: {:?}",
1214 frames.len(),
1215 frames
1216 );
1217 assert!(frames.last().unwrap().contains("stream_complete"));
1219 }
1220
1221 #[tokio::test]
1223 async fn ws_send_message_invalid_params() {
1224 let addr = spawn_ws_server().await;
1225 let mut ws = ws_connect(addr).await;
1226
1227 let req = serde_json::json!({
1228 "jsonrpc": "2.0",
1229 "method": "SendMessage",
1230 "id": "smi-1",
1231 "params": {"not_message": true}
1232 })
1233 .to_string();
1234 ws.send(WsMessage::Text(req.into())).await.unwrap();
1235
1236 let text = read_text(&mut ws).await;
1237 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1238 assert!(
1239 v.get("error").is_some(),
1240 "expected error for bad send params: {text}"
1241 );
1242 }
1243
1244 #[tokio::test]
1246 async fn ws_subscribe_invalid_params() {
1247 let addr = spawn_ws_server().await;
1248 let mut ws = ws_connect(addr).await;
1249
1250 let req = serde_json::json!({
1251 "jsonrpc": "2.0",
1252 "method": "SubscribeToTask",
1253 "id": "subi-1",
1254 "params": {}
1255 })
1256 .to_string();
1257 ws.send(WsMessage::Text(req.into())).await.unwrap();
1258
1259 let text = read_text(&mut ws).await;
1260 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1261 assert!(
1262 v.get("error").is_some(),
1263 "expected error for bad subscribe params: {text}"
1264 );
1265 }
1266
1267 #[tokio::test]
1269 async fn ws_cancel_task_invalid_params() {
1270 let addr = spawn_ws_server().await;
1271 let mut ws = ws_connect(addr).await;
1272
1273 let req = serde_json::json!({
1274 "jsonrpc": "2.0",
1275 "method": "CancelTask",
1276 "id": "cti-1",
1277 "params": {"wrong": 1}
1278 })
1279 .to_string();
1280 ws.send(WsMessage::Text(req.into())).await.unwrap();
1281
1282 let text = read_text(&mut ws).await;
1283 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1284 assert!(v.get("error").is_some(), "expected error: {text}");
1285 }
1286
1287 #[tokio::test]
1289 async fn ws_list_tasks_with_filters() {
1290 let addr = spawn_ws_server().await;
1291 let mut ws = ws_connect(addr).await;
1292
1293 let req = serde_json::json!({
1294 "jsonrpc": "2.0",
1295 "method": "ListTasks",
1296 "id": "ltf-1",
1297 "params": {
1298 "contextId": "ctx-1",
1299 "pageSize": 10
1300 }
1301 })
1302 .to_string();
1303 ws.send(WsMessage::Text(req.into())).await.unwrap();
1304
1305 let text = read_text(&mut ws).await;
1306 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1307 assert_eq!(v["id"], "ltf-1");
1308 assert!(v.get("result").is_some(), "expected result: {text}");
1309 }
1310
1311 use tokio_tungstenite::tungstenite::client::IntoClientRequest;
1314
1315 async fn ws_call(
1317 ws: &mut tokio_tungstenite::WebSocketStream<
1318 tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
1319 >,
1320 req: serde_json::Value,
1321 ) -> serde_json::Value {
1322 ws.send(WsMessage::Text(req.to_string().into()))
1323 .await
1324 .expect("send");
1325 let text = read_text(ws).await;
1326 serde_json::from_str(&text).expect("response should be JSON")
1327 }
1328
1329 #[tokio::test]
1334 async fn ws_legacy_method_names_rejected() {
1335 let addr = spawn_ws_server().await;
1336 let mut ws = ws_connect(addr).await;
1337
1338 for legacy in ["message/send", "tasks/list", "tasks/get"] {
1339 let v = ws_call(
1340 &mut ws,
1341 serde_json::json!({
1342 "jsonrpc": "2.0",
1343 "method": legacy,
1344 "id": format!("legacy-{legacy}"),
1345 "params": {}
1346 }),
1347 )
1348 .await;
1349 assert_eq!(
1350 v["error"]["code"].as_i64(),
1351 Some(-32601),
1352 "v0.3-style name {legacy} must be MethodNotFound: {v}"
1353 );
1354 }
1355 }
1356
1357 #[tokio::test]
1360 #[allow(clippy::too_many_lines)]
1361 async fn ws_push_config_methods_routed() {
1362 use crate::push::PushSender;
1363 use a2a_protocol_types::push::TaskPushNotificationConfig;
1364
1365 struct NoopSender;
1366 impl PushSender for NoopSender {
1367 fn send<'a>(
1368 &'a self,
1369 _url: &'a str,
1370 _event: &'a StreamResponse,
1371 _config: &'a TaskPushNotificationConfig,
1372 ) -> std::pin::Pin<
1373 Box<
1374 dyn std::future::Future<Output = a2a_protocol_types::error::A2aResult<()>>
1375 + Send
1376 + 'a,
1377 >,
1378 > {
1379 Box::pin(async { Ok(()) })
1380 }
1381 fn allows_private_urls(&self) -> bool {
1382 true
1383 }
1384 }
1385
1386 let handler = Arc::new(
1387 RequestHandlerBuilder::new(EchoExec)
1388 .with_push_sender(NoopSender)
1389 .build()
1390 .unwrap(),
1391 );
1392 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1393 let addr = dispatcher
1394 .serve_with_addr("127.0.0.1:0")
1395 .await
1396 .expect("bind");
1397 let mut ws = ws_connect(addr).await;
1398
1399 let v = ws_call(
1401 &mut ws,
1402 serde_json::from_str::<serde_json::Value>(&send_message_json("pc-0")).unwrap(),
1403 )
1404 .await;
1405 let task_id = v["result"]["task"]["id"]
1406 .as_str()
1407 .expect("task id in send result")
1408 .to_owned();
1409
1410 let v = ws_call(
1412 &mut ws,
1413 serde_json::json!({
1414 "jsonrpc": "2.0",
1415 "method": "CreateTaskPushNotificationConfig",
1416 "id": "pc-1",
1417 "params": {
1418 "taskId": task_id,
1419 "url": "https://example.com/hook"
1420 }
1421 }),
1422 )
1423 .await;
1424 assert!(v.get("result").is_some(), "set push config failed: {v}");
1425 let config_id = v["result"]["id"]
1426 .as_str()
1427 .expect("server-assigned config id")
1428 .to_owned();
1429
1430 let v = ws_call(
1432 &mut ws,
1433 serde_json::json!({
1434 "jsonrpc": "2.0",
1435 "method": "GetTaskPushNotificationConfig",
1436 "id": "pc-2",
1437 "params": {"taskId": task_id, "id": config_id}
1438 }),
1439 )
1440 .await;
1441 assert!(v.get("result").is_some(), "get push config failed: {v}");
1442
1443 let v = ws_call(
1445 &mut ws,
1446 serde_json::json!({
1447 "jsonrpc": "2.0",
1448 "method": "ListTaskPushNotificationConfigs",
1449 "id": "pc-3",
1450 "params": {"taskId": task_id}
1451 }),
1452 )
1453 .await;
1454 assert!(v.get("result").is_some(), "list push configs failed: {v}");
1455 assert!(
1456 v["result"]["configs"].is_array(),
1457 "expected configs array: {v}"
1458 );
1459
1460 let v = ws_call(
1462 &mut ws,
1463 serde_json::json!({
1464 "jsonrpc": "2.0",
1465 "method": "DeleteTaskPushNotificationConfig",
1466 "id": "pc-4",
1467 "params": {"taskId": task_id, "id": config_id}
1468 }),
1469 )
1470 .await;
1471 assert!(v.get("result").is_some(), "delete push config failed: {v}");
1472 }
1473
1474 #[tokio::test]
1477 async fn ws_get_extended_agent_card_routed() {
1478 let addr = spawn_ws_server().await;
1479 let mut ws = ws_connect(addr).await;
1480
1481 let v = ws_call(
1482 &mut ws,
1483 serde_json::json!({
1484 "jsonrpc": "2.0",
1485 "method": "GetExtendedAgentCard",
1486 "id": "card-1",
1487 "params": {}
1488 }),
1489 )
1490 .await;
1491 let err = v.get("error").expect("expected an error response");
1494 assert_ne!(
1495 err["code"], -32601,
1496 "GetExtendedAgentCard must be routed, got: {v}"
1497 );
1498 }
1499
1500 #[tokio::test]
1504 async fn ws_upgrade_headers_drive_tenant_resolution() {
1505 use crate::tenant_resolver::HeaderTenantResolver;
1506
1507 let handler = Arc::new(
1508 RequestHandlerBuilder::new(EchoExec)
1509 .with_tenant_resolver(HeaderTenantResolver::default())
1510 .require_resolved_tenant()
1511 .build()
1512 .unwrap(),
1513 );
1514 let dispatcher = Arc::new(WebSocketDispatcher::new(handler));
1515 let addr = dispatcher
1516 .serve_with_addr("127.0.0.1:0")
1517 .await
1518 .expect("bind");
1519
1520 let mut ws = ws_connect(addr).await;
1522 let v = ws_call(
1523 &mut ws,
1524 serde_json::json!({
1525 "jsonrpc": "2.0",
1526 "method": "ListTasks",
1527 "id": "t-1",
1528 "params": {}
1529 }),
1530 )
1531 .await;
1532 let err = v.get("error").expect("headerless request must be rejected");
1533 let msg = err["message"].as_str().unwrap_or("");
1534 assert!(
1535 msg.contains("tenant"),
1536 "expected strict-tenancy rejection, got: {v}"
1537 );
1538
1539 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1541 req.headers_mut()
1542 .insert("a2a-version", "1.0".parse().unwrap());
1543 req.headers_mut()
1544 .insert("x-tenant-id", "acme".parse().unwrap());
1545 let (mut ws, _) = tokio_tungstenite::connect_async(req)
1546 .await
1547 .expect("connect");
1548 let v = ws_call(
1549 &mut ws,
1550 serde_json::json!({
1551 "jsonrpc": "2.0",
1552 "method": "ListTasks",
1553 "id": "t-2",
1554 "params": {}
1555 }),
1556 )
1557 .await;
1558 assert!(
1559 v.get("result").is_some(),
1560 "tenant header on the upgrade request must reach the resolver: {v}"
1561 );
1562 }
1563
1564 #[tokio::test]
1566 async fn ws_version_mismatch_rejects_handshake() {
1567 let addr = spawn_ws_server().await;
1568
1569 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1570 req.headers_mut()
1571 .insert("a2a-version", "2.0".parse().unwrap());
1572 let outcome = tokio_tungstenite::connect_async(req).await;
1573 assert!(
1574 outcome.is_err(),
1575 "handshake with A2A-Version 2.0 must be rejected"
1576 );
1577
1578 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1580 req.headers_mut()
1581 .insert("a2a-version", "1.0".parse().unwrap());
1582 assert!(
1583 tokio_tungstenite::connect_async(req).await.is_ok(),
1584 "handshake with A2A-Version 1.0 must succeed"
1585 );
1586 }
1587
1588 #[tokio::test]
1599 async fn ws_missing_version_header_rejected_by_default() {
1600 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1601
1602 let addr = spawn_ws_server().await;
1603 let req = format!("ws://{addr}").into_client_request().unwrap();
1605 assert!(
1606 tokio_tungstenite::connect_async(req).await.is_err(),
1607 "a handshake with no A2A-Version header must be rejected by default \
1608 (§3.6.2 reads it as 0.3)"
1609 );
1610 }
1611
1612 #[tokio::test]
1613 async fn ws_missing_version_header_accepted_with_optout() {
1614 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1615
1616 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1617 let dispatcher =
1618 Arc::new(WebSocketDispatcher::new(handler).accept_missing_version_header());
1619 let addr = dispatcher
1620 .serve_with_addr("127.0.0.1:0")
1621 .await
1622 .expect("bind to port 0");
1623
1624 let req = format!("ws://{addr}").into_client_request().unwrap();
1625 assert!(
1626 tokio_tungstenite::connect_async(req).await.is_ok(),
1627 "accept_missing_version_header() must restore the tolerant behaviour"
1628 );
1629 }
1630
1631 #[tokio::test]
1640 async fn ws_version_rejection_body_carries_error_details() {
1641 use tokio_tungstenite::tungstenite::client::IntoClientRequest as _;
1642 use tokio_tungstenite::tungstenite::Error as WsError;
1643
1644 let addr = spawn_ws_server().await;
1645 let mut req = format!("ws://{addr}").into_client_request().unwrap();
1646 req.headers_mut()
1647 .insert("a2a-version", "2.0".parse().unwrap());
1648
1649 match tokio_tungstenite::connect_async(req).await {
1650 Err(WsError::Http(resp)) => {
1651 assert_eq!(resp.status(), 400, "version rejection is HTTP 400");
1652 let body = resp.body().as_ref().expect("rejection carries a body");
1653 let text = String::from_utf8_lossy(body);
1654 let json: serde_json::Value =
1655 serde_json::from_str(&text).expect("rejection body is JSON");
1656 assert!(
1657 !json["error"]["details"].is_null(),
1658 "the AIP-193 details block must be present, got: {text}"
1659 );
1660 }
1661 other => panic!("expected an HTTP 400 rejection, got: {other:?}"),
1662 }
1663 }
1664
1665 #[tokio::test]
1679 async fn ws_lagged_stream_reports_server_error_code() {
1680 struct FloodExec;
1681 agent_executor!(FloodExec, |ctx, queue| async {
1682 for _ in 0..512 {
1683 queue
1684 .write(StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
1685 task_id: ctx.task_id.clone(),
1686 context_id: ContextId::new(ctx.context_id.clone()),
1687 status: TaskStatus::new(TaskState::Working),
1688 metadata: None,
1689 }))
1690 .await?;
1691 }
1692 Ok(())
1693 });
1694
1695 let handler = Arc::new(
1696 RequestHandlerBuilder::new(FloodExec)
1697 .with_event_queue_capacity(1)
1698 .build()
1699 .unwrap(),
1700 );
1701 let addr = Arc::new(WebSocketDispatcher::new(handler))
1702 .serve_with_addr("127.0.0.1:0")
1703 .await
1704 .expect("bind to port 0");
1705 let mut ws = ws_connect(addr).await;
1706
1707 let req = serde_json::json!({
1708 "jsonrpc": "2.0",
1709 "method": "SendStreamingMessage",
1710 "id": "lag-1",
1711 "params": {
1712 "message": {
1713 "messageId": "msg-lag-1",
1714 "role": "ROLE_USER",
1715 "parts": [{"text": "flood"}]
1716 }
1717 }
1718 })
1719 .to_string();
1720 ws.send(WsMessage::Text(req.into())).await.unwrap();
1721
1722 let found = tokio::time::timeout(std::time::Duration::from_secs(10), async {
1723 while let Some(Ok(msg)) = ws.next().await {
1724 let Ok(text) = msg.into_text() else { continue };
1725 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
1726 continue;
1727 };
1728 if let Some(code) = v["error"]["code"].as_i64() {
1729 return Some(code);
1730 }
1731 }
1732 None
1733 })
1734 .await
1735 .expect("the lagged stream must produce an error frame within 10s");
1736
1737 assert_eq!(
1738 found,
1739 Some(-32000),
1740 "a lagged stream must be reported with the JSON-RPC server-error \
1741 code -32000, not a positive code"
1742 );
1743 }
1744
1745 #[tokio::test]
1756 async fn ws_over_concurrency_limit_is_rejected_with_server_error_code() {
1757 struct BlockingExec;
1758 agent_executor!(BlockingExec, |_ctx, _queue| async {
1759 std::future::pending::<()>().await;
1761 Ok(())
1762 });
1763
1764 let handler = Arc::new(RequestHandlerBuilder::new(BlockingExec).build().unwrap());
1765 let addr = Arc::new(WebSocketDispatcher::new(handler))
1766 .serve_with_addr("127.0.0.1:0")
1767 .await
1768 .expect("bind to port 0");
1769 let mut ws = ws_connect(addr).await;
1770
1771 for i in 0..65 {
1773 let req = serde_json::json!({
1774 "jsonrpc": "2.0",
1775 "method": "SendMessage",
1776 "id": format!("busy-{i}"),
1777 "params": {
1778 "message": {
1779 "messageId": format!("msg-busy-{i}"),
1780 "role": "ROLE_USER",
1781 "parts": [{"text": "block"}]
1782 }
1783 }
1784 })
1785 .to_string();
1786 ws.send(WsMessage::Text(req.into())).await.unwrap();
1787 }
1788
1789 let code = tokio::time::timeout(std::time::Duration::from_secs(10), async {
1791 while let Some(Ok(msg)) = ws.next().await {
1792 let Ok(text) = msg.into_text() else { continue };
1793 let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) else {
1794 continue;
1795 };
1796 if let Some(c) = v["error"]["code"].as_i64() {
1797 assert!(
1798 v["error"]["message"]
1799 .as_str()
1800 .is_some_and(|m| m.contains("server busy")),
1801 "expected the back-pressure rejection, got: {v}"
1802 );
1803 return Some(c);
1804 }
1805 }
1806 None
1807 })
1808 .await
1809 .expect("the over-limit request must be answered within 10s");
1810
1811 assert_eq!(
1812 code,
1813 Some(-32000),
1814 "back-pressure must be reported with the JSON-RPC server-error \
1815 code -32000, not a positive code"
1816 );
1817 }
1818
1819 #[tokio::test]
1822 async fn ws_handshake_timeout_disconnects_stalled_peer() {
1823 let handler = Arc::new(RequestHandlerBuilder::new(EchoExec).build().unwrap());
1824 let dispatcher = Arc::new(
1825 WebSocketDispatcher::new(handler)
1826 .with_handshake_timeout(std::time::Duration::from_millis(200)),
1827 );
1828 let addr = dispatcher
1829 .serve_with_addr("127.0.0.1:0")
1830 .await
1831 .expect("bind");
1832
1833 let mut stream = tokio::net::TcpStream::connect(addr).await.expect("tcp");
1835 let mut buf = [0u8; 16];
1836 let read = tokio::time::timeout(
1839 std::time::Duration::from_secs(5),
1840 tokio::io::AsyncReadExt::read(&mut stream, &mut buf),
1841 )
1842 .await
1843 .expect("server should close the stalled connection");
1844 assert!(
1845 matches!(read, Ok(0) | Err(_)),
1846 "expected EOF/reset from server, got: {read:?}"
1847 );
1848 }
1849
1850 #[tokio::test]
1852 async fn ws_binary_frame_gets_error_response() {
1853 let addr = spawn_ws_server().await;
1854 let mut ws = ws_connect(addr).await;
1855
1856 ws.send(WsMessage::Binary(vec![1, 2, 3].into()))
1857 .await
1858 .unwrap();
1859
1860 let text = read_text(&mut ws).await;
1861 let v: serde_json::Value = serde_json::from_str(&text).unwrap();
1862 assert_eq!(v["error"]["code"], -32700, "expected parse-error code: {v}");
1863 assert!(
1864 v["error"]["message"]
1865 .as_str()
1866 .unwrap_or("")
1867 .contains("binary"),
1868 "error should explain binary frames are unsupported: {v}"
1869 );
1870 }
1871}