weixin_agent/util/
net_error.rs1use crate::error::Error;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum NetErrorKind {
21 Dns,
23 Tcp,
25 Tls,
27 Timeout,
29 Unknown,
31}
32
33impl NetErrorKind {
34 pub fn description(self) -> &'static str {
36 match self {
37 Self::Dns => "DNS resolution failed — check the resolver and the API host name",
38 Self::Tcp => "TCP connection failed — check network reachability and egress rules",
39 Self::Tls => "TLS handshake failed — check the certificate chain and system time",
40 Self::Timeout => "request timed out on the client side",
41 Self::Unknown => "unclassified transport failure",
42 }
43 }
44
45 pub fn as_str(self) -> &'static str {
47 match self {
48 Self::Dns => "dns",
49 Self::Tcp => "tcp",
50 Self::Tls => "tls",
51 Self::Timeout => "timeout",
52 Self::Unknown => "unknown",
53 }
54 }
55}
56
57fn source_chain_text(err: &(dyn std::error::Error + 'static)) -> String {
61 let mut text = err.to_string().to_lowercase();
62 let mut current = err.source();
63 while let Some(source) = current {
64 text.push(' ');
65 text.push_str(&source.to_string().to_lowercase());
66 current = source.source();
67 }
68 text
69}
70
71pub(crate) fn classify(err: &Error) -> NetErrorKind {
73 if let Error::Http(http_err) = err {
74 if http_err.is_timeout() {
75 return NetErrorKind::Timeout;
76 }
77 }
78 if let Error::Timeout(_) = err {
79 return NetErrorKind::Timeout;
80 }
81
82 let text = source_chain_text(err);
83 if text.contains("dns error")
84 || text.contains("failed to lookup address")
85 || text.contains("name or service not known")
86 || text.contains("nodename nor servname")
87 || text.contains("enotfound")
88 {
89 return NetErrorKind::Dns;
90 }
91 if text.contains("invalid peer certificate")
92 || text.contains("certificate")
93 || text.contains("tls")
94 || text.contains("handshake")
95 {
96 return NetErrorKind::Tls;
97 }
98 if text.contains("connection refused")
99 || text.contains("connection reset")
100 || text.contains("unreachable")
101 || text.contains("connect timed out")
102 || text.contains("econnrefused")
103 {
104 return NetErrorKind::Tcp;
105 }
106 NetErrorKind::Unknown
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn io_error(msg: &str) -> Error {
116 Error::Io(std::io::Error::other(msg.to_owned()))
117 }
118
119 #[test]
120 fn classifies_dns_failure() {
121 assert_eq!(
122 classify(&io_error("failed to lookup address information")),
123 NetErrorKind::Dns
124 );
125 assert_eq!(
126 classify(&io_error("dns error: no records")),
127 NetErrorKind::Dns
128 );
129 }
130
131 #[test]
132 fn classifies_connection_refused_as_tcp() {
133 assert_eq!(
134 classify(&io_error(
135 "tcp connect error: Connection refused (os error 61)"
136 )),
137 NetErrorKind::Tcp
138 );
139 assert_eq!(
140 classify(&io_error("network is unreachable")),
141 NetErrorKind::Tcp
142 );
143 }
144
145 #[test]
146 fn classifies_certificate_failure_as_tls() {
147 assert_eq!(
148 classify(&io_error("invalid peer certificate: Expired")),
149 NetErrorKind::Tls
150 );
151 }
152
153 #[test]
154 fn classifies_unmatched_as_unknown() {
155 assert_eq!(
156 classify(&io_error("something odd happened")),
157 NetErrorKind::Unknown
158 );
159 assert_eq!(
161 classify(&Error::Timeout("waited too long".into())),
162 NetErrorKind::Timeout
163 );
164 }
165
166 #[test]
167 fn descriptions_are_non_empty() {
168 for kind in [
169 NetErrorKind::Dns,
170 NetErrorKind::Tcp,
171 NetErrorKind::Tls,
172 NetErrorKind::Timeout,
173 NetErrorKind::Unknown,
174 ] {
175 assert!(!kind.description().is_empty());
176 assert!(!kind.as_str().is_empty());
177 }
178 }
179}