#[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);
}
}