1use std::collections::HashMap;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::Arc;
22use std::time::Duration;
23
24use http_body_util::Full;
25use hyper::body::Bytes;
26use hyper::header;
27#[cfg(not(feature = "tls-rustls"))]
28use hyper_util::client::legacy::connect::HttpConnector;
29#[cfg(not(feature = "tls-rustls"))]
30use hyper_util::client::legacy::Client;
31#[cfg(not(feature = "tls-rustls"))]
32use hyper_util::rt::TokioExecutor;
33use tokio::sync::mpsc;
34use uuid::Uuid;
35
36use a2a_protocol_types::{JsonRpcRequest, JsonRpcResponse};
37
38use crate::error::{ClientError, ClientResult};
39use crate::streaming::EventStream;
40use crate::transport::Transport;
41
42#[cfg(not(feature = "tls-rustls"))]
45type HttpClient = Client<HttpConnector, Full<Bytes>>;
46
47#[cfg(feature = "tls-rustls")]
48type HttpClient = crate::tls::HttpsClient;
49
50#[derive(Clone, Debug)]
57pub struct JsonRpcTransport {
58 inner: Arc<Inner>,
59}
60
61#[derive(Debug, Clone)]
62struct Inner {
63 client: HttpClient,
64 endpoint: String,
65 request_timeout: Duration,
66 stream_connect_timeout: Duration,
67 max_response_size: usize,
68}
69
70impl JsonRpcTransport {
71 pub fn new(endpoint: impl Into<String>) -> ClientResult<Self> {
79 Self::with_timeout(endpoint, Duration::from_secs(30))
80 }
81
82 pub fn with_timeout(
88 endpoint: impl Into<String>,
89 request_timeout: Duration,
90 ) -> ClientResult<Self> {
91 Self::with_timeouts(endpoint, request_timeout, request_timeout)
92 }
93
94 pub fn with_timeouts(
102 endpoint: impl Into<String>,
103 request_timeout: Duration,
104 stream_connect_timeout: Duration,
105 ) -> ClientResult<Self> {
106 Self::with_all_timeouts(
107 endpoint,
108 request_timeout,
109 stream_connect_timeout,
110 Duration::from_secs(10),
111 )
112 }
113
114 pub fn with_all_timeouts(
123 endpoint: impl Into<String>,
124 request_timeout: Duration,
125 stream_connect_timeout: Duration,
126 connection_timeout: Duration,
127 ) -> ClientResult<Self> {
128 let endpoint = endpoint.into();
129 validate_url(&endpoint)?;
130
131 #[cfg(not(feature = "tls-rustls"))]
132 let client = {
133 let mut connector = HttpConnector::new();
134 connector.set_connect_timeout(Some(connection_timeout));
135 connector.set_nodelay(true);
136 Client::builder(TokioExecutor::new())
137 .pool_idle_timeout(Duration::from_secs(90))
138 .build(connector)
139 };
140
141 #[cfg(feature = "tls-rustls")]
142 let client = crate::tls::build_https_client_with_connect_timeout(
143 crate::tls::default_tls_config(),
144 connection_timeout,
145 );
146
147 Ok(Self {
148 inner: Arc::new(Inner {
149 client,
150 endpoint,
151 request_timeout,
152 stream_connect_timeout,
153 max_response_size: super::DEFAULT_MAX_RESPONSE_SIZE,
154 }),
155 })
156 }
157
158 #[must_use]
164 pub fn with_max_response_size(mut self, max_bytes: usize) -> Self {
165 Arc::make_mut(&mut self.inner).max_response_size = max_bytes;
166 self
167 }
168
169 #[must_use]
171 pub fn endpoint(&self) -> &str {
172 &self.inner.endpoint
173 }
174
175 fn build_request(
178 &self,
179 method: &str,
180 params: serde_json::Value,
181 extra_headers: &HashMap<String, String>,
182 accept_sse: bool,
183 ) -> ClientResult<(serde_json::Value, hyper::Request<Full<Bytes>>)> {
184 let id = serde_json::Value::String(Uuid::new_v4().to_string());
185 let rpc_req = JsonRpcRequest::with_params(id.clone(), method, params);
186 let body_bytes = serde_json::to_vec(&rpc_req).map_err(ClientError::Serialization)?;
187
188 let accept = if accept_sse {
189 "text/event-stream"
190 } else {
191 "application/json"
192 };
193
194 let mut builder = hyper::Request::builder()
195 .method(hyper::Method::POST)
196 .uri(&self.inner.endpoint)
197 .header(header::CONTENT_TYPE, a2a_protocol_types::JSON_CONTENT_TYPE)
198 .header(
199 a2a_protocol_types::A2A_VERSION_HEADER,
200 a2a_protocol_types::A2A_VERSION,
201 )
202 .header(header::ACCEPT, accept);
203
204 for (k, v) in extra_headers {
205 builder = builder.header(k.as_str(), v.as_str());
206 }
207
208 let req = builder
209 .body(Full::new(Bytes::from(body_bytes)))
210 .map_err(|e| ClientError::Transport(e.to_string()))?;
211 Ok((id, req))
212 }
213
214 #[allow(clippy::too_many_lines)]
215 async fn execute_request(
216 &self,
217 method: &str,
218 params: serde_json::Value,
219 extra_headers: &HashMap<String, String>,
220 ) -> ClientResult<serde_json::Value> {
221 trace_info!(method, endpoint = %self.inner.endpoint, "sending JSON-RPC request");
222
223 let (request_id, req) = self.build_request(method, params, extra_headers, false)?;
224
225 let deadline = tokio::time::Instant::now() + self.inner.request_timeout;
230
231 let resp = tokio::time::timeout_at(deadline, self.inner.client.request(req))
232 .await
233 .map_err(|_| {
234 trace_error!(method, "request timed out");
235 ClientError::Timeout("request timed out".into())
236 })?
237 .map_err(|e| {
238 trace_error!(method, error = %e, "HTTP client error");
239 ClientError::HttpClient(e.to_string())
240 })?;
241
242 let status = resp.status();
243 trace_debug!(method, %status, "received response");
244
245 let retry_after = crate::error::parse_retry_after(resp.headers());
248
249 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
250 let body_bytes =
251 match super::collect_response_limited(resp, self.inner.max_response_size, remaining)
252 .await
253 {
254 Ok(bytes) => bytes,
255 Err(e) => {
256 trace_error!(method, error = %e, "response body read failed");
257 return Err(e);
258 }
259 };
260
261 if !status.is_success() {
262 let body_str = String::from_utf8_lossy(&body_bytes);
263 trace_warn!(method, %status, "unexpected HTTP status");
264 return Err(ClientError::UnexpectedStatus {
265 status: status.as_u16(),
266 body: super::truncate_body(&body_str),
267 retry_after,
268 });
269 }
270
271 let envelope: JsonRpcResponse<serde_json::Value> = serde_json::from_slice(&body_bytes)
272 .map_err(|e| {
273 let preview = String::from_utf8_lossy(&body_bytes[..body_bytes.len().min(200)]);
276 if preview.contains("jsonrpc") {
277 ClientError::Serialization(e)
278 } else {
279 ClientError::ProtocolBindingMismatch(format!(
280 "response is not JSON-RPC ({e}); the server may use REST transport",
281 ))
282 }
283 })?;
284
285 match envelope {
286 JsonRpcResponse::Success(ok) => {
287 if !response_id_matches(ok.id.as_ref(), &request_id) {
289 trace_warn!(
290 method,
291 "JSON-RPC response id mismatch (expected {request_id}, got {:?})",
292 ok.id
293 );
294 return Err(ClientError::Transport(
295 "JSON-RPC response id does not match request id".into(),
296 ));
297 }
298 trace_info!(method, "request succeeded");
299 Ok(ok.result)
300 }
301 JsonRpcResponse::Error(err) => {
302 if !response_id_matches(err.id.as_ref(), &request_id) {
304 trace_warn!(
305 method,
306 "JSON-RPC error response id mismatch (expected {request_id}, got {:?})",
307 err.id
308 );
309 }
310 trace_warn!(method, code = err.error.code, "JSON-RPC error response");
311 let a2a =
312 super::map_jsonrpc_error(err.error.code, err.error.message, err.error.data);
313 Err(ClientError::Protocol(a2a))
314 }
315 }
316 }
317
318 async fn execute_streaming_request(
319 &self,
320 method: &str,
321 params: serde_json::Value,
322 extra_headers: &HashMap<String, String>,
323 ) -> ClientResult<EventStream> {
324 trace_info!(method, endpoint = %self.inner.endpoint, "opening SSE stream");
325
326 let (_request_id, req) = self.build_request(method, params, extra_headers, true)?;
327
328 let resp = tokio::time::timeout(
329 self.inner.stream_connect_timeout,
330 self.inner.client.request(req),
331 )
332 .await
333 .map_err(|_| {
334 trace_error!(method, "stream connect timed out");
335 ClientError::Timeout("stream connect timed out".into())
336 })?
337 .map_err(|e| {
338 trace_error!(method, error = %e, "HTTP client error");
339 ClientError::HttpClient(e.to_string())
340 })?;
341
342 let status = resp.status();
343 if !status.is_success() {
344 let retry_after = crate::error::parse_retry_after(resp.headers());
348 let body_bytes = super::collect_response_limited(
349 resp,
350 self.inner.max_response_size,
351 self.inner.stream_connect_timeout,
352 )
353 .await?;
354 let body_str = String::from_utf8_lossy(&body_bytes);
355 return Err(ClientError::UnexpectedStatus {
356 status: status.as_u16(),
357 body: super::truncate_body(&body_str),
358 retry_after,
359 });
360 }
361
362 let content_type = resp
367 .headers()
368 .get(hyper::header::CONTENT_TYPE)
369 .and_then(|v| v.to_str().ok())
370 .unwrap_or("")
371 .to_ascii_lowercase();
372 if !content_type.starts_with("text/event-stream") {
373 let body_bytes = super::collect_response_limited(
374 resp,
375 self.inner.max_response_size,
376 self.inner.stream_connect_timeout,
377 )
378 .await?;
379 return Err(non_sse_stream_response_error(&content_type, &body_bytes));
380 }
381
382 let actual_status = status.as_u16();
383 let (tx, rx) = mpsc::channel::<crate::streaming::event_stream::BodyChunk>(64);
384 let body = resp.into_body();
385
386 let task_handle = tokio::spawn(async move {
388 body_reader_task(body, tx).await;
389 });
390
391 Ok(
397 EventStream::with_status(rx, task_handle.abort_handle(), actual_status)
398 .with_first_event_timeout(self.inner.stream_connect_timeout),
399 )
400 }
401}
402
403impl Transport for JsonRpcTransport {
404 fn send_request<'a>(
405 &'a self,
406 method: &'a str,
407 params: serde_json::Value,
408 extra_headers: &'a HashMap<String, String>,
409 ) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>> {
410 Box::pin(self.execute_request(method, params, extra_headers))
411 }
412
413 fn send_streaming_request<'a>(
414 &'a self,
415 method: &'a str,
416 params: serde_json::Value,
417 extra_headers: &'a HashMap<String, String>,
418 ) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>> {
419 Box::pin(self.execute_streaming_request(method, params, extra_headers))
420 }
421}
422
423async fn body_reader_task(
430 mut body: hyper::body::Incoming,
431 tx: mpsc::Sender<crate::streaming::event_stream::BodyChunk>,
432) {
433 use http_body_util::BodyExt;
434
435 tokio::task::yield_now().await;
442
443 loop {
444 match body.frame().await {
445 None => break, Some(Err(e)) => {
447 let _ = tx.send(Err(ClientError::Http(e))).await;
448 break;
449 }
450 Some(Ok(frame)) => {
451 if let Ok(data) = frame.into_data() {
452 if tx.send(Ok(data)).await.is_err() {
453 break;
455 }
456 }
457 }
459 }
460 }
461}
462
463fn response_id_matches(
471 response_id: Option<&serde_json::Value>,
472 request_id: &serde_json::Value,
473) -> bool {
474 response_id == Some(request_id)
475}
476
477pub(crate) fn non_sse_stream_response_error(content_type: &str, body_bytes: &[u8]) -> ClientError {
482 if let Ok(JsonRpcResponse::Error(err)) =
483 serde_json::from_slice::<JsonRpcResponse<serde_json::Value>>(body_bytes)
484 {
485 trace_warn!(
486 code = err.error.code,
487 "JSON-RPC error response to streaming request"
488 );
489 let a2a = super::map_jsonrpc_error(err.error.code, err.error.message, err.error.data);
490 return ClientError::Protocol(a2a);
491 }
492 let body_str = String::from_utf8_lossy(body_bytes);
493 ClientError::Transport(format!(
494 "expected text/event-stream response, got '{content_type}': {}",
495 super::truncate_body(&body_str)
496 ))
497}
498
499fn validate_url(url: &str) -> ClientResult<()> {
500 if url.is_empty() {
501 return Err(ClientError::InvalidEndpoint("URL must not be empty".into()));
502 }
503 if !url.starts_with("http://") && !url.starts_with("https://") {
504 return Err(ClientError::InvalidEndpoint(format!(
505 "URL must start with http:// or https://: {url}"
506 )));
507 }
508 #[cfg(not(feature = "tls-rustls"))]
513 if url.starts_with("https://") {
514 return Err(ClientError::InvalidEndpoint(format!(
515 "https:// requires the `tls-rustls` feature (enabled by default); \
516 this build has it disabled: {url}"
517 )));
518 }
519 Ok(())
520}
521
522#[cfg(test)]
525mod tests {
526 use http_body_util::BodyExt;
527
528 use super::*;
529
530 #[test]
531 fn validate_url_rejects_empty() {
532 assert!(validate_url("").is_err());
533 }
534
535 #[test]
538 fn response_id_matches_equal_strings() {
539 let rid = serde_json::Value::String("abc".into());
540 assert!(response_id_matches(Some(&rid), &rid));
541 }
542
543 #[test]
544 fn response_id_matches_different_strings() {
545 let rid = serde_json::Value::String("abc".into());
546 let other = serde_json::Value::String("xyz".into());
547 assert!(!response_id_matches(Some(&other), &rid));
548 }
549
550 #[test]
551 fn response_id_matches_none() {
552 let rid = serde_json::Value::String("abc".into());
553 assert!(!response_id_matches(None, &rid));
554 }
555
556 #[test]
557 fn response_id_matches_numeric() {
558 let a = serde_json::json!(42);
559 let b = serde_json::json!(41);
560 let eq = serde_json::json!(42);
561 assert!(response_id_matches(Some(&a), &eq));
562 assert!(!response_id_matches(Some(&b), &eq));
563 }
564
565 #[test]
566 fn response_id_matches_type_mismatch() {
567 let s = serde_json::json!("1");
569 let n = serde_json::json!(1);
570 assert!(!response_id_matches(Some(&s), &n));
571 }
572
573 #[test]
574 fn validate_url_rejects_non_http() {
575 assert!(validate_url("ftp://example.com").is_err());
576 }
577
578 #[test]
579 fn validate_url_accepts_http() {
580 assert!(validate_url("http://localhost:8080").is_ok());
581 }
582
583 #[cfg(feature = "tls-rustls")]
584 #[test]
585 fn validate_url_accepts_https() {
586 assert!(validate_url("https://agent.example.com/a2a").is_ok());
587 }
588
589 #[cfg(not(feature = "tls-rustls"))]
590 #[test]
591 fn validate_url_rejects_https_without_tls_feature() {
592 let err = validate_url("https://agent.example.com/a2a").unwrap_err();
593 assert!(
594 err.to_string().contains("tls-rustls"),
595 "error should point at the tls-rustls feature, got: {err}"
596 );
597 }
598
599 #[test]
600 fn transport_new_rejects_bad_url() {
601 assert!(JsonRpcTransport::new("not-a-url").is_err());
602 }
603
604 #[test]
605 fn transport_new_stores_endpoint() {
606 let t = JsonRpcTransport::new("http://localhost:9090").unwrap();
607 assert_eq!(t.endpoint(), "http://localhost:9090");
608 }
609
610 async fn start_server(status: u16, body: impl Into<String>) -> std::net::SocketAddr {
615 let body: String = body.into();
616 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
617 let addr = listener.local_addr().unwrap();
618
619 tokio::spawn(async move {
620 loop {
621 let (stream, _) = listener.accept().await.unwrap();
622 let io = hyper_util::rt::TokioIo::new(stream);
623 let body = body.clone();
624 tokio::spawn(async move {
625 let service = hyper::service::service_fn(
626 move |req: hyper::Request<hyper::body::Incoming>| {
627 let body_template = body.clone();
628 async move {
629 let req_bytes = req
631 .into_body()
632 .collect()
633 .await
634 .map(http_body_util::Collected::to_bytes)
635 .unwrap_or_default();
636 let response_body = if let Ok(req_json) =
637 serde_json::from_slice::<serde_json::Value>(&req_bytes)
638 {
639 if let Some(id) = req_json.get("id") {
640 body_template.replace("\"__ID__\"", &id.to_string())
641 } else {
642 body_template
643 }
644 } else {
645 body_template
646 };
647 Ok::<_, hyper::Error>(
648 hyper::Response::builder()
649 .status(status)
650 .header("content-type", "application/json")
651 .body(Full::new(Bytes::from(response_body)))
652 .unwrap(),
653 )
654 }
655 },
656 );
657 let _ = hyper_util::server::conn::auto::Builder::new(
658 hyper_util::rt::TokioExecutor::new(),
659 )
660 .serve_connection(io, service)
661 .await;
662 });
663 }
664 });
665
666 addr
667 }
668
669 #[tokio::test]
670 async fn execute_request_non_success_status_returns_error() {
671 let addr = start_server(404, "Not Found").await;
672 let url = format!("http://127.0.0.1:{}", addr.port());
673 let transport = JsonRpcTransport::new(&url).unwrap();
674 let result = transport
675 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
676 .await;
677 match result {
678 Err(ClientError::UnexpectedStatus { status, .. }) => {
679 assert_eq!(status, 404);
680 }
681 other => panic!("expected UnexpectedStatus, got {other:?}"),
682 }
683 }
684
685 #[tokio::test]
686 async fn execute_request_success_parses_jsonrpc() {
687 let response_body = r#"{"jsonrpc":"2.0","id":"__ID__","result":{"hello":"world"}}"#;
688 let addr = start_server(200, response_body).await;
689 let url = format!("http://127.0.0.1:{}", addr.port());
690 let transport = JsonRpcTransport::new(&url).unwrap();
691 let result = transport
692 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
693 .await;
694 let value = result.unwrap();
695 assert_eq!(value["hello"], "world");
696 }
697
698 #[tokio::test]
699 async fn execute_streaming_request_jsonrpc_error_envelope_returns_protocol_error() {
700 let addr = start_server(
704 200,
705 r#"{"jsonrpc":"2.0","id":"__ID__","error":{"code":-32602,"message":"task_id exists but belongs to a different context"}}"#,
706 )
707 .await;
708 let url = format!("http://127.0.0.1:{}", addr.port());
709 let transport = JsonRpcTransport::new(&url).unwrap();
710 let result = transport
711 .execute_streaming_request(
712 "SendStreamingMessage",
713 serde_json::json!({}),
714 &HashMap::new(),
715 )
716 .await;
717 match result {
718 Err(ClientError::Protocol(e)) => {
719 assert_eq!(
720 e.code,
721 a2a_protocol_types::ErrorCode::InvalidParams,
722 "JSON-RPC code -32602 should map to InvalidParams"
723 );
724 assert!(
725 e.message.contains("different context"),
726 "error message should be preserved, got: {}",
727 e.message
728 );
729 }
730 other => panic!("expected ClientError::Protocol, got {other:?}"),
731 }
732 }
733
734 #[tokio::test]
735 async fn execute_streaming_request_non_sse_body_returns_transport_error() {
736 let addr = start_server(200, "not json at all").await;
739 let url = format!("http://127.0.0.1:{}", addr.port());
740 let transport = JsonRpcTransport::new(&url).unwrap();
741 let result = transport
742 .execute_streaming_request(
743 "SendStreamingMessage",
744 serde_json::json!({}),
745 &HashMap::new(),
746 )
747 .await;
748 match result {
749 Err(ClientError::Transport(msg)) => {
750 assert!(
751 msg.contains("text/event-stream"),
752 "error should name the expected content type, got: {msg}"
753 );
754 }
755 other => panic!("expected ClientError::Transport, got {other:?}"),
756 }
757 }
758
759 #[tokio::test]
760 async fn execute_streaming_request_non_success_returns_error() {
761 let addr = start_server(500, "Internal Server Error").await;
762 let url = format!("http://127.0.0.1:{}", addr.port());
763 let transport = JsonRpcTransport::new(&url).unwrap();
764 let result = transport
765 .execute_streaming_request(
766 "SendStreamingMessage",
767 serde_json::json!({}),
768 &HashMap::new(),
769 )
770 .await;
771 match result {
772 Err(ClientError::UnexpectedStatus { status, .. }) => {
773 assert_eq!(status, 500);
774 }
775 other => panic!("expected UnexpectedStatus, got {other:?}"),
776 }
777 }
778
779 #[tokio::test]
781 async fn execute_request_jsonrpc_error_returns_protocol_error() {
782 let response_body = r#"{"jsonrpc":"2.0","id":"__ID__","error":{"code":-32603,"message":"internal failure"}}"#;
783 let addr = start_server(200, response_body).await;
784 let url = format!("http://127.0.0.1:{}", addr.port());
785 let transport = JsonRpcTransport::new(&url).unwrap();
786 let result = transport
787 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
788 .await;
789 match result {
790 Err(ClientError::Protocol(a2a_err)) => {
791 assert!(
792 a2a_err.message.contains("internal failure"),
793 "got: {}",
794 a2a_err.message
795 );
796 }
797 other => panic!("expected Protocol error, got {other:?}"),
798 }
799 }
800
801 #[tokio::test]
803 async fn execute_request_non_jsonrpc_returns_binding_mismatch() {
804 let response_body = r#"{"status":"ok","data":42}"#;
806 let addr = start_server(200, response_body).await;
807 let url = format!("http://127.0.0.1:{}", addr.port());
808 let transport = JsonRpcTransport::new(&url).unwrap();
809 let result = transport
810 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
811 .await;
812 match result {
813 Err(ClientError::ProtocolBindingMismatch(msg)) => {
814 assert!(msg.contains("REST"), "should mention REST transport: {msg}");
815 }
816 other => panic!("expected ProtocolBindingMismatch, got {other:?}"),
817 }
818 }
819
820 #[tokio::test]
822 async fn send_streaming_request_via_trait_delegation() {
823 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
825 let addr = listener.local_addr().unwrap();
826
827 tokio::spawn(async move {
828 loop {
829 let (stream, _) = listener.accept().await.unwrap();
830 let io = hyper_util::rt::TokioIo::new(stream);
831 tokio::spawn(async move {
832 let service = hyper::service::service_fn(|_req| async {
833 let sse_body = "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"status\":\"ok\"}}\n\n";
834 Ok::<_, hyper::Error>(
835 hyper::Response::builder()
836 .status(200)
837 .header("content-type", "text/event-stream")
838 .body(Full::new(Bytes::from(sse_body)))
839 .unwrap(),
840 )
841 });
842 let _ = hyper_util::server::conn::auto::Builder::new(
843 hyper_util::rt::TokioExecutor::new(),
844 )
845 .serve_connection(io, service)
846 .await;
847 });
848 }
849 });
850
851 let url = format!("http://127.0.0.1:{}", addr.port());
852 let transport = JsonRpcTransport::new(&url).unwrap();
853 let dyn_transport: &dyn Transport = &transport;
855 let result = dyn_transport
856 .send_streaming_request(
857 "SendStreamingMessage",
858 serde_json::json!({}),
859 &HashMap::new(),
860 )
861 .await;
862 assert!(result.is_ok(), "streaming via trait delegation should work");
863 }
864
865 #[tokio::test]
867 async fn send_request_via_trait_delegation() {
868 let response_body = r#"{"jsonrpc":"2.0","id":"__ID__","result":{"hello":"world"}}"#;
869 let addr = start_server(200, response_body).await;
870 let url = format!("http://127.0.0.1:{}", addr.port());
871 let transport = JsonRpcTransport::new(&url).unwrap();
872 let dyn_transport: &dyn Transport = &transport;
874 let result = dyn_transport
875 .send_request("GetTask", serde_json::json!({}), &HashMap::new())
876 .await;
877 let value = result.unwrap();
878 assert_eq!(value["hello"], "world");
879 }
880
881 #[tokio::test]
886 async fn oversized_response_rejected_by_content_length() {
887 let big = format!(
888 r#"{{"jsonrpc":"2.0","id":"1","result":{{"pad":"{}"}}}}"#,
889 "x".repeat(64 * 1024)
890 );
891 let addr = start_server(200, big).await;
892 let url = format!("http://127.0.0.1:{}", addr.port());
893 let transport = JsonRpcTransport::new(&url)
894 .unwrap()
895 .with_max_response_size(1024);
896 let result = transport
897 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
898 .await;
899 match result {
900 Err(ClientError::Transport(msg)) => {
901 assert!(msg.contains("too large"), "got: {msg}");
902 }
903 other => panic!("expected Transport 'too large' error, got {other:?}"),
904 }
905 }
906
907 #[tokio::test]
910 async fn oversized_chunked_response_aborted_mid_read() {
911 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
912 let addr = listener.local_addr().unwrap();
913
914 tokio::spawn(async move {
915 loop {
916 let (stream, _) = listener.accept().await.unwrap();
917 let io = hyper_util::rt::TokioIo::new(stream);
918 tokio::spawn(async move {
919 let service = hyper::service::service_fn(|_req| async {
920 let chunks: Vec<
923 Result<hyper::body::Frame<Bytes>, std::convert::Infallible>,
924 > = (0..64)
925 .map(|_| {
926 Ok(hyper::body::Frame::data(Bytes::from(vec![b'x'; 64 * 1024])))
927 })
928 .collect();
929 let body =
930 http_body_util::StreamBody::new(futures_util::stream::iter(chunks));
931 Ok::<_, hyper::Error>(
932 hyper::Response::builder()
933 .status(200)
934 .header("content-type", "application/json")
935 .body(body)
936 .unwrap(),
937 )
938 });
939 let _ = hyper_util::server::conn::auto::Builder::new(
940 hyper_util::rt::TokioExecutor::new(),
941 )
942 .serve_connection(io, service)
943 .await;
944 });
945 }
946 });
947
948 let url = format!("http://127.0.0.1:{}", addr.port());
949 let transport = JsonRpcTransport::new(&url)
950 .unwrap()
951 .with_max_response_size(1024 * 1024);
952 let result = transport
953 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
954 .await;
955 match result {
956 Err(ClientError::Transport(msg)) => {
957 assert!(msg.contains("too large"), "got: {msg}");
958 }
959 other => panic!("expected Transport 'too large' error, got {other:?}"),
960 }
961 }
962
963 #[tokio::test]
966 async fn large_response_under_cap_succeeds() {
967 let big = format!(
968 r#"{{"jsonrpc":"2.0","id":"__ID__","result":{{"pad":"{}"}}}}"#,
969 "y".repeat(256 * 1024)
970 );
971 let addr = start_server(200, big).await;
972 let url = format!("http://127.0.0.1:{}", addr.port());
973 let transport = JsonRpcTransport::new(&url).unwrap();
974 let value = transport
975 .execute_request("GetTask", serde_json::json!({}), &HashMap::new())
976 .await
977 .expect("large response under the default cap must succeed");
978 assert_eq!(value["pad"].as_str().unwrap().len(), 256 * 1024);
979 }
980
981 #[tokio::test]
982 async fn execute_streaming_request_success_returns_event_stream() {
983 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
985 let addr = listener.local_addr().unwrap();
986
987 tokio::spawn(async move {
988 loop {
989 let (stream, _) = listener.accept().await.unwrap();
990 let io = hyper_util::rt::TokioIo::new(stream);
991 tokio::spawn(async move {
992 let service = hyper::service::service_fn(|_req| async {
993 let sse_body = "data: {\"jsonrpc\":\"2.0\",\"id\":\"1\",\"result\":{\"status\":\"ok\"}}\n\n";
994 Ok::<_, hyper::Error>(
995 hyper::Response::builder()
996 .status(200)
997 .header("content-type", "text/event-stream")
998 .body(Full::new(Bytes::from(sse_body)))
999 .unwrap(),
1000 )
1001 });
1002 let _ = hyper_util::server::conn::auto::Builder::new(
1003 hyper_util::rt::TokioExecutor::new(),
1004 )
1005 .serve_connection(io, service)
1006 .await;
1007 });
1008 }
1009 });
1010
1011 let url = format!("http://127.0.0.1:{}", addr.port());
1012 let transport = JsonRpcTransport::new(&url).unwrap();
1013 let mut stream = transport
1014 .execute_streaming_request(
1015 "SendStreamingMessage",
1016 serde_json::json!({}),
1017 &HashMap::new(),
1018 )
1019 .await
1020 .unwrap();
1021 let event = tokio::time::timeout(std::time::Duration::from_secs(5), stream.next())
1023 .await
1024 .expect("timed out waiting for event");
1025 assert!(
1026 event.is_some(),
1027 "expected at least one event from the stream"
1028 );
1029 }
1030}