#[cfg(feature = "grpc")]
pub mod grpc;
pub mod jsonrpc;
pub mod rest;
#[cfg(feature = "websocket")]
pub mod websocket;
#[cfg(feature = "grpc")]
pub use grpc::GrpcTransport;
pub use jsonrpc::JsonRpcTransport;
pub use rest::RestTransport;
#[cfg(feature = "websocket")]
pub use websocket::{WebSocketTransport, WebSocketTransportConfig};
const MAX_ERROR_BODY_LEN: usize = 512;
pub(crate) const DEFAULT_MAX_RESPONSE_SIZE: usize = 32 * 1024 * 1024;
pub(crate) async fn collect_response_limited(
resp: hyper::Response<hyper::body::Incoming>,
max_size: usize,
read_timeout: std::time::Duration,
) -> crate::error::ClientResult<hyper::body::Bytes> {
use http_body_util::{BodyExt, LengthLimitError, Limited};
use crate::error::ClientError;
let body = resp.into_body();
let size_hint = <hyper::body::Incoming as hyper::body::Body>::size_hint(&body);
if let Some(upper) = size_hint.upper() {
if upper > max_size as u64 {
return Err(ClientError::Transport(format!(
"response body too large: {upper} bytes exceeds {max_size} byte limit"
)));
}
}
let limited = Limited::new(body, max_size);
match tokio::time::timeout(read_timeout, limited.collect()).await {
Err(_) => Err(crate::error::ClientError::Timeout(
"response body read timed out".into(),
)),
Ok(Ok(collected)) => Ok(collected.to_bytes()),
Ok(Err(err)) => {
if err.downcast_ref::<LengthLimitError>().is_some() {
return Err(ClientError::Transport(format!(
"response body too large: exceeds {max_size} byte limit"
)));
}
match err.downcast::<hyper::Error>() {
Ok(hyper_err) => Err(ClientError::Http(*hyper_err)),
Err(other) => Err(ClientError::Transport(other.to_string())),
}
}
}
}
pub(crate) fn map_jsonrpc_error(
code: i32,
message: impl Into<String>,
data: Option<serde_json::Value>,
) -> a2a_protocol_types::A2aError {
use a2a_protocol_types::{A2aError, ErrorCode};
let message = message.into();
match ErrorCode::try_from(code) {
Ok(known) => match data {
Some(d) => A2aError::with_data(known, message, d),
None => A2aError::new(known, message),
},
Err(unknown) => {
let mut payload = serde_json::Map::new();
payload.insert("originalCode".into(), serde_json::Value::from(unknown));
if let Some(d) = data {
payload.insert("data".into(), d);
}
A2aError::with_data(
ErrorCode::InternalError,
message,
serde_json::Value::Object(payload),
)
}
}
}
pub(crate) fn truncate_body(body: &str) -> String {
if body.len() <= MAX_ERROR_BODY_LEN {
body.to_owned()
} else {
let end = (0..=MAX_ERROR_BODY_LEN)
.rev()
.find(|&i| body.is_char_boundary(i))
.unwrap_or(0);
format!("{}...(truncated)", &body[..end])
}
}
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use crate::error::ClientResult;
use crate::streaming::EventStream;
pub trait Transport: Send + Sync + 'static {
fn send_request<'a>(
&'a self,
method: &'a str,
params: serde_json::Value,
extra_headers: &'a HashMap<String, String>,
) -> Pin<Box<dyn Future<Output = ClientResult<serde_json::Value>> + Send + 'a>>;
fn send_streaming_request<'a>(
&'a self,
method: &'a str,
params: serde_json::Value,
extra_headers: &'a HashMap<String, String>,
) -> Pin<Box<dyn Future<Output = ClientResult<EventStream>> + Send + 'a>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truncate_body_short_string_unchanged() {
let short = "hello world";
let result = truncate_body(short);
assert_eq!(result, short);
}
#[test]
fn truncate_body_exact_limit_unchanged() {
let body = "x".repeat(MAX_ERROR_BODY_LEN);
let result = truncate_body(&body);
assert_eq!(result, body, "body at exact limit should not be truncated");
}
#[test]
fn truncate_body_over_limit_is_truncated() {
let body = "a".repeat(MAX_ERROR_BODY_LEN + 100);
let result = truncate_body(&body);
assert!(
result.len() < body.len(),
"result should be shorter than input"
);
assert!(
result.ends_with("...(truncated)"),
"truncated body should end with marker: {result}"
);
assert!(
result.starts_with(&"a".repeat(MAX_ERROR_BODY_LEN)),
"truncated body should start with the first MAX_ERROR_BODY_LEN chars"
);
}
#[test]
fn truncate_body_empty_string() {
let result = truncate_body("");
assert_eq!(result, "");
}
#[test]
fn truncate_body_multibyte_utf8_no_panic() {
let base = "é".repeat(MAX_ERROR_BODY_LEN); assert!(base.len() > MAX_ERROR_BODY_LEN);
let result = truncate_body(&base);
assert!(
result.ends_with("...(truncated)"),
"should be truncated: {result}"
);
let prefix = result.trim_end_matches("...(truncated)");
assert!(
prefix.len() <= MAX_ERROR_BODY_LEN,
"prefix should not exceed limit"
);
}
#[test]
fn truncate_body_mid_multibyte_boundary() {
let mut body = "a".repeat(MAX_ERROR_BODY_LEN - 1); body.push('€'); assert_eq!(body.len(), MAX_ERROR_BODY_LEN + 2);
assert!(
!body.is_char_boundary(MAX_ERROR_BODY_LEN),
"byte 512 should be mid-character"
);
let result = truncate_body(&body);
assert!(
result.ends_with("...(truncated)"),
"should be truncated: {result}"
);
let prefix = result.trim_end_matches("...(truncated)");
assert_eq!(
prefix.len(),
MAX_ERROR_BODY_LEN - 1,
"should truncate to last valid char boundary before limit"
);
assert_eq!(prefix, "a".repeat(MAX_ERROR_BODY_LEN - 1));
}
#[test]
fn truncate_body_two_byte_char_at_boundary() {
let mut body = "b".repeat(MAX_ERROR_BODY_LEN - 1); body.push('é'); assert_eq!(body.len(), MAX_ERROR_BODY_LEN + 1);
assert!(
!body.is_char_boundary(MAX_ERROR_BODY_LEN),
"byte 512 should be inside 'é'"
);
let result = truncate_body(&body);
let prefix = result.trim_end_matches("...(truncated)");
assert_eq!(prefix.len(), MAX_ERROR_BODY_LEN - 1);
}
async fn spawn_sized_body(declared: usize, body_len: usize) -> std::net::SocketAddr {
use tokio::io::AsyncWriteExt as _;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind");
let addr = listener.local_addr().expect("addr");
tokio::spawn(async move {
if let Ok((mut stream, _)) = listener.accept().await {
let mut buf = [0_u8; 1024];
let _ = tokio::io::AsyncReadExt::read(&mut stream, &mut buf).await;
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: \
{declared}\r\n\r\n"
);
let _ = stream.write_all(head.as_bytes()).await;
let _ = stream.write_all(&vec![b'x'; body_len]).await;
let _ = stream.flush().await;
}
});
addr
}
async fn fetch(addr: std::net::SocketAddr) -> hyper::Response<hyper::body::Incoming> {
use http_body_util::Full;
use hyper::body::Bytes;
let client: hyper_util::client::legacy::Client<
hyper_util::client::legacy::connect::HttpConnector,
Full<Bytes>,
> = hyper_util::client::legacy::Client::builder(hyper_util::rt::TokioExecutor::new())
.build_http();
let uri: hyper::Uri = format!("http://{addr}/").parse().expect("uri");
client.get(uri).await.expect("request")
}
#[tokio::test]
async fn body_of_exactly_max_size_is_accepted() {
const LIMIT: usize = 4096;
let addr = spawn_sized_body(LIMIT, LIMIT).await;
let resp = fetch(addr).await;
let bytes = collect_response_limited(resp, LIMIT, std::time::Duration::from_secs(10))
.await
.expect("a body of exactly the limit is not over the limit");
assert_eq!(bytes.len(), LIMIT);
}
#[tokio::test]
async fn oversized_body_is_rejected_before_reading_and_names_the_size() {
const LIMIT: usize = 4096;
const DECLARED: usize = LIMIT * 4;
let addr = spawn_sized_body(DECLARED, 64).await;
let resp = fetch(addr).await;
let err = collect_response_limited(resp, LIMIT, std::time::Duration::from_secs(10))
.await
.expect_err("a declared body four times the limit must be rejected");
let msg = err.to_string();
assert!(
msg.contains(&DECLARED.to_string()),
"the early Content-Length rejection names the declared size; a \
message without it means the read-time limiter caught this \
instead, leaving the size_hint branch untested. got: {msg}"
);
}
#[tokio::test]
async fn two_mib_body_is_within_the_default_ceiling() {
const BODY: usize = 2 * 1024 * 1024;
const _: () = assert!(
BODY > 32 * 1024 + 1024,
"must exceed the `32 * 1024 + 1024` mutant"
);
const _: () = assert!(
BODY > 32 + 1024 * 1024,
"must exceed the `32 + 1024 * 1024` mutant"
);
const _: () = assert!(
BODY < DEFAULT_MAX_RESPONSE_SIZE,
"must stay under the real ceiling"
);
let addr = spawn_sized_body(BODY, BODY).await;
let resp = fetch(addr).await;
let bytes = collect_response_limited(
resp,
DEFAULT_MAX_RESPONSE_SIZE,
std::time::Duration::from_secs(30),
)
.await
.expect("2 MiB is well inside the 32 MiB default");
assert_eq!(bytes.len(), BODY);
}
}