Skip to main content

leyline/core/error/
classify.rs

1use std::io;
2
3use super::{Error, ErrorCategory, Kind};
4use crate::core::retry::is_gateway_status;
5use crate::core::session::decompress::BodyLimit;
6use crate::h2::H2Error;
7use crate::h2::error::ErrorCode;
8use crate::tls::TlsError;
9use crate::tls::error::ProxyReply;
10
11impl Error {
12    pub fn is_timeout(&self) -> bool {
13        self.inner.kind == Kind::Timeout
14            || self
15                .io_cause()
16                .is_some_and(|e| e.kind() == io::ErrorKind::TimedOut)
17    }
18
19    pub fn is_connect(&self) -> bool {
20        match self.category() {
21            ErrorCategory::Connect | ErrorCategory::Dns => true,
22            ErrorCategory::Tls => self.tls().is_some_and(TlsError::is_connect_phase),
23            ErrorCategory::Timeout => {
24                self.inner.kind == Kind::Connect
25                    || self.tls().is_some_and(TlsError::is_connect_phase)
26            }
27            _ => false,
28        }
29    }
30
31    pub(super) fn is_socket_unreachable(&self) -> bool {
32        self.io().is_some_and(|e| {
33            matches!(
34                e.kind(),
35                io::ErrorKind::ConnectionRefused
36                    | io::ErrorKind::AddrNotAvailable
37                    | io::ErrorKind::NotConnected
38                    | io::ErrorKind::NetworkUnreachable
39            )
40        })
41    }
42
43    pub fn is_status(&self) -> bool {
44        self.inner.kind == Kind::Status
45    }
46
47    pub fn is_retryable(&self) -> bool {
48        self.is_timeout()
49            || self.is_connect()
50            || self.is_connection_closed()
51            || self.is_proxy_transient()
52    }
53
54    fn is_proxy_transient(&self) -> bool {
55        match self.tls() {
56            Some(TlsError::Proxy { status, source, .. }) => {
57                source.is_some() || status.is_some_and(is_gateway_status)
58            }
59            Some(TlsError::ProxyTargetUnreachable {
60                reply: ProxyReply::HttpStatus(code),
61                ..
62            }) => is_gateway_status(*code),
63            _ => false,
64        }
65    }
66
67    pub(crate) fn is_connection_closed(&self) -> bool {
68        if self.io().is_some_and(|e| {
69            matches!(
70                e.kind(),
71                io::ErrorKind::UnexpectedEof
72                    | io::ErrorKind::ConnectionReset
73                    | io::ErrorKind::ConnectionAborted
74                    | io::ErrorKind::BrokenPipe
75            )
76        }) {
77            return true;
78        }
79        if matches!(
80            self.h2(),
81            Some(
82                H2Error::Io(_)
83                    | H2Error::Connection {
84                        code: ErrorCode::NoError,
85                        ..
86                    }
87            )
88        ) || self.is_refused_stream()
89        {
90            return true;
91        }
92        self.tls().is_some_and(TlsError::is_handshake)
93    }
94
95    pub(crate) fn is_refused_stream(&self) -> bool {
96        matches!(
97            self.h2(),
98            Some(H2Error::Stream {
99                code: ErrorCode::RefusedStream,
100                ..
101            })
102        ) || self.not_processed()
103    }
104
105    pub fn is_body_limit(&self) -> bool {
106        self.body_limit().is_some()
107    }
108
109    pub(crate) fn body_limit(&self) -> Option<BodyLimit> {
110        self.source_as::<BodyLimit>()
111            .copied()
112            .or_else(|| self.io().and_then(BodyLimit::of_io))
113    }
114
115    pub fn is_proxy(&self) -> bool {
116        matches!(self.tls(), Some(TlsError::Proxy { .. }))
117    }
118
119    fn io_cause(&self) -> Option<&io::Error> {
120        self.io()
121            .or_else(|| self.tls().and_then(TlsError::io_source))
122    }
123}