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}