a2a_protocol_server/agent_card/
static_handler.rs1use a2a_protocol_types::agent_card::AgentCard;
14use bytes::Bytes;
15use http_body_util::Full;
16
17use crate::agent_card::caching::{
18 check_conditional, format_http_date, make_etag, CacheConfig, ConditionalResult,
19};
20use crate::agent_card::CORS_ALLOW_ALL;
21use crate::error::ServerResult;
22
23#[derive(Debug, Clone)]
25pub struct StaticAgentCardHandler {
26 card_json: Bytes,
27 etag: String,
28 last_modified: String,
29 cache_config: CacheConfig,
30}
31
32impl StaticAgentCardHandler {
33 pub fn new(card: &AgentCard) -> ServerResult<Self> {
42 let json = serde_json::to_vec(card)?;
43 let etag = make_etag(&json);
44 Ok(Self {
45 card_json: Bytes::from(json),
46 etag,
47 last_modified: format_http_date(std::time::SystemTime::now()),
48 cache_config: CacheConfig::default(),
49 })
50 }
51
52 #[must_use]
54 pub const fn with_max_age(mut self, seconds: u32) -> Self {
55 self.cache_config = CacheConfig::with_max_age(seconds);
56 self
57 }
58
59 #[must_use]
64 pub fn handle(
65 &self,
66 req: &hyper::Request<impl hyper::body::Body>,
67 ) -> hyper::Response<Full<Bytes>> {
68 let result = check_conditional(req, &self.etag, &self.last_modified);
69 match result {
70 ConditionalResult::NotModified => self.not_modified_response(),
71 ConditionalResult::SendFull => self.full_response(),
72 }
73 }
74
75 #[must_use]
77 pub fn handle_unconditional(&self) -> hyper::Response<Full<Bytes>> {
78 self.full_response()
79 }
80
81 fn full_response(&self) -> hyper::Response<Full<Bytes>> {
82 hyper::Response::builder()
83 .status(200)
84 .header("content-type", "application/json")
85 .header("access-control-allow-origin", CORS_ALLOW_ALL)
86 .header("etag", &self.etag)
87 .header("last-modified", &self.last_modified)
88 .header("cache-control", self.cache_config.header_value())
89 .body(Full::new(self.card_json.clone()))
90 .unwrap_or_else(|_| hyper::Response::new(Full::new(Bytes::new())))
91 }
92
93 fn not_modified_response(&self) -> hyper::Response<Full<Bytes>> {
94 hyper::Response::builder()
95 .status(304)
96 .header("etag", &self.etag)
97 .header("last-modified", &self.last_modified)
98 .header("cache-control", self.cache_config.header_value())
99 .body(Full::new(Bytes::new()))
100 .unwrap_or_else(|_| hyper::Response::new(Full::new(Bytes::new())))
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::agent_card::caching::tests::minimal_agent_card;
108
109 #[test]
110 fn static_handler_returns_etag_and_cache_headers() {
111 let card = minimal_agent_card();
112 let handler = StaticAgentCardHandler::new(&card).unwrap();
113 let req = hyper::Request::builder()
114 .method("GET")
115 .uri("/.well-known/agent.json")
116 .body(Full::new(Bytes::new()))
117 .unwrap();
118 let resp = handler.handle(&req);
119 assert_eq!(resp.status(), 200);
120 assert!(resp.headers().contains_key("etag"));
121 assert!(resp.headers().contains_key("last-modified"));
122 assert!(resp.headers().contains_key("cache-control"));
123 }
124
125 #[test]
126 fn static_handler_304_on_matching_etag() {
127 let card = minimal_agent_card();
128 let handler = StaticAgentCardHandler::new(&card).unwrap();
129 let req1 = hyper::Request::builder()
131 .method("GET")
132 .uri("/.well-known/agent.json")
133 .body(Full::new(Bytes::new()))
134 .unwrap();
135 let resp1 = handler.handle(&req1);
136 let etag = resp1
137 .headers()
138 .get("etag")
139 .unwrap()
140 .to_str()
141 .unwrap()
142 .to_owned();
143
144 let req2 = hyper::Request::builder()
146 .method("GET")
147 .uri("/.well-known/agent.json")
148 .header("if-none-match", &etag)
149 .body(Full::new(Bytes::new()))
150 .unwrap();
151 let resp2 = handler.handle(&req2);
152 assert_eq!(resp2.status(), 304);
153 }
154
155 #[test]
156 fn static_handler_200_on_mismatched_etag() {
157 let card = minimal_agent_card();
158 let handler = StaticAgentCardHandler::new(&card).unwrap();
159 let req = hyper::Request::builder()
160 .method("GET")
161 .uri("/.well-known/agent.json")
162 .header("if-none-match", "\"wrong-etag\"")
163 .body(Full::new(Bytes::new()))
164 .unwrap();
165 let resp = handler.handle(&req);
166 assert_eq!(resp.status(), 200);
167 }
168
169 #[test]
171 fn static_handler_unconditional_returns_200() {
172 let card = minimal_agent_card();
173 let handler = StaticAgentCardHandler::new(&card).unwrap();
174 let resp = handler.handle_unconditional();
175 assert_eq!(resp.status(), 200);
176 assert!(resp.headers().contains_key("etag"));
177 assert!(resp.headers().contains_key("content-type"));
178 assert_eq!(
179 resp.headers().get("access-control-allow-origin").unwrap(),
180 "*"
181 );
182 }
183
184 #[test]
185 fn static_handler_custom_max_age() {
186 let card = minimal_agent_card();
187 let handler = StaticAgentCardHandler::new(&card)
188 .unwrap()
189 .with_max_age(7200);
190 let req = hyper::Request::builder()
191 .method("GET")
192 .uri("/.well-known/agent.json")
193 .body(Full::new(Bytes::new()))
194 .unwrap();
195 let resp = handler.handle(&req);
196 let cc = resp
197 .headers()
198 .get("cache-control")
199 .unwrap()
200 .to_str()
201 .unwrap();
202 assert!(cc.contains("max-age=7200"));
203 }
204}