Skip to main content

aep_core/
transport.rs

1use async_trait::async_trait;
2use http::{HeaderMap, Method, StatusCode};
3use std::{error::Error as StdError, fmt};
4
5use thiserror::Error;
6use url::Url;
7
8#[derive(Clone)]
9pub struct HttpRequest {
10    pub method: Method,
11    pub url: Url,
12    pub headers: HeaderMap,
13    pub body: Vec<u8>,
14}
15
16impl fmt::Debug for HttpRequest {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        formatter
19            .debug_struct("HttpRequest")
20            .field("method", &self.method)
21            .field("url", &"[REDACTED]")
22            .field("headers", &"[REDACTED]")
23            .field("body", &"[REDACTED]")
24            .finish()
25    }
26}
27
28#[derive(Clone)]
29pub struct HttpResponse {
30    pub status: StatusCode,
31    pub final_url: Url,
32    pub headers: HeaderMap,
33    pub body: Vec<u8>,
34}
35
36impl fmt::Debug for HttpResponse {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter
39            .debug_struct("HttpResponse")
40            .field("status", &self.status)
41            .field("final_url", &"[REDACTED]")
42            .field("headers", &"[REDACTED]")
43            .field("body", &"[REDACTED]")
44            .finish()
45    }
46}
47
48#[derive(Debug, Error)]
49#[error("{message}")]
50pub struct TransportError {
51    message: String,
52    #[source]
53    source: Option<Box<dyn StdError + Send + Sync>>,
54}
55
56impl TransportError {
57    pub fn new(message: impl Into<String>) -> Self {
58        Self {
59            message: message.into(),
60            source: None,
61        }
62    }
63
64    pub fn with_source(
65        message: impl Into<String>,
66        source: impl StdError + Send + Sync + 'static,
67    ) -> Self {
68        Self {
69            message: message.into(),
70            source: Some(Box::new(source)),
71        }
72    }
73}
74
75#[async_trait]
76pub trait HttpTransport: Send + Sync {
77    async fn send(&self, request: HttpRequest) -> Result<HttpResponse, TransportError>;
78}
79
80#[cfg(test)]
81mod tests {
82    use std::fmt;
83
84    use super::*;
85
86    #[derive(Debug)]
87    struct ExampleError;
88
89    impl fmt::Display for ExampleError {
90        fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
91            formatter.write_str("cause")
92        }
93    }
94
95    impl StdError for ExampleError {}
96
97    #[test]
98    fn preserves_transport_error_context() {
99        assert_eq!(
100            TransportError::new("request failed").to_string(),
101            "request failed"
102        );
103        let error = TransportError::with_source("request failed", ExampleError);
104        assert_eq!(error.to_string(), "request failed");
105        assert_eq!(
106            StdError::source(&error).map(ToString::to_string),
107            Some("cause".to_owned())
108        );
109    }
110
111    #[test]
112    fn redacts_http_messages_from_debug_output() {
113        let request = HttpRequest {
114            method: Method::POST,
115            url: Url::parse("https://service.example/path?token=secret").expect("URL"),
116            headers: HeaderMap::from_iter([(
117                http::header::AUTHORIZATION,
118                "Bearer secret".parse().expect("header"),
119            )]),
120            body: b"secret body".to_vec(),
121        };
122        let response = HttpResponse {
123            status: StatusCode::OK,
124            final_url: request.url.clone(),
125            headers: request.headers.clone(),
126            body: request.body.clone(),
127        };
128        for output in [format!("{request:?}"), format!("{response:?}")] {
129            assert!(!output.contains("secret"));
130            assert!(output.contains("[REDACTED]"));
131        }
132    }
133}