Skip to main content

tap_http/
client.rs

1//! HTTP client for delivering DIDComm messages to external endpoints.
2
3use crate::error::{Error, Result};
4use reqwest::{Client as ReqwestClient, StatusCode};
5use std::time::Duration;
6use tokio::time::timeout;
7use tracing::{debug, error, info};
8
9/// Default timeout for HTTP requests in seconds.
10const DEFAULT_TIMEOUT_SECS: u64 = 30;
11
12/// DIDComm HTTP Client for delivering messages to external endpoints.
13pub struct DIDCommClient {
14    /// HTTP client.
15    client: ReqwestClient,
16
17    /// Request timeout in seconds.
18    timeout_secs: u64,
19}
20
21impl DIDCommClient {
22    /// Creates a new DIDComm HTTP client.
23    pub fn new(timeout_secs: Option<u64>) -> Self {
24        Self {
25            client: ReqwestClient::new(),
26            timeout_secs: timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS),
27        }
28    }
29
30    /// Sets the request timeout in seconds.
31    pub fn with_timeout(mut self, timeout_secs: u64) -> Self {
32        self.timeout_secs = timeout_secs;
33        self
34    }
35
36    /// Delivers a DIDComm message to an external endpoint.
37    pub async fn deliver_message(&self, endpoint: &str, message: &str) -> Result<()> {
38        info!("Delivering DIDComm message to {}", endpoint);
39        debug!("Message size: {} bytes", message.len());
40
41        // Create a timeout for the request
42        let request_timeout = Duration::from_secs(self.timeout_secs);
43
44        // Set up the request
45        let request = self
46            .client
47            .post(endpoint)
48            .header("Content-Type", "application/didcomm-encrypted+json")
49            .body(message.to_string());
50
51        // Execute the request with a timeout
52        let response = match timeout(request_timeout, request.send()).await {
53            Ok(result) => match result {
54                Ok(response) => response,
55                Err(e) => return Err(Error::Http(format!("Failed to send message: {}", e))),
56            },
57            Err(_) => {
58                return Err(Error::Http(format!(
59                    "Request timed out after {} seconds",
60                    self.timeout_secs
61                )))
62            }
63        };
64
65        // Check the response status
66        match response.status() {
67            StatusCode::OK | StatusCode::ACCEPTED | StatusCode::CREATED => {
68                info!("Message delivered successfully");
69                Ok(())
70            }
71            status => {
72                // Try to get the response body if there's an error
73                let error_body = match response.text().await {
74                    Ok(body) => body,
75                    Err(_) => "<unable to read error response>".to_string(),
76                };
77
78                error!(
79                    "Failed to deliver message: Status {}, Body: {}",
80                    status, error_body
81                );
82                Err(Error::Http(format!(
83                    "Delivery failed with status code {}: {}",
84                    status, error_body
85                )))
86            }
87        }
88    }
89}
90
91impl Default for DIDCommClient {
92    fn default() -> Self {
93        Self::new(None)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    // Since mockito seems to have changed its API, we'll use a more basic test approach
102    #[tokio::test]
103    async fn test_client_creation() {
104        // Test client creation
105        let client = DIDCommClient::new(Some(10));
106        assert_eq!(client.timeout_secs, 10);
107
108        // Test default client
109        let default_client = DIDCommClient::default();
110        assert_eq!(default_client.timeout_secs, DEFAULT_TIMEOUT_SECS);
111
112        // Test with_timeout
113        let custom_client = DIDCommClient::default().with_timeout(15);
114        assert_eq!(custom_client.timeout_secs, 15);
115    }
116}