a2a_server/push/
sender.rs1use a2a::*;
4use std::time::Duration;
5
6pub struct HttpPushSender {
10 client: reqwest::Client,
11 fail_on_error: bool,
12}
13
14pub struct HttpPushSenderConfig {
16 pub timeout: Duration,
18 pub fail_on_error: bool,
20}
21
22impl Default for HttpPushSenderConfig {
23 fn default() -> Self {
24 HttpPushSenderConfig {
25 timeout: Duration::from_secs(30),
26 fail_on_error: false,
27 }
28 }
29}
30
31impl HttpPushSender {
32 pub fn new(config: Option<HttpPushSenderConfig>) -> Self {
33 let config = config.unwrap_or_default();
34 let client = reqwest::Client::builder()
35 .timeout(config.timeout)
36 .build()
37 .expect("failed to create HTTP client");
38 HttpPushSender {
39 client,
40 fail_on_error: config.fail_on_error,
41 }
42 }
43
44 pub async fn send_push(
46 &self,
47 config: &PushNotificationConfig,
48 event: StreamResponse,
49 ) -> Result<(), A2AError> {
50 let body = match serde_json::to_vec(&event) {
51 Ok(b) => b,
52 Err(e) => return self.handle_error(format!("failed to serialize event: {e}")),
53 };
54
55 let mut request = self
56 .client
57 .post(&config.url)
58 .header("Content-Type", "application/json")
59 .body(body);
60
61 if let Some(ref token) = config.token {
62 request = request.header("A2A-Notification-Token", token);
63 }
64
65 if let Some(ref auth) = config.authentication {
66 if let Some(ref creds) = auth.credentials {
67 match auth.scheme.to_lowercase().as_str() {
68 "bearer" => {
69 request = request.header("Authorization", format!("Bearer {creds}"));
70 }
71 "basic" => {
72 request = request.header("Authorization", format!("Basic {creds}"));
73 }
74 _ => {}
75 }
76 }
77 }
78
79 match request.send().await {
80 Ok(resp) => {
81 if !resp.status().is_success() {
82 return self
83 .handle_error(format!("push endpoint returned status: {}", resp.status()));
84 }
85 Ok(())
86 }
87 Err(e) => self.handle_error(format!("failed to send push notification: {e}")),
88 }
89 }
90
91 fn handle_error(&self, msg: String) -> Result<(), A2AError> {
92 if self.fail_on_error {
93 Err(A2AError::internal(&msg))
94 } else {
95 tracing::error!("{}", msg);
96 Ok(())
97 }
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_default_config() {
107 let config = HttpPushSenderConfig::default();
108 assert_eq!(config.timeout, Duration::from_secs(30));
109 assert!(!config.fail_on_error);
110 }
111
112 #[test]
113 fn test_sender_new_default() {
114 let sender = HttpPushSender::new(None);
115 assert!(!sender.fail_on_error);
116 }
117
118 #[test]
119 fn test_sender_new_custom() {
120 let config = HttpPushSenderConfig {
121 timeout: Duration::from_secs(10),
122 fail_on_error: true,
123 };
124 let sender = HttpPushSender::new(Some(config));
125 assert!(sender.fail_on_error);
126 }
127
128 #[test]
129 fn test_handle_error_fail_on_error() {
130 let sender = HttpPushSender::new(Some(HttpPushSenderConfig {
131 timeout: Duration::from_secs(5),
132 fail_on_error: true,
133 }));
134 let result = sender.handle_error("test error".to_string());
135 assert!(result.is_err());
136 }
137
138 #[test]
139 fn test_handle_error_ignore() {
140 let sender = HttpPushSender::new(None);
141 let result = sender.handle_error("test error".to_string());
142 assert!(result.is_ok());
143 }
144
145 #[tokio::test]
146 async fn test_send_push_connection_refused_no_fail() {
147 use a2a::event::*;
148 let sender = HttpPushSender::new(None);
150 let config = PushNotificationConfig {
151 url: "http://127.0.0.1:1/callback".to_string(),
152 id: None,
153 token: Some("tok".to_string()),
154 authentication: Some(AuthenticationInfo {
155 scheme: "bearer".to_string(),
156 credentials: Some("secret".to_string()),
157 }),
158 };
159 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
160 task_id: "t1".into(),
161 context_id: "c1".into(),
162 status: TaskStatus {
163 state: TaskState::Working,
164 message: None,
165 timestamp: None,
166 },
167 metadata: None,
168 });
169 let result = sender.send_push(&config, event).await;
170 assert!(result.is_ok());
172 }
173
174 #[tokio::test]
175 async fn test_send_push_connection_refused_fail() {
176 use a2a::event::*;
177 let sender = HttpPushSender::new(Some(HttpPushSenderConfig {
178 timeout: std::time::Duration::from_millis(100),
179 fail_on_error: true,
180 }));
181 let config = PushNotificationConfig {
182 url: "http://127.0.0.1:1/callback".to_string(),
183 id: None,
184 token: None,
185 authentication: None,
186 };
187 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
188 task_id: "t1".into(),
189 context_id: "c1".into(),
190 status: TaskStatus {
191 state: TaskState::Working,
192 message: None,
193 timestamp: None,
194 },
195 metadata: None,
196 });
197 let result = sender.send_push(&config, event).await;
198 assert!(result.is_err());
199 }
200
201 #[tokio::test]
202 async fn test_send_push_with_basic_auth() {
203 use a2a::event::*;
204 let sender = HttpPushSender::new(None);
205 let config = PushNotificationConfig {
206 url: "http://127.0.0.1:1/callback".to_string(),
207 id: None,
208 token: None,
209 authentication: Some(AuthenticationInfo {
210 scheme: "basic".to_string(),
211 credentials: Some("dXNlcjpwYXNz".to_string()),
212 }),
213 };
214 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
215 task_id: "t1".into(),
216 context_id: "c1".into(),
217 status: TaskStatus {
218 state: TaskState::Working,
219 message: None,
220 timestamp: None,
221 },
222 metadata: None,
223 });
224 let result = sender.send_push(&config, event).await;
226 assert!(result.is_ok()); }
228
229 #[tokio::test]
230 async fn test_send_push_with_unknown_auth_scheme() {
231 use a2a::event::*;
232 let sender = HttpPushSender::new(None);
233 let config = PushNotificationConfig {
234 url: "http://127.0.0.1:1/callback".to_string(),
235 id: None,
236 token: None,
237 authentication: Some(AuthenticationInfo {
238 scheme: "custom".to_string(),
239 credentials: Some("cred".to_string()),
240 }),
241 };
242 let event = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
243 task_id: "t1".into(),
244 context_id: "c1".into(),
245 status: TaskStatus {
246 state: TaskState::Working,
247 message: None,
248 timestamp: None,
249 },
250 metadata: None,
251 });
252 let result = sender.send_push(&config, event).await;
253 assert!(result.is_ok());
254 }
255}