saluki_io/net/util/retry/lifecycle/
http.rs1use std::{borrow::Cow, error::Error as _, fmt, time::Duration};
2
3use http::StatusCode;
4use tracing::{debug, warn};
5
6use super::RetryLifecycle;
7
8#[derive(Clone)]
14pub struct StandardHttpRetryLifecycle;
15
16impl<B, B2, E> RetryLifecycle<http::Request<B>, http::Response<B2>, E> for StandardHttpRetryLifecycle
17where
18 E: DynError,
19{
20 fn before_retry(
21 &self, req: &http::Request<B>, res: &Result<http::Response<B2>, E>, retry_backoff: Duration, error_count: u32,
22 ) {
23 let request_uri = SanitizedRequestUri(req.uri());
24 let categorized_error = CategorizedError::try_categorize(res);
25
26 warn!(error_count, %request_uri, "{}. Retrying after {:?}.", categorized_error, retry_backoff);
27 }
28
29 fn after_success(&self, req: &http::Request<B>, _: &Result<http::Response<B2>, E>) {
30 let request_uri = SanitizedRequestUri(req.uri());
31 debug!(%request_uri, "Request succeeded.");
32 }
33}
34
35struct SanitizedRequestUri<'a>(&'a http::Uri);
36
37impl fmt::Display for SanitizedRequestUri<'_> {
38 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39 let maybe_scheme = self.0.scheme_str();
42 let maybe_host = self.0.host();
43
44 if let (Some(scheme), Some(host)) = (maybe_scheme, maybe_host) {
45 write!(f, "{}://{}", scheme, host)?;
46 }
47
48 let maybe_port = self.0.port_u16();
49 if let Some(port) = maybe_port {
50 write!(f, ":{}", port)?;
51 }
52
53 write!(f, "{}", self.0.path())
55 }
56}
57
58enum CategorizedError {
59 Client(String),
60 Tls(String),
61 Http(StatusCode),
62 Other(String),
63}
64
65impl CategorizedError {
66 fn try_categorize<B, E>(res: &Result<http::Response<B>, E>) -> Self
67 where
68 E: DynError,
69 {
70 match res {
71 Ok(resp) => Self::Http(resp.status()),
72 Err(e) => Self::extract_nested(e.as_dyn_error()),
73 }
74 }
75
76 fn extract_nested(error: &(dyn std::error::Error + 'static)) -> Self {
77 if let Some(hyper_error) = error.downcast_ref::<hyper_util::client::legacy::Error>() {
79 return match hyper_error.source() {
80 Some(source) => Self::extract_nested(source),
81 None => Self::Client(hyper_error.to_string()),
82 };
83 }
84
85 if let Some(rustls_error) = error.downcast_ref::<rustls::Error>() {
87 return Self::from_rustls(rustls_error);
88 }
89
90 if let Some(io_error) = error.downcast_ref::<std::io::Error>() {
94 return match io_error.get_ref() {
95 Some(source) => Self::extract_nested(source),
96 None => Self::Other(io_error.to_string()),
97 };
98 }
99
100 Self::Other(error.to_string())
101 }
102
103 fn from_rustls(error: &rustls::Error) -> Self {
104 let reason = match error {
107 rustls::Error::InvalidCertificate(cert_error) => format!(
108 "peer certificate is invalid: {}",
109 rustls_cert_error_to_string(cert_error)
110 ),
111 _ => error.to_string(),
112 };
113
114 Self::Tls(reason)
115 }
116}
117
118impl fmt::Display for CategorizedError {
119 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120 match self {
121 CategorizedError::Client(reason) => write!(f, "Request failed due to a client error: {}", reason),
122 CategorizedError::Tls(reason) => write!(f, "Request failed due to a TLS error: {}", reason),
123 CategorizedError::Http(status_code) => write!(
124 f,
125 "Server responded with non-success status code {}.",
126 status_code.as_str()
127 ),
128 CategorizedError::Other(reason) => write!(f, "Request failed: {}", reason),
129 }
130 }
131}
132
133fn rustls_cert_error_to_string(cert_error: &rustls::CertificateError) -> Cow<'static, str> {
134 match cert_error {
135 rustls::CertificateError::BadEncoding => "certificate incorrectly encoded".into(),
136 rustls::CertificateError::Expired => "certificate expired (current time is after notAfter time)".into(),
137 rustls::CertificateError::NotValidYet => {
138 "certificate not valid yet (current time is before notBefore time)".into()
139 }
140 rustls::CertificateError::Revoked => "certificate has been revoked".into(),
141 rustls::CertificateError::UnhandledCriticalExtension => {
142 "certificate contains an extension marked critical, but it was not processed by the certificate validator"
143 .into()
144 }
145 rustls::CertificateError::UnknownIssuer => "certificate chain is not issued by a known root certificate".into(),
146 rustls::CertificateError::UnknownRevocationStatus => {
147 "certificate's revocation status could not be determined".into()
148 }
149 rustls::CertificateError::ExpiredRevocationList => {
150 "certificate's revocation status could not be determined due to an expired CRL".into()
151 }
152 rustls::CertificateError::BadSignature => {
153 "certificate is not signed correctly by the key of its alleged issuer".into()
154 }
155 rustls::CertificateError::NotValidForName => "certificate is not valid for the given entity name".into(),
156 rustls::CertificateError::InvalidPurpose => "certificate is not valid for the requested purpose".into(),
157 rustls::CertificateError::ApplicationVerificationFailure => {
158 "certificate is valid overall, but the handshake was rejected".into()
159 }
160
161 rustls::CertificateError::Other(other) => format!("generic error: {}", other).into(),
165 other => format!("generic unhandled error: {:?}", other).into(),
166 }
167}
168
169trait DynError {
171 fn as_dyn_error(&self) -> &(dyn std::error::Error + 'static);
172}
173
174impl DynError for Box<dyn std::error::Error + Send + Sync> {
175 fn as_dyn_error(&self) -> &(dyn std::error::Error + 'static) {
176 &**self
177 }
178}