a2a_protocol_server/agent_card/
dynamic_handler.rs1use std::future::Future;
12use std::pin::Pin;
13
14use a2a_protocol_types::agent_card::AgentCard;
15use a2a_protocol_types::error::A2aResult;
16use bytes::Bytes;
17use http_body_util::Full;
18
19use crate::agent_card::caching::{format_http_date, make_etag, CacheConfig};
20use crate::agent_card::CORS_ALLOW_ALL;
21
22pub trait AgentCardProducer: Send + Sync + 'static {
26 fn produce<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<AgentCard>> + Send + 'a>>;
32}
33
34#[derive(Debug)]
36pub struct DynamicAgentCardHandler<P> {
37 producer: P,
38 cache_config: CacheConfig,
39}
40
41impl<P: AgentCardProducer> DynamicAgentCardHandler<P> {
42 #[must_use]
44 pub fn new(producer: P) -> Self {
45 Self {
46 producer,
47 cache_config: CacheConfig::default(),
48 }
49 }
50
51 #[must_use]
53 pub const fn with_max_age(mut self, seconds: u32) -> Self {
54 self.cache_config = CacheConfig::with_max_age(seconds);
55 self
56 }
57
58 #[allow(clippy::future_not_send)] pub async fn handle(
64 &self,
65 req: &hyper::Request<impl hyper::body::Body>,
66 ) -> hyper::Response<Full<Bytes>> {
67 let if_none_match = req
70 .headers()
71 .get("if-none-match")
72 .and_then(|v| v.to_str().ok())
73 .map(str::to_owned);
74 let if_modified_since = req
75 .headers()
76 .get("if-modified-since")
77 .and_then(|v| v.to_str().ok())
78 .map(str::to_owned);
79
80 match self.producer.produce().await {
81 Ok(card) => match serde_json::to_vec(&card) {
82 Ok(json) => {
83 let etag = make_etag(&json);
84 let last_modified = format_http_date(std::time::SystemTime::now());
85
86 let not_modified = is_not_modified(
87 if_none_match.as_deref(),
88 if_modified_since.as_deref(),
89 &etag,
90 &last_modified,
91 );
92
93 if not_modified {
94 hyper::Response::builder()
95 .status(304)
96 .header("etag", &etag)
97 .header("last-modified", &last_modified)
98 .header("cache-control", self.cache_config.header_value())
99 .body(Full::new(Bytes::new()))
100 .unwrap_or_else(|_| fallback_error_response())
101 } else {
102 hyper::Response::builder()
103 .status(200)
104 .header("content-type", "application/json")
105 .header("access-control-allow-origin", CORS_ALLOW_ALL)
106 .header("etag", &etag)
107 .header("last-modified", &last_modified)
108 .header("cache-control", self.cache_config.header_value())
109 .body(Full::new(Bytes::from(json)))
110 .unwrap_or_else(|_| fallback_error_response())
111 }
112 }
113 Err(e) => error_response(500, &format!("serialization error: {e}")),
114 },
115 Err(e) => error_response(500, &format!("card producer error: {e}")),
116 }
117 }
118
119 pub async fn handle_unconditional(&self) -> hyper::Response<Full<Bytes>> {
121 match self.producer.produce().await {
122 Ok(card) => match serde_json::to_vec(&card) {
123 Ok(json) => {
124 let etag = make_etag(&json);
125 let last_modified = format_http_date(std::time::SystemTime::now());
126 hyper::Response::builder()
127 .status(200)
128 .header("content-type", "application/json")
129 .header("access-control-allow-origin", CORS_ALLOW_ALL)
130 .header("etag", &etag)
131 .header("last-modified", &last_modified)
132 .header("cache-control", self.cache_config.header_value())
133 .body(Full::new(Bytes::from(json)))
134 .unwrap_or_else(|_| fallback_error_response())
135 }
136 Err(e) => error_response(500, &format!("serialization error: {e}")),
137 },
138 Err(e) => error_response(500, &format!("card producer error: {e}")),
139 }
140 }
141}
142
143fn is_not_modified(
145 if_none_match: Option<&str>,
146 if_modified_since: Option<&str>,
147 current_etag: &str,
148 current_last_modified: &str,
149) -> bool {
150 if let Some(inm) = if_none_match {
152 return etag_matches(inm, current_etag);
153 }
154 if let Some(ims) = if_modified_since {
155 return ims == current_last_modified;
156 }
157 false
158}
159
160fn etag_matches(header_value: &str, current: &str) -> bool {
162 let header_value = header_value.trim();
163 if header_value == "*" {
164 return true;
165 }
166 let current_bare = current.strip_prefix("W/").unwrap_or(current);
167 for candidate in header_value.split(',') {
168 let candidate = candidate.trim();
169 let candidate_bare = candidate.strip_prefix("W/").unwrap_or(candidate);
170 if candidate_bare == current_bare {
171 return true;
172 }
173 }
174 false
175}
176
177fn error_response(status: u16, message: &str) -> hyper::Response<Full<Bytes>> {
179 let body = serde_json::json!({ "error": message });
180 let bytes = serde_json::to_vec(&body).unwrap_or_default();
181 hyper::Response::builder()
182 .status(status)
183 .header("content-type", "application/json")
184 .body(Full::new(Bytes::from(bytes)))
185 .unwrap_or_else(|_| fallback_error_response())
186}
187
188fn fallback_error_response() -> hyper::Response<Full<Bytes>> {
191 hyper::Response::new(Full::new(Bytes::from_static(
192 br#"{"error":"internal server error"}"#,
193 )))
194}