use std::collections::HashMap;
use std::convert::Infallible;
use bytes::Bytes;
use http_body_util::combinators::BoxBody;
use http_body_util::{BodyExt, Full};
use hyper::body::Incoming;
pub(super) fn extract_headers(headers: &hyper::HeaderMap) -> HashMap<String, String> {
let mut map = HashMap::with_capacity(headers.len());
for (key, value) in headers {
if let Ok(v) = value.to_str() {
map.insert(key.as_str().to_owned(), v.to_owned());
}
}
map
}
pub fn json_ok_response<T: serde::Serialize>(
value: &T,
) -> hyper::Response<BoxBody<Bytes, Infallible>> {
match serde_json::to_vec(value) {
Ok(body) => build_json_response(200, body),
Err(_err) => {
trace_error!(error = %_err, "REST response serialization failed");
internal_error_response()
}
}
}
pub(super) fn internal_error_response() -> hyper::Response<BoxBody<Bytes, Infallible>> {
let body = br#"{"error":{"code":500,"message":"internal serialization error"}}"#;
build_json_response(500, body.to_vec())
}
pub(super) fn health_response() -> hyper::Response<BoxBody<Bytes, Infallible>> {
let body = br#"{"status":"ok"}"#;
build_json_response(200, body.to_vec())
}
pub(super) fn build_json_response(
status: u16,
body: Vec<u8>,
) -> hyper::Response<BoxBody<Bytes, Infallible>> {
hyper::Response::builder()
.status(status)
.header("content-type", a2a_protocol_types::JSON_CONTENT_TYPE)
.header(
a2a_protocol_types::A2A_VERSION_HEADER,
a2a_protocol_types::A2A_VERSION,
)
.body(Full::new(Bytes::from(body)).boxed())
.unwrap_or_else(|_| {
hyper::Response::new(
Full::new(Bytes::from_static(br#"{"error":"response build error"}"#)).boxed(),
)
})
}
pub(super) async fn read_body_limited(
body: Incoming,
max_size: usize,
read_timeout: std::time::Duration,
) -> Result<Bytes, String> {
use http_body_util::{BodyExt, LengthLimitError, Limited};
let size_hint = <Incoming as hyper::body::Body>::size_hint(&body);
if let Some(upper) = size_hint.upper()
&& upper > max_size as u64
{
return Err(format!(
"request 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("request body read timed out".to_owned()),
Ok(Ok(collected)) => Ok(collected.to_bytes()),
Ok(Err(err)) => Err(if err.downcast_ref::<LengthLimitError>().is_some() {
format!("request body too large: exceeds {max_size} byte limit")
} else {
err.to_string()
}),
}
}
pub(super) fn inject_field_if_missing(
mut value: serde_json::Value,
field: &str,
path_value: &str,
) -> serde_json::Value {
if let Some(obj) = value.as_object_mut() {
obj.entry(field.to_owned())
.or_insert_with(|| serde_json::Value::String(path_value.to_owned()));
}
value
}
#[cfg(test)]
mod tests {
use super::*;
use http_body_util::BodyExt;
#[test]
fn extract_headers_lowercased_keys() {
let mut hm = hyper::HeaderMap::new();
hm.insert("Content-Type", "application/json".parse().unwrap());
hm.insert("Authorization", "Bearer tok".parse().unwrap());
let map = extract_headers(&hm);
assert_eq!(map.get("content-type").unwrap(), "application/json");
assert_eq!(map.get("authorization").unwrap(), "Bearer tok");
}
#[test]
fn extract_headers_empty() {
let hm = hyper::HeaderMap::new();
let map = extract_headers(&hm);
assert!(map.is_empty());
}
#[tokio::test]
async fn health_response_status_and_body() {
let resp = health_response();
assert_eq!(resp.status().as_u16(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let val: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(val["status"], "ok");
}
#[tokio::test]
async fn internal_error_response_is_500() {
let resp = internal_error_response();
assert_eq!(resp.status().as_u16(), 500);
}
#[tokio::test]
async fn build_json_response_includes_version_header() {
let resp = build_json_response(200, b"{}".to_vec());
assert_eq!(
resp.headers()
.get(a2a_protocol_types::A2A_VERSION_HEADER)
.and_then(|v| v.to_str().ok()),
Some(a2a_protocol_types::A2A_VERSION),
);
}
#[tokio::test]
async fn json_ok_response_serializes_value() {
let val = serde_json::json!({"key": "value"});
let resp = json_ok_response(&val);
assert_eq!(resp.status().as_u16(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(parsed["key"], "value");
}
#[test]
fn inject_field_when_missing() {
let val = serde_json::json!({"url": "https://example.com"});
let result = inject_field_if_missing(val, "taskId", "task-1");
assert_eq!(result["taskId"], "task-1");
assert_eq!(result["url"], "https://example.com");
}
#[test]
fn inject_field_preserves_existing() {
let val = serde_json::json!({"taskId": "existing", "url": "https://example.com"});
let result = inject_field_if_missing(val, "taskId", "task-1");
assert_eq!(
result["taskId"], "existing",
"should not overwrite existing field"
);
}
#[tokio::test]
async fn internal_error_response_has_json_body() {
let resp = internal_error_response();
assert_eq!(resp.status().as_u16(), 500);
let body = resp.into_body().collect().await.unwrap().to_bytes();
let text = String::from_utf8_lossy(&body);
assert!(
text.contains("internal serialization error"),
"internal error response should contain error message: {text}"
);
}
#[tokio::test]
async fn build_json_response_various_statuses() {
for status in [200, 201, 400, 404, 500] {
let resp = build_json_response(status, b"{}".to_vec());
assert_eq!(resp.status().as_u16(), status);
assert_eq!(
resp.headers()
.get("content-type")
.and_then(|v| v.to_str().ok()),
Some(a2a_protocol_types::JSON_CONTENT_TYPE),
);
}
}
#[test]
fn inject_field_on_non_object_is_noop() {
let val = serde_json::json!("string value");
let result = inject_field_if_missing(val.clone(), "taskId", "task-1");
assert_eq!(result, val);
}
#[tokio::test]
async fn build_json_response_with_empty_body() {
let resp = build_json_response(200, vec![]);
assert_eq!(resp.status().as_u16(), 200);
let body = resp.into_body().collect().await.unwrap().to_bytes();
assert!(body.is_empty());
}
}