use futures_util::StreamExt;
use reqwest::StatusCode;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use super::A2A_PROTOCOL_VERSION;
use crate::a2a::config::{A2aAgentConfig, A2aConfigError, A2aLimits};
use crate::a2a::error::A2aError;
const VERSION_HEADER: &str = "a2a-version";
pub(super) async fn exchange(
request: reqwest::RequestBuilder,
limits: &A2aLimits,
) -> Result<(StatusCode, Vec<u8>), A2aError> {
let operation = async {
let response = send(request).await?;
let status = response.status();
let body = read_bounded(response, limits.max_response_bytes).await?;
Ok((status, body))
};
tokio::time::timeout(limits.request_timeout, operation)
.await
.map_err(|_| A2aError::Timeout(limits.request_timeout))?
}
pub(super) async fn send(request: reqwest::RequestBuilder) -> Result<reqwest::Response, A2aError> {
let response = request.send().await.map_err(transport_error)?;
if response.status().is_redirection() {
return Err(A2aError::RedirectRefused);
}
Ok(response)
}
pub(super) async fn read_bounded(
response: reqwest::Response,
limit: usize,
) -> Result<Vec<u8>, A2aError> {
if response
.content_length()
.is_some_and(|length| length > limit as u64)
{
return Err(A2aError::ResponseTooLarge { limit });
}
let mut body = response.bytes_stream();
let mut buffer = Vec::new();
while let Some(chunk) = body.next().await {
let chunk = chunk.map_err(|_| A2aError::Transport("the reply body could not be read"))?;
if buffer.len().saturating_add(chunk.len()) > limit {
return Err(A2aError::ResponseTooLarge { limit });
}
buffer.extend_from_slice(&chunk);
}
Ok(buffer)
}
pub(super) fn build_http_client(limits: &A2aLimits) -> Result<reqwest::Client, A2aError> {
reqwest::Client::builder()
.redirect(reqwest::redirect::Policy::none())
.retry(reqwest::retry::never())
.connect_timeout(limits.connect_timeout)
.build()
.map_err(|_| A2aError::Transport("the HTTP client could not be built"))
}
pub(super) fn build_headers(config: &A2aAgentConfig) -> Result<HeaderMap, A2aError> {
let mut headers = HeaderMap::new();
for (name, value) in &config.headers {
let header_name = HeaderName::try_from(name.as_str())
.map_err(|_| A2aConfigError::InvalidHeaderName { name: name.clone() })?;
let mut header_value = HeaderValue::try_from(value.expose_secret())
.map_err(|_| A2aConfigError::InvalidHeaderValue { name: name.clone() })?;
header_value.set_sensitive(true);
headers.insert(header_name, header_value);
}
headers.insert(
HeaderName::from_static(VERSION_HEADER),
HeaderValue::from_static(A2A_PROTOCOL_VERSION),
);
Ok(headers)
}
pub(super) fn transport_error(error: reqwest::Error) -> A2aError {
if error.is_connect() {
A2aError::Connect
} else if error.is_timeout() {
A2aError::Transport("the request timed out")
} else if error.is_request() {
A2aError::Transport("the request could not be sent")
} else {
A2aError::Transport("the connection failed")
}
}