1use super::rpc::{SendStreamingMessageResponse, StreamingEvent, TaskPushNotificationConfig};
7use reqwest::Client;
8use std::time::Duration;
9use tracing::{debug, warn};
10
11#[derive(Debug, Clone)]
13pub struct WebhookNotifier {
14 client: Client,
15 max_retries: u32,
16 retry_delay_ms: u64,
17}
18
19impl Default for WebhookNotifier {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl WebhookNotifier {
26 fn build_http_client() -> Client {
27 match Client::builder().timeout(Duration::from_secs(10)).build() {
28 Ok(client) => client,
29 Err(error) => {
30 warn!(error = %error, "Failed to configure webhook HTTP client; using default client");
31 Client::new()
32 }
33 }
34 }
35
36 pub(crate) fn new() -> Self {
38 Self {
39 client: Self::build_http_client(),
40 max_retries: 3,
41 retry_delay_ms: 1000,
42 }
43 }
44
45 fn with_settings(max_retries: u32, retry_delay_ms: u64) -> Self {
47 Self {
48 client: Self::build_http_client(),
49 max_retries,
50 retry_delay_ms,
51 }
52 }
53
54 pub(crate) async fn send_event(
56 &self,
57 config: &TaskPushNotificationConfig,
58 event: StreamingEvent,
59 ) -> Result<(), WebhookError> {
60 let response = SendStreamingMessageResponse { event };
61 let json = serde_json::to_string(&response).map_err(|e| WebhookError::Serialization(e.to_string()))?;
62
63 self.send_with_retry(&config.url, &json, config.authentication.as_deref()).await
64 }
65
66 async fn send_with_retry(&self, url: &str, json: &str, auth: Option<&str>) -> Result<(), WebhookError> {
68 let mut last_error = None;
69
70 for attempt in 0..=self.max_retries {
71 if attempt > 0 {
72 let delay = self.retry_delay_ms * 2u64.pow(attempt - 1); debug!("Retrying webhook delivery after {}ms (attempt {})", delay, attempt);
74 tokio::time::sleep(Duration::from_millis(delay)).await;
75 }
76
77 match self.send_request(url, json, auth).await {
78 Ok(()) => {
79 debug!("Webhook delivered successfully to {}", url);
80 return Ok(());
81 }
82 Err(e) => {
83 warn!("Webhook delivery attempt {} failed: {}", attempt + 1, e);
84 last_error = Some(e);
85 }
86 }
87 }
88
89 Err(last_error.unwrap_or(WebhookError::Unknown))
90 }
91
92 async fn send_request(&self, url: &str, json: &str, auth: Option<&str>) -> Result<(), WebhookError> {
94 let mut request = self
95 .client
96 .post(url)
97 .header("Content-Type", "application/json")
98 .header("User-Agent", "VT Code-A2A/1.0");
99
100 if let Some(auth_header) = auth {
101 request = request.header("Authorization", auth_header);
102 }
103
104 let response = request
105 .body(json.to_string())
106 .send()
107 .await
108 .map_err(|e| WebhookError::Network(e.to_string()))?;
109
110 if response.status().is_success() {
111 Ok(())
112 } else {
113 Err(WebhookError::HttpError(response.status().as_u16()))
114 }
115 }
116}
117
118#[derive(Debug, Clone, thiserror::Error)]
120pub enum WebhookError {
121 #[error("Network error: {0}")]
123 Network(String),
124 #[error("HTTP error: {0}")]
126 HttpError(u16),
127 #[error("Serialization error: {0}")]
129 Serialization(String),
130 #[error("Unknown error")]
132 Unknown,
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::types::{TaskState, TaskStatus};
139
140 #[test]
141 fn test_webhook_notifier_creation() {
142 let notifier = WebhookNotifier::new();
143 assert_eq!(notifier.max_retries, 3);
144 assert_eq!(notifier.retry_delay_ms, 1000);
145 }
146
147 #[test]
148 fn test_webhook_notifier_with_settings() {
149 let notifier = WebhookNotifier::with_settings(5, 2000);
150 assert_eq!(notifier.max_retries, 5);
151 assert_eq!(notifier.retry_delay_ms, 2000);
152 }
153
154 #[tokio::test]
155 async fn test_webhook_error_display() {
156 let err = WebhookError::Network("Connection refused".to_string());
157 assert!(err.to_string().contains("Network error"));
158
159 let err = WebhookError::HttpError(404);
160 assert!(err.to_string().contains("404"));
161 }
162
163 #[tokio::test]
164 async fn test_send_event_serialization() {
165 let notifier = WebhookNotifier::new();
166 let config = TaskPushNotificationConfig {
167 task_id: "task-1".to_string(),
168 url: "https://example.com/webhook".to_string(),
169 authentication: None,
170 };
171
172 let event = StreamingEvent::TaskStatus {
173 task_id: "task-1".to_string(),
174 context_id: None,
175 status: TaskStatus::new(TaskState::Completed),
176 kind: "status-update".to_string(),
177 r#final: true,
178 };
179
180 let result = notifier.send_event(&config, event).await;
183 assert!(result.is_err());
184
185 if let Err(WebhookError::Serialization(_)) = result {
186 panic!("Unexpected serialization error");
187 }
188 }
189}