Skip to main content

net_io/
error.rs

1//! Errors, close reasons, and the small enums they carry.
2
3use std::fmt;
4use std::io;
5use std::net::SocketAddr;
6use std::path::PathBuf;
7use std::time::Duration;
8
9use crate::addr::AddrError;
10use crate::driver::DriverKind;
11use crate::handoff::HandoffError;
12use crate::inherit::InheritError;
13use crate::proxy::ProxyError;
14use crate::tls::TlsError;
15
16/// The one error type of the crate.
17///
18/// Every variant's `Display` says what happened and what to do about it.
19///
20/// # Examples
21///
22/// ```no_run
23/// use net_io::{ErrorClass, NetError};
24///
25/// match net_io::connect("127.0.0.1:9") {
26///     Ok(_) => {}
27///     Err(NetError::ConnectionRefused { addr }) => eprintln!("nothing listens on {addr}"),
28///     Err(e) if e.class() == ErrorClass::Timeout => eprintln!("slow network: {e}"),
29///     Err(e) => return Err(e),
30/// }
31/// # Ok::<(), NetError>(())
32/// ```
33#[non_exhaustive]
34#[derive(Debug)]
35pub enum NetError {
36    /// Binding failed for a reason without its own variant; `source` has the OS error.
37    Bind {
38        /// The address that could not be bound.
39        addr: SocketAddr,
40        /// The OS error.
41        source: io::Error,
42    },
43    /// Another socket holds the address.
44    AddrInUse {
45        /// The address in use.
46        addr: SocketAddr,
47    },
48    /// The address is not on this host, or IPv6 is disabled.
49    AddrNotAvailable {
50        /// The address that is not available.
51        addr: SocketAddr,
52    },
53    /// The OS refused the operation for lack of permission.
54    PermissionDenied {
55        /// What was refused.
56        what: &'static str,
57    },
58    /// Name resolution returned no address.
59    NoAddresses,
60    /// No answer from the peer within the connect deadline.
61    ConnectTimeout {
62        /// The address that did not answer.
63        addr: SocketAddr,
64        /// How long the attempt waited.
65        after: Duration,
66    },
67    /// Nothing listens at the address.
68    ConnectionRefused {
69        /// The address that refused.
70        addr: SocketAddr,
71    },
72    /// No route to the host.
73    HostUnreachable {
74        /// The unreachable address.
75        addr: SocketAddr,
76    },
77    /// No route to the network.
78    NetworkUnreachable {
79        /// The unreachable address.
80        addr: SocketAddr,
81    },
82    /// One of net-io's deadlines passed.
83    TimedOut {
84        /// Which deadline.
85        kind: TimeoutKind,
86    },
87    /// The peer reset the connection.
88    ConnectionReset,
89    /// A write after the peer closed the connection.
90    BrokenPipe,
91    /// The connection was already closed by this side or the peer.
92    Closed,
93    /// A kill handle closed the connection.
94    Killed {
95        /// The caller's code.
96        reason: KillReason,
97    },
98    /// The gate denied a write; the connection was reset.
99    EgressDenied {
100        /// The gate's code.
101        code: u16,
102    },
103    /// The connection was refused.
104    Refused {
105        /// Why.
106        cause: RefusalCause,
107    },
108    /// A configured bound was hit.
109    Limit {
110        /// Which bound.
111        which: LimitKind,
112    },
113    /// The minimum transfer rate was not met.
114    SlowPeer {
115        /// In which direction.
116        direction: Direction,
117    },
118    /// A builder or option value is out of range or inconsistent.
119    InvalidConfig {
120        /// The setter or field that holds the value.
121        field: &'static str,
122        /// The rule it broke.
123        reason: ConfigReason,
124    },
125    /// The platform lacks the feature.
126    Unsupported {
127        /// The feature.
128        feature: &'static str,
129        /// The platform.
130        platform: &'static str,
131    },
132    /// A Unix socket path exceeds the platform limit.
133    PathTooLong {
134        /// The limit in bytes, including the terminator.
135        max: usize,
136    },
137    /// A live listener owns the socket path, or the path is not a socket.
138    StaleSocketInUse {
139        /// The path.
140        path: PathBuf,
141    },
142    /// Another process created the named pipe first.
143    PipeNameInUse {
144        /// The pipe name.
145        name: String,
146    },
147    /// No local endpoint with that name.
148    NotFound,
149    /// A file shrank while it was being sent.
150    FileTruncated {
151        /// Bytes sent before the file ended.
152        sent: u64,
153    },
154    /// An address, CIDR block, or trusted-proxy list did not parse.
155    Address(AddrError),
156    /// A PROXY protocol header was invalid.
157    Proxy(ProxyError),
158    /// The TLS engine failed.
159    Tls(TlsError),
160    /// Inherited descriptors failed verification.
161    Inherit(InheritError),
162    /// A listener hand-off failed; nothing was adopted.
163    Handoff(HandoffError),
164    /// The selected driver could not start.
165    DriverSetup {
166        /// The driver that failed.
167        driver: DriverKind,
168        /// The OS error.
169        source: io::Error,
170    },
171    /// The operation needs the door open.
172    DoorClosed,
173    /// The server is draining or stopped.
174    ShuttingDown,
175    /// A server was used in a process created by `fork` after it was built.
176    ForkedChild,
177    /// Any other OS failure.
178    Os {
179        /// The operation that failed.
180        op: &'static str,
181        /// The OS error.
182        source: io::Error,
183    },
184}
185
186/// The broad kind of a [`NetError`], for policy decisions.
187///
188/// # Examples
189///
190/// ```
191/// use net_io::{ErrorClass, NetError};
192/// assert_eq!(NetError::ConnectionReset.class(), ErrorClass::Peer);
193/// ```
194#[non_exhaustive]
195#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
196pub enum ErrorClass {
197    /// Fix the configuration.
198    Config,
199    /// Descriptors, memory, or buffers ran out; retry later.
200    Resource,
201    /// The other side misbehaved or left.
202    Peer,
203    /// A deadline passed.
204    Timeout,
205    /// The gate, a limit, or a refusal stopped it.
206    Policy,
207    /// The platform lacks the feature.
208    Platform,
209    /// A contract was broken by the caller.
210    Bug,
211}
212
213impl NetError {
214    /// The broad kind of this error.
215    ///
216    /// ```
217    /// use net_io::{ErrorClass, NetError, TimeoutKind};
218    /// assert_eq!(NetError::TimedOut { kind: TimeoutKind::Read }.class(), ErrorClass::Timeout);
219    /// ```
220    #[must_use]
221    pub fn class(&self) -> ErrorClass {
222        match self {
223            NetError::InvalidConfig { .. }
224            | NetError::Address(_)
225            | NetError::AddrInUse { .. }
226            | NetError::AddrNotAvailable { .. }
227            | NetError::PermissionDenied { .. }
228            | NetError::NoAddresses
229            | NetError::PathTooLong { .. }
230            | NetError::StaleSocketInUse { .. }
231            | NetError::PipeNameInUse { .. }
232            | NetError::NotFound
233            | NetError::Bind { .. } => ErrorClass::Config,
234            NetError::Limit { which } => match which {
235                LimitKind::BufferBudget | LimitKind::Descriptors | LimitKind::CompletionQueue => {
236                    ErrorClass::Resource
237                }
238                _ => ErrorClass::Policy,
239            },
240            NetError::ConnectTimeout { .. } | NetError::TimedOut { .. } => ErrorClass::Timeout,
241            NetError::ConnectionRefused { .. }
242            | NetError::HostUnreachable { .. }
243            | NetError::NetworkUnreachable { .. }
244            | NetError::ConnectionReset
245            | NetError::BrokenPipe
246            | NetError::Closed
247            | NetError::FileTruncated { .. }
248            | NetError::Proxy(_)
249            | NetError::Tls(_)
250            | NetError::SlowPeer { .. } => ErrorClass::Peer,
251            NetError::Killed { .. }
252            | NetError::EgressDenied { .. }
253            | NetError::Refused { .. }
254            | NetError::DoorClosed
255            | NetError::ShuttingDown => ErrorClass::Policy,
256            NetError::Unsupported { .. } | NetError::DriverSetup { .. } => ErrorClass::Platform,
257            NetError::ForkedChild => ErrorClass::Bug,
258            NetError::Inherit(_) | NetError::Handoff(_) => ErrorClass::Config,
259            NetError::Os { source, .. } => class_of_io(source),
260        }
261    }
262
263    /// Whether retrying the same operation later can succeed.
264    ///
265    /// ```
266    /// use net_io::NetError;
267    /// assert!(!NetError::ConnectionReset.is_retryable());
268    /// ```
269    #[must_use]
270    pub fn is_retryable(&self) -> bool {
271        matches!(
272            self,
273            NetError::ConnectTimeout { .. }
274                | NetError::TimedOut {
275                    kind: TimeoutKind::Connect
276                }
277        ) || self.class() == ErrorClass::Resource
278    }
279
280    /// The raw OS error number, when the error came from the OS.
281    ///
282    /// ```
283    /// use net_io::NetError;
284    /// assert_eq!(NetError::Closed.os_error_code(), None);
285    /// ```
286    #[must_use]
287    pub fn os_error_code(&self) -> Option<i32> {
288        match self {
289            NetError::Bind { source, .. }
290            | NetError::DriverSetup { source, .. }
291            | NetError::Os { source, .. } => source.raw_os_error(),
292            _ => None,
293        }
294    }
295
296    /// Builds the error for a failed OS call, mapping the common kinds to their own variants.
297    pub(crate) fn from_os(op: &'static str, source: io::Error) -> NetError {
298        match source.kind() {
299            io::ErrorKind::ConnectionReset | io::ErrorKind::ConnectionAborted => {
300                NetError::ConnectionReset
301            }
302            io::ErrorKind::BrokenPipe => NetError::BrokenPipe,
303            io::ErrorKind::NotConnected => NetError::Closed,
304            _ => NetError::Os { op, source },
305        }
306    }
307}
308
309fn class_of_io(e: &io::Error) -> ErrorClass {
310    match e.kind() {
311        io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => ErrorClass::Timeout,
312        io::ErrorKind::OutOfMemory => ErrorClass::Resource,
313        io::ErrorKind::PermissionDenied | io::ErrorKind::InvalidInput => ErrorClass::Config,
314        io::ErrorKind::Unsupported => ErrorClass::Platform,
315        _ => ErrorClass::Peer,
316    }
317}
318
319impl fmt::Display for NetError {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        match self {
322            NetError::Bind { addr, source } => write!(f, "could not bind {addr}: {source}"),
323            NetError::AddrInUse { addr } => write!(
324                f,
325                "another socket is listening on {addr}; stop it or choose another port (on restart, use listener hand-off)"
326            ),
327            NetError::AddrNotAvailable { addr } => write!(
328                f,
329                "{addr} is not an address of this host (or IPv6 is disabled); bind an address this host owns"
330            ),
331            NetError::PermissionDenied { what } => write!(
332                f,
333                "permission denied: {what}; ports below 1024 need privileges: bind before dropping them, or use a high port behind a reverse proxy"
334            ),
335            NetError::NoAddresses => {
336                f.write_str("name resolution returned no address; check the host name")
337            }
338            NetError::ConnectTimeout { addr, after } => write!(
339                f,
340                "no answer from {addr} within {after:?}; check reachability or raise the connect timeout"
341            ),
342            NetError::ConnectionRefused { addr } => {
343                write!(f, "{addr} refused the connection; nothing listens there")
344            }
345            NetError::HostUnreachable { addr } => write!(f, "no route to host {addr}"),
346            NetError::NetworkUnreachable { addr } => write!(f, "no route to the network of {addr}"),
347            NetError::TimedOut { kind } => write!(
348                f,
349                "the {} deadline passed; the peer is too slow, or the limit is too low for this workload",
350                kind.as_str()
351            ),
352            NetError::ConnectionReset => f.write_str("the peer reset the connection"),
353            NetError::BrokenPipe => f.write_str("write after the peer closed the connection"),
354            NetError::Closed => f.write_str("the connection is already closed"),
355            NetError::Killed { reason } => {
356                write!(f, "the connection was killed (code {})", reason.0)
357            }
358            NetError::EgressDenied { code } => {
359                write!(
360                    f,
361                    "the gate denied a write (code {code}); the connection was reset"
362                )
363            }
364            NetError::Refused { cause } => {
365                write!(f, "the connection was refused: {}", cause.as_str())
366            }
367            NetError::Limit { which } => {
368                write!(
369                    f,
370                    "the {} limit was reached; raise it or shed load",
371                    which.as_str()
372                )
373            }
374            NetError::SlowPeer { direction } => write!(
375                f,
376                "the peer did not reach the minimum transfer rate ({})",
377                direction.as_str()
378            ),
379            NetError::InvalidConfig { field, reason } => {
380                write!(f, "invalid configuration for `{field}`: {reason}")
381            }
382            NetError::Unsupported { feature, platform } => {
383                write!(f, "{feature} is not supported on {platform}")
384            }
385            NetError::PathTooLong { max } => write!(
386                f,
387                "the socket path is longer than the platform allows ({max} bytes including the terminator); use a shorter path"
388            ),
389            NetError::StaleSocketInUse { path } => write!(
390                f,
391                "{} is in use by a live listener or is not a socket; stop that listener or choose another path",
392                path.display()
393            ),
394            NetError::PipeNameInUse { name } => write!(
395                f,
396                "another process created the pipe {name} first (possible squatting); choose another name or find that process"
397            ),
398            NetError::NotFound => {
399                f.write_str("no local endpoint with that name; start the server first")
400            }
401            NetError::FileTruncated { sent } => write!(
402                f,
403                "the file ended after {sent} bytes while it was being sent; the connection was reset"
404            ),
405            NetError::Address(e) => write!(f, "{e}"),
406            NetError::Proxy(e) => write!(f, "PROXY protocol: {e}"),
407            NetError::Tls(e) => write!(f, "TLS: {e}"),
408            NetError::Inherit(e) => write!(f, "inherited listeners: {e}"),
409            NetError::Handoff(e) => write!(f, "listener hand-off: {e}"),
410            NetError::DriverSetup { driver, source } => write!(
411                f,
412                "the {driver:?} driver could not start: {source}; use DriverKind::Auto or the blocking driver"
413            ),
414            NetError::DoorClosed => f.write_str("the door is closed; call open_door first"),
415            NetError::ShuttingDown => f.write_str("the server is draining or stopped"),
416            NetError::ForkedChild => f.write_str(
417                "a server was used in a child process after fork; create servers after forking",
418            ),
419            NetError::Os { op, source } => write!(f, "{op} failed: {source}"),
420        }
421    }
422}
423
424impl std::error::Error for NetError {
425    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
426        match self {
427            NetError::Bind { source, .. }
428            | NetError::DriverSetup { source, .. }
429            | NetError::Os { source, .. } => Some(source),
430            NetError::Address(e) => Some(e),
431            NetError::Proxy(e) => Some(e),
432            NetError::Tls(e) => Some(e),
433            NetError::Inherit(e) => Some(e),
434            NetError::Handoff(e) => Some(e),
435            _ => None,
436        }
437    }
438}
439
440impl From<io::Error> for NetError {
441    fn from(e: io::Error) -> NetError {
442        match e.kind() {
443            io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock => NetError::TimedOut {
444                kind: TimeoutKind::Read,
445            },
446            _ => NetError::from_os("io", e),
447        }
448    }
449}
450
451impl From<AddrError> for NetError {
452    fn from(e: AddrError) -> NetError {
453        NetError::Address(e)
454    }
455}
456
457impl From<NetError> for io::Error {
458    fn from(e: NetError) -> io::Error {
459        let kind = match &e {
460            NetError::TimedOut { .. }
461            | NetError::ConnectTimeout { .. }
462            | NetError::SlowPeer { .. } => io::ErrorKind::TimedOut,
463            NetError::ConnectionReset | NetError::Killed { .. } | NetError::EgressDenied { .. } => {
464                io::ErrorKind::ConnectionReset
465            }
466            NetError::BrokenPipe => io::ErrorKind::BrokenPipe,
467            NetError::Closed => io::ErrorKind::NotConnected,
468            NetError::ConnectionRefused { .. } => io::ErrorKind::ConnectionRefused,
469            NetError::AddrInUse { .. } => io::ErrorKind::AddrInUse,
470            NetError::AddrNotAvailable { .. } => io::ErrorKind::AddrNotAvailable,
471            NetError::PermissionDenied { .. } => io::ErrorKind::PermissionDenied,
472            NetError::NotFound => io::ErrorKind::NotFound,
473            NetError::Unsupported { .. } => io::ErrorKind::Unsupported,
474            NetError::InvalidConfig { .. } | NetError::Address(_) => io::ErrorKind::InvalidInput,
475            _ => io::ErrorKind::Other,
476        };
477        io::Error::new(kind, e)
478    }
479}
480
481/// Why a connection closed. Every connection ends with exactly one.
482///
483/// # Examples
484///
485/// ```
486/// use net_io::{CloseReason, Direction};
487/// let reason = CloseReason::SlowPeer { direction: Direction::In };
488/// assert_eq!(reason.as_str(), "slow_peer_in");
489/// assert!(reason.is_abortive());
490/// assert!(!CloseReason::PeerClosed.is_abortive());
491/// ```
492#[non_exhaustive]
493#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
494pub enum CloseReason {
495    /// The peer closed its side and the session closed.
496    PeerClosed,
497    /// The peer sent RST.
498    PeerReset,
499    /// No exchange started within the idle timeout.
500    Idle,
501    /// The PROXY header and TLS handshake did not finish within the handshake timeout.
502    HandshakeTimeout,
503    /// No progress within the read timeout.
504    ReadTimeout,
505    /// No progress within the write timeout.
506    WriteTimeout,
507    /// The session's deadline for the current exchange passed.
508    ExchangeDeadline,
509    /// A parked connection was not woken in time.
510    ParkDeadline,
511    /// The minimum transfer rate was not met.
512    SlowPeer {
513        /// In which direction.
514        direction: Direction,
515    },
516    /// Refused at accept or by the session factory.
517    Refused(RefusalCause),
518    /// A PROXY-required listener got a connection from an untrusted peer.
519    ProxyUntrusted,
520    /// A trusted peer sent no PROXY header.
521    ProxyMissing,
522    /// The PROXY header was malformed or too large.
523    ProxyInvalid,
524    /// The TLS engine failed.
525    TlsFailed,
526    /// The TLS engine's hello hook refused the ClientHello.
527    HelloRefused,
528    /// A kill handle closed the connection.
529    Killed(KillReason),
530    /// The gate denied a write.
531    EgressDenied(u16),
532    /// The session finished after drain started.
533    Drained,
534    /// The session had not finished when the drain deadline passed.
535    DrainDeadline,
536    /// The control line shed this lane.
537    Shed,
538    /// The control line said `Stopping`, or the server shut down.
539    Shutdown,
540    /// The session returned `Step::Close`.
541    SessionClosed,
542    /// The session returned `Step::Abort`, or a handler returned an error.
543    SessionError,
544    /// The session panicked.
545    SessionPanicked,
546    /// A per-connection bound was hit.
547    Limit(LimitKind),
548    /// The OS reported an error not covered above.
549    Io(OsErrorClass),
550}
551
552impl CloseReason {
553    /// A stable snake_case name for logs.
554    ///
555    /// ```
556    /// assert_eq!(net_io::CloseReason::Idle.as_str(), "idle");
557    /// ```
558    #[must_use]
559    pub fn as_str(&self) -> &'static str {
560        match self {
561            CloseReason::PeerClosed => "peer_closed",
562            CloseReason::PeerReset => "peer_reset",
563            CloseReason::Idle => "idle",
564            CloseReason::HandshakeTimeout => "handshake_timeout",
565            CloseReason::ReadTimeout => "read_timeout",
566            CloseReason::WriteTimeout => "write_timeout",
567            CloseReason::ExchangeDeadline => "exchange_deadline",
568            CloseReason::ParkDeadline => "park_deadline",
569            CloseReason::SlowPeer {
570                direction: Direction::In,
571            } => "slow_peer_in",
572            CloseReason::SlowPeer {
573                direction: Direction::Out,
574            } => "slow_peer_out",
575            CloseReason::Refused(_) => "refused",
576            CloseReason::ProxyUntrusted => "proxy_untrusted",
577            CloseReason::ProxyMissing => "proxy_missing",
578            CloseReason::ProxyInvalid => "proxy_invalid",
579            CloseReason::TlsFailed => "tls_failed",
580            CloseReason::HelloRefused => "hello_refused",
581            CloseReason::Killed(_) => "killed",
582            CloseReason::EgressDenied(_) => "egress_denied",
583            CloseReason::Drained => "drained",
584            CloseReason::DrainDeadline => "drain_deadline",
585            CloseReason::Shed => "shed",
586            CloseReason::Shutdown => "shutdown",
587            CloseReason::SessionClosed => "session_closed",
588            CloseReason::SessionError => "session_error",
589            CloseReason::SessionPanicked => "session_panicked",
590            CloseReason::Limit(_) => "limit",
591            CloseReason::Io(_) => "io",
592        }
593    }
594
595    /// Whether the connection is closed with RST (`true`) or gracefully with FIN (`false`).
596    ///
597    /// ```
598    /// assert!(net_io::CloseReason::ReadTimeout.is_abortive());
599    /// ```
600    #[must_use]
601    pub fn is_abortive(&self) -> bool {
602        !matches!(
603            self,
604            CloseReason::PeerClosed
605                | CloseReason::Idle
606                | CloseReason::Drained
607                | CloseReason::SessionClosed
608        )
609    }
610
611    /// An index per variant for per-reason counters (inner values ignored).
612    pub(crate) fn index(&self) -> usize {
613        match self {
614            CloseReason::PeerClosed => 0,
615            CloseReason::PeerReset => 1,
616            CloseReason::Idle => 2,
617            CloseReason::HandshakeTimeout => 3,
618            CloseReason::ReadTimeout => 4,
619            CloseReason::WriteTimeout => 5,
620            CloseReason::ExchangeDeadline => 6,
621            CloseReason::ParkDeadline => 7,
622            CloseReason::SlowPeer { .. } => 8,
623            CloseReason::Refused(_) => 9,
624            CloseReason::ProxyUntrusted => 10,
625            CloseReason::ProxyMissing => 11,
626            CloseReason::ProxyInvalid => 12,
627            CloseReason::TlsFailed => 13,
628            CloseReason::HelloRefused => 14,
629            CloseReason::Killed(_) => 15,
630            CloseReason::EgressDenied(_) => 16,
631            CloseReason::Drained => 17,
632            CloseReason::DrainDeadline => 18,
633            CloseReason::Shed => 19,
634            CloseReason::Shutdown => 20,
635            CloseReason::SessionClosed => 21,
636            CloseReason::SessionError => 22,
637            CloseReason::SessionPanicked => 23,
638            CloseReason::Limit(_) => 24,
639            CloseReason::Io(_) => 25,
640        }
641    }
642
643    /// The number of distinct indexes [`CloseReason::index`] returns.
644    pub(crate) const COUNT: usize = 26;
645
646    /// The close reason for a timeout of this kind.
647    pub(crate) fn for_timeout(kind: TimeoutKind) -> CloseReason {
648        match kind {
649            TimeoutKind::Handshake => CloseReason::HandshakeTimeout,
650            TimeoutKind::Idle => CloseReason::Idle,
651            TimeoutKind::Read | TimeoutKind::Connect => CloseReason::ReadTimeout,
652            TimeoutKind::Write => CloseReason::WriteTimeout,
653            TimeoutKind::Exchange => CloseReason::ExchangeDeadline,
654            TimeoutKind::Park => CloseReason::ParkDeadline,
655            TimeoutKind::Drain => CloseReason::DrainDeadline,
656        }
657    }
658}
659
660impl fmt::Display for CloseReason {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        f.write_str(self.as_str())
663    }
664}
665
666/// Why a connection was refused.
667///
668/// ```
669/// assert_eq!(net_io::RefusalCause::SourceCap.as_str(), "source_cap");
670/// ```
671#[non_exhaustive]
672#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
673pub enum RefusalCause {
674    /// The global connection cap was reached.
675    GlobalCap,
676    /// The listener's connection cap was reached.
677    ListenerCap,
678    /// The per-source concurrency cap was reached.
679    SourceCap,
680    /// The lane's reservation was used up.
681    LaneReserve,
682    /// The gate refused, with its code.
683    Gate(u16),
684    /// The session factory refused.
685    Session,
686    /// The blocking driver had no free worker.
687    Busy,
688    /// The door was closed.
689    DoorClosed,
690    /// The control line shed this lane.
691    Shed,
692    /// The control line is in lockdown and the listener does not allow it.
693    Lockdown,
694}
695
696impl RefusalCause {
697    /// A stable snake_case name for logs.
698    #[must_use]
699    pub fn as_str(&self) -> &'static str {
700        match self {
701            RefusalCause::GlobalCap => "global_cap",
702            RefusalCause::ListenerCap => "listener_cap",
703            RefusalCause::SourceCap => "source_cap",
704            RefusalCause::LaneReserve => "lane_reserve",
705            RefusalCause::Gate(_) => "gate",
706            RefusalCause::Session => "session",
707            RefusalCause::Busy => "busy",
708            RefusalCause::DoorClosed => "door_closed",
709            RefusalCause::Shed => "shed",
710            RefusalCause::Lockdown => "lockdown",
711        }
712    }
713
714    pub(crate) fn index(&self) -> usize {
715        match self {
716            RefusalCause::GlobalCap => 0,
717            RefusalCause::ListenerCap => 1,
718            RefusalCause::SourceCap => 2,
719            RefusalCause::LaneReserve => 3,
720            RefusalCause::Gate(_) => 4,
721            RefusalCause::Session => 5,
722            RefusalCause::Busy => 6,
723            RefusalCause::DoorClosed => 7,
724            RefusalCause::Shed => 8,
725            RefusalCause::Lockdown => 9,
726        }
727    }
728
729    pub(crate) const COUNT: usize = 10;
730}
731
732/// Which configured bound was hit.
733///
734/// ```
735/// assert_eq!(net_io::LimitKind::OutputBudget.as_str(), "output_budget");
736/// ```
737#[non_exhaustive]
738#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
739pub enum LimitKind {
740    /// The global connection cap.
741    Connections,
742    /// A listener's connection cap.
743    PerListener,
744    /// The per-source concurrency cap.
745    PerSource,
746    /// The total buffer budget.
747    BufferBudget,
748    /// A connection's output budget.
749    OutputBudget,
750    /// A worker's completion queue.
751    CompletionQueue,
752    /// The PROXY header size.
753    ProxyHeader,
754    /// The accept batch.
755    AcceptBatch,
756    /// A connection's read buffer (full and not consumed).
757    ReadBuffer,
758    /// The UDP amplification guard.
759    Amplification,
760    /// A UDP source budget.
761    SourceBudget,
762    /// The process's descriptor limit.
763    Descriptors,
764}
765
766impl LimitKind {
767    /// A stable snake_case name for logs.
768    #[must_use]
769    pub fn as_str(&self) -> &'static str {
770        match self {
771            LimitKind::Connections => "connections",
772            LimitKind::PerListener => "per_listener",
773            LimitKind::PerSource => "per_source",
774            LimitKind::BufferBudget => "buffer_budget",
775            LimitKind::OutputBudget => "output_budget",
776            LimitKind::CompletionQueue => "completion_queue",
777            LimitKind::ProxyHeader => "proxy_header",
778            LimitKind::AcceptBatch => "accept_batch",
779            LimitKind::ReadBuffer => "read_buffer",
780            LimitKind::Amplification => "amplification",
781            LimitKind::SourceBudget => "source_budget",
782            LimitKind::Descriptors => "descriptors",
783        }
784    }
785}
786
787/// Which deadline passed.
788///
789/// ```
790/// assert_eq!(net_io::TimeoutKind::Idle.as_str(), "idle");
791/// ```
792#[non_exhaustive]
793#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
794pub enum TimeoutKind {
795    /// The handshake (PROXY and TLS).
796    Handshake,
797    /// Waiting for the next exchange.
798    Idle,
799    /// Waiting for bytes during an exchange.
800    Read,
801    /// Waiting for the peer to take bytes.
802    Write,
803    /// The session's total deadline for the exchange.
804    Exchange,
805    /// A parked connection.
806    Park,
807    /// An outbound connect.
808    Connect,
809    /// A drain.
810    Drain,
811}
812
813impl TimeoutKind {
814    /// A stable snake_case name for logs.
815    #[must_use]
816    pub fn as_str(&self) -> &'static str {
817        match self {
818            TimeoutKind::Handshake => "handshake",
819            TimeoutKind::Idle => "idle",
820            TimeoutKind::Read => "read",
821            TimeoutKind::Write => "write",
822            TimeoutKind::Exchange => "exchange",
823            TimeoutKind::Park => "park",
824            TimeoutKind::Connect => "connect",
825            TimeoutKind::Drain => "drain",
826        }
827    }
828}
829
830/// A caller-defined code for a kill.
831///
832/// ```
833/// let reason = net_io::KillReason(7);
834/// assert_eq!(reason.0, 7);
835/// ```
836#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
837pub struct KillReason(pub u16);
838
839/// A direction of transfer.
840///
841/// ```
842/// assert_eq!(net_io::Direction::In.as_str(), "in");
843/// ```
844#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
845pub enum Direction {
846    /// From the peer to us.
847    In,
848    /// From us to the peer.
849    Out,
850}
851
852impl Direction {
853    /// `"in"` or `"out"`.
854    #[must_use]
855    pub fn as_str(&self) -> &'static str {
856        match self {
857            Direction::In => "in",
858            Direction::Out => "out",
859        }
860    }
861}
862
863/// The class of an OS error, without its message text.
864///
865/// ```
866/// use net_io::OsErrorClass;
867/// let class = OsErrorClass::from_io(&std::io::Error::from(std::io::ErrorKind::ConnectionReset));
868/// assert_eq!(class, OsErrorClass::Reset);
869/// ```
870#[non_exhaustive]
871#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
872pub enum OsErrorClass {
873    /// The peer reset the connection.
874    Reset,
875    /// The connection was aborted locally.
876    Aborted,
877    /// The OS timed the connection out (for example failed keepalive probes).
878    TimedOut,
879    /// The connection was refused.
880    Refused,
881    /// No route.
882    Unreachable,
883    /// A file ended while it was being sent.
884    FileTruncated,
885    /// Any other OS error, by number.
886    Other(i32),
887}
888
889impl OsErrorClass {
890    /// Classifies an `io::Error`.
891    #[must_use]
892    pub fn from_io(e: &io::Error) -> OsErrorClass {
893        match e.kind() {
894            io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe => OsErrorClass::Reset,
895            io::ErrorKind::ConnectionAborted => OsErrorClass::Aborted,
896            io::ErrorKind::TimedOut => OsErrorClass::TimedOut,
897            io::ErrorKind::ConnectionRefused => OsErrorClass::Refused,
898            io::ErrorKind::HostUnreachable | io::ErrorKind::NetworkUnreachable => {
899                OsErrorClass::Unreachable
900            }
901            _ => OsErrorClass::Other(e.raw_os_error().unwrap_or(0)),
902        }
903    }
904}
905
906/// The rule a configuration value broke.
907///
908/// ```
909/// use net_io::ConfigReason;
910/// assert_eq!(ConfigReason::Zero.to_string(), "zero is not allowed here");
911/// ```
912#[non_exhaustive]
913#[derive(Clone, Copy, Debug, PartialEq, Eq)]
914pub enum ConfigReason {
915    /// Zero where zero is meaningless.
916    Zero,
917    /// Above the maximum.
918    TooLarge {
919        /// The maximum.
920        max: u64,
921    },
922    /// Below the minimum.
923    TooSmall {
924        /// The minimum.
925        min: u64,
926    },
927    /// A per-listener or per-source cap above the global cap.
928    AboveGlobal,
929    /// A deadline shorter than one timer tick.
930    BelowTick,
931    /// The minimum-rate window is shorter than its check interval.
932    WindowBelowCheck,
933    /// The buffer budget is below workers times the largest class times two.
934    BudgetBelowWorkers {
935        /// The smallest valid budget in bytes.
936        need: u64,
937    },
938    /// The caps exceed the process's descriptor limit.
939    DescriptorLimit {
940        /// The descriptor limit.
941        have: u64,
942        /// Descriptors the configuration needs.
943        need: u64,
944    },
945    /// A required part is not set.
946    Missing,
947    /// An adopted socket is not a listening socket.
948    NotListening,
949    /// Values that contradict each other.
950    Inconsistent,
951}
952
953impl fmt::Display for ConfigReason {
954    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
955        match self {
956            ConfigReason::Zero => f.write_str("zero is not allowed here"),
957            ConfigReason::TooLarge { max } => write!(f, "above the maximum of {max}"),
958            ConfigReason::TooSmall { min } => write!(f, "below the minimum of {min}"),
959            ConfigReason::AboveGlobal => f.write_str("above the global connection cap"),
960            ConfigReason::BelowTick => f.write_str("shorter than one timer tick"),
961            ConfigReason::WindowBelowCheck => {
962                f.write_str("the minimum-rate window is shorter than its check interval")
963            }
964            ConfigReason::BudgetBelowWorkers { need } => write!(
965                f,
966                "the buffer budget must be at least {need} bytes (workers x largest class x 2)"
967            ),
968            ConfigReason::DescriptorLimit { have, need } => write!(
969                f,
970                "needs {need} descriptors but the process limit is {have}; raise the limit or lower the caps"
971            ),
972            ConfigReason::Missing => f.write_str("a required part is not set"),
973            ConfigReason::NotListening => {
974                f.write_str("the socket is not a listening stream socket")
975            }
976            ConfigReason::Inconsistent => f.write_str("values contradict each other"),
977        }
978    }
979}
980
981#[cfg(test)]
982mod tests {
983    use super::*;
984
985    #[test]
986    fn test_close_reason_index_covers_every_variant_once() {
987        let all = [
988            CloseReason::PeerClosed,
989            CloseReason::PeerReset,
990            CloseReason::Idle,
991            CloseReason::HandshakeTimeout,
992            CloseReason::ReadTimeout,
993            CloseReason::WriteTimeout,
994            CloseReason::ExchangeDeadline,
995            CloseReason::ParkDeadline,
996            CloseReason::SlowPeer {
997                direction: Direction::Out,
998            },
999            CloseReason::Refused(RefusalCause::Busy),
1000            CloseReason::ProxyUntrusted,
1001            CloseReason::ProxyMissing,
1002            CloseReason::ProxyInvalid,
1003            CloseReason::TlsFailed,
1004            CloseReason::HelloRefused,
1005            CloseReason::Killed(KillReason(1)),
1006            CloseReason::EgressDenied(1),
1007            CloseReason::Drained,
1008            CloseReason::DrainDeadline,
1009            CloseReason::Shed,
1010            CloseReason::Shutdown,
1011            CloseReason::SessionClosed,
1012            CloseReason::SessionError,
1013            CloseReason::SessionPanicked,
1014            CloseReason::Limit(LimitKind::ReadBuffer),
1015            CloseReason::Io(OsErrorClass::Reset),
1016        ];
1017        let mut seen = [false; CloseReason::COUNT];
1018        for r in all {
1019            assert!(!seen[r.index()], "duplicate index for {r:?}");
1020            seen[r.index()] = true;
1021        }
1022        assert!(seen.iter().all(|s| *s));
1023    }
1024
1025    #[test]
1026    fn test_close_reason_size_is_eight_bytes() {
1027        assert!(std::mem::size_of::<CloseReason>() <= 8);
1028    }
1029
1030    #[test]
1031    fn test_close_style_graceful_reasons_are_not_abortive() {
1032        for r in [
1033            CloseReason::PeerClosed,
1034            CloseReason::Idle,
1035            CloseReason::Drained,
1036            CloseReason::SessionClosed,
1037        ] {
1038            assert!(!r.is_abortive());
1039        }
1040        assert!(CloseReason::DrainDeadline.is_abortive());
1041        assert!(CloseReason::Killed(KillReason(0)).is_abortive());
1042    }
1043
1044    #[test]
1045    fn test_from_io_timed_out_maps_to_read_timeout() {
1046        let e: NetError = io::Error::from(io::ErrorKind::TimedOut).into();
1047        assert!(matches!(
1048            e,
1049            NetError::TimedOut {
1050                kind: TimeoutKind::Read
1051            }
1052        ));
1053    }
1054
1055    #[test]
1056    fn test_from_io_reset_maps_to_connection_reset() {
1057        let e: NetError = io::Error::from(io::ErrorKind::ConnectionReset).into();
1058        assert!(matches!(e, NetError::ConnectionReset));
1059        assert_eq!(e.class(), ErrorClass::Peer);
1060    }
1061
1062    #[test]
1063    fn test_into_io_keeps_kind_and_source() {
1064        let io_err: io::Error = NetError::BrokenPipe.into();
1065        assert_eq!(io_err.kind(), io::ErrorKind::BrokenPipe);
1066        assert!(io_err.get_ref().is_some());
1067    }
1068
1069    #[test]
1070    fn test_retryable_connect_timeout_true_reset_false() {
1071        let addr: SocketAddr = "127.0.0.1:1".parse().unwrap();
1072        assert!(
1073            NetError::ConnectTimeout {
1074                addr,
1075                after: Duration::from_secs(1)
1076            }
1077            .is_retryable()
1078        );
1079        assert!(!NetError::ConnectionReset.is_retryable());
1080        assert!(
1081            NetError::Limit {
1082                which: LimitKind::BufferBudget
1083            }
1084            .is_retryable()
1085        );
1086    }
1087
1088    #[test]
1089    fn test_display_says_what_to_do() {
1090        let addr: SocketAddr = "0.0.0.0:443".parse().unwrap();
1091        let text = NetError::AddrInUse { addr }.to_string();
1092        assert!(text.contains("0.0.0.0:443"));
1093        assert!(text.contains("hand-off"));
1094    }
1095
1096    #[test]
1097    fn test_os_error_code_present_only_for_os_errors() {
1098        let e = NetError::Os {
1099            op: "x",
1100            source: io::Error::from_raw_os_error(5),
1101        };
1102        assert_eq!(e.os_error_code(), Some(5));
1103        assert_eq!(NetError::NotFound.os_error_code(), None);
1104    }
1105
1106    #[test]
1107    fn test_timeout_kind_maps_to_close_reason() {
1108        assert_eq!(
1109            CloseReason::for_timeout(TimeoutKind::Idle),
1110            CloseReason::Idle
1111        );
1112        assert_eq!(
1113            CloseReason::for_timeout(TimeoutKind::Write),
1114            CloseReason::WriteTimeout
1115        );
1116    }
1117}