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    /// One value per counter slot, inner values at their first or zero value, in
554    /// `index` order (for iterating counters).
555    pub(crate) const BY_INDEX: [CloseReason; CloseReason::COUNT] = [
556        CloseReason::PeerClosed,
557        CloseReason::PeerReset,
558        CloseReason::Idle,
559        CloseReason::HandshakeTimeout,
560        CloseReason::ReadTimeout,
561        CloseReason::WriteTimeout,
562        CloseReason::ExchangeDeadline,
563        CloseReason::ParkDeadline,
564        CloseReason::SlowPeer {
565            direction: Direction::In,
566        },
567        CloseReason::Refused(RefusalCause::GlobalCap),
568        CloseReason::ProxyUntrusted,
569        CloseReason::ProxyMissing,
570        CloseReason::ProxyInvalid,
571        CloseReason::TlsFailed,
572        CloseReason::HelloRefused,
573        CloseReason::Killed(KillReason(0)),
574        CloseReason::EgressDenied(0),
575        CloseReason::Drained,
576        CloseReason::DrainDeadline,
577        CloseReason::Shed,
578        CloseReason::Shutdown,
579        CloseReason::SessionClosed,
580        CloseReason::SessionError,
581        CloseReason::SessionPanicked,
582        CloseReason::Limit(LimitKind::Connections),
583        CloseReason::Io(OsErrorClass::Other(0)),
584    ];
585
586    /// A stable snake_case name for logs.
587    ///
588    /// ```
589    /// assert_eq!(net_io::CloseReason::Idle.as_str(), "idle");
590    /// ```
591    #[must_use]
592    pub fn as_str(&self) -> &'static str {
593        match self {
594            CloseReason::PeerClosed => "peer_closed",
595            CloseReason::PeerReset => "peer_reset",
596            CloseReason::Idle => "idle",
597            CloseReason::HandshakeTimeout => "handshake_timeout",
598            CloseReason::ReadTimeout => "read_timeout",
599            CloseReason::WriteTimeout => "write_timeout",
600            CloseReason::ExchangeDeadline => "exchange_deadline",
601            CloseReason::ParkDeadline => "park_deadline",
602            CloseReason::SlowPeer {
603                direction: Direction::In,
604            } => "slow_peer_in",
605            CloseReason::SlowPeer {
606                direction: Direction::Out,
607            } => "slow_peer_out",
608            CloseReason::Refused(_) => "refused",
609            CloseReason::ProxyUntrusted => "proxy_untrusted",
610            CloseReason::ProxyMissing => "proxy_missing",
611            CloseReason::ProxyInvalid => "proxy_invalid",
612            CloseReason::TlsFailed => "tls_failed",
613            CloseReason::HelloRefused => "hello_refused",
614            CloseReason::Killed(_) => "killed",
615            CloseReason::EgressDenied(_) => "egress_denied",
616            CloseReason::Drained => "drained",
617            CloseReason::DrainDeadline => "drain_deadline",
618            CloseReason::Shed => "shed",
619            CloseReason::Shutdown => "shutdown",
620            CloseReason::SessionClosed => "session_closed",
621            CloseReason::SessionError => "session_error",
622            CloseReason::SessionPanicked => "session_panicked",
623            CloseReason::Limit(_) => "limit",
624            CloseReason::Io(_) => "io",
625        }
626    }
627
628    /// Whether the connection is closed with RST (`true`) or gracefully with FIN (`false`).
629    ///
630    /// ```
631    /// assert!(net_io::CloseReason::ReadTimeout.is_abortive());
632    /// ```
633    #[must_use]
634    pub fn is_abortive(&self) -> bool {
635        !matches!(
636            self,
637            CloseReason::PeerClosed
638                | CloseReason::Idle
639                | CloseReason::Drained
640                | CloseReason::SessionClosed
641        )
642    }
643
644    /// An index per variant for per-reason counters (inner values ignored).
645    pub(crate) fn index(&self) -> usize {
646        match self {
647            CloseReason::PeerClosed => 0,
648            CloseReason::PeerReset => 1,
649            CloseReason::Idle => 2,
650            CloseReason::HandshakeTimeout => 3,
651            CloseReason::ReadTimeout => 4,
652            CloseReason::WriteTimeout => 5,
653            CloseReason::ExchangeDeadline => 6,
654            CloseReason::ParkDeadline => 7,
655            CloseReason::SlowPeer { .. } => 8,
656            CloseReason::Refused(_) => 9,
657            CloseReason::ProxyUntrusted => 10,
658            CloseReason::ProxyMissing => 11,
659            CloseReason::ProxyInvalid => 12,
660            CloseReason::TlsFailed => 13,
661            CloseReason::HelloRefused => 14,
662            CloseReason::Killed(_) => 15,
663            CloseReason::EgressDenied(_) => 16,
664            CloseReason::Drained => 17,
665            CloseReason::DrainDeadline => 18,
666            CloseReason::Shed => 19,
667            CloseReason::Shutdown => 20,
668            CloseReason::SessionClosed => 21,
669            CloseReason::SessionError => 22,
670            CloseReason::SessionPanicked => 23,
671            CloseReason::Limit(_) => 24,
672            CloseReason::Io(_) => 25,
673        }
674    }
675
676    /// The number of distinct indexes [`CloseReason::index`] returns.
677    pub(crate) const COUNT: usize = 26;
678
679    /// The close reason for a timeout of this kind.
680    pub(crate) fn for_timeout(kind: TimeoutKind) -> CloseReason {
681        match kind {
682            TimeoutKind::Handshake => CloseReason::HandshakeTimeout,
683            TimeoutKind::Idle => CloseReason::Idle,
684            TimeoutKind::Read | TimeoutKind::Connect => CloseReason::ReadTimeout,
685            TimeoutKind::Write => CloseReason::WriteTimeout,
686            TimeoutKind::Exchange => CloseReason::ExchangeDeadline,
687            TimeoutKind::Park => CloseReason::ParkDeadline,
688            TimeoutKind::Drain => CloseReason::DrainDeadline,
689        }
690    }
691}
692
693impl fmt::Display for CloseReason {
694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695        f.write_str(self.as_str())
696    }
697}
698
699/// Why a connection was refused.
700///
701/// ```
702/// assert_eq!(net_io::RefusalCause::SourceCap.as_str(), "source_cap");
703/// ```
704#[non_exhaustive]
705#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
706pub enum RefusalCause {
707    /// The global connection cap was reached.
708    GlobalCap,
709    /// The listener's connection cap was reached.
710    ListenerCap,
711    /// The per-source concurrency cap was reached.
712    SourceCap,
713    /// The lane's reservation was used up.
714    LaneReserve,
715    /// The gate refused, with its code.
716    Gate(u16),
717    /// The session factory refused.
718    Session,
719    /// The blocking driver had no free worker.
720    Busy,
721    /// The door was closed.
722    DoorClosed,
723    /// The control line shed this lane.
724    Shed,
725    /// The control line is in lockdown and the listener does not allow it.
726    Lockdown,
727    /// A local socket peer's user id is not the one the listener requires
728    /// (`LocalOptions::require_uid`).
729    Credentials,
730}
731
732impl RefusalCause {
733    /// One value per counter slot in `index` order (`Gate` carries code 0).
734    pub(crate) const BY_INDEX: [RefusalCause; RefusalCause::COUNT] = [
735        RefusalCause::GlobalCap,
736        RefusalCause::ListenerCap,
737        RefusalCause::SourceCap,
738        RefusalCause::LaneReserve,
739        RefusalCause::Gate(0),
740        RefusalCause::Session,
741        RefusalCause::Busy,
742        RefusalCause::DoorClosed,
743        RefusalCause::Shed,
744        RefusalCause::Lockdown,
745        RefusalCause::Credentials,
746    ];
747
748    /// A stable snake_case name for logs.
749    #[must_use]
750    pub fn as_str(&self) -> &'static str {
751        match self {
752            RefusalCause::GlobalCap => "global_cap",
753            RefusalCause::ListenerCap => "listener_cap",
754            RefusalCause::SourceCap => "source_cap",
755            RefusalCause::LaneReserve => "lane_reserve",
756            RefusalCause::Gate(_) => "gate",
757            RefusalCause::Session => "session",
758            RefusalCause::Busy => "busy",
759            RefusalCause::DoorClosed => "door_closed",
760            RefusalCause::Shed => "shed",
761            RefusalCause::Lockdown => "lockdown",
762            RefusalCause::Credentials => "credentials",
763        }
764    }
765
766    pub(crate) fn index(&self) -> usize {
767        match self {
768            RefusalCause::GlobalCap => 0,
769            RefusalCause::ListenerCap => 1,
770            RefusalCause::SourceCap => 2,
771            RefusalCause::LaneReserve => 3,
772            RefusalCause::Gate(_) => 4,
773            RefusalCause::Session => 5,
774            RefusalCause::Busy => 6,
775            RefusalCause::DoorClosed => 7,
776            RefusalCause::Shed => 8,
777            RefusalCause::Lockdown => 9,
778            RefusalCause::Credentials => 10,
779        }
780    }
781
782    pub(crate) const COUNT: usize = 11;
783}
784
785/// Which configured bound was hit.
786///
787/// ```
788/// assert_eq!(net_io::LimitKind::OutputBudget.as_str(), "output_budget");
789/// ```
790#[non_exhaustive]
791#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
792pub enum LimitKind {
793    /// The global connection cap.
794    Connections,
795    /// A listener's connection cap.
796    PerListener,
797    /// The per-source concurrency cap.
798    PerSource,
799    /// The total buffer budget.
800    BufferBudget,
801    /// A connection's output budget.
802    OutputBudget,
803    /// A worker's completion queue.
804    CompletionQueue,
805    /// The PROXY header size.
806    ProxyHeader,
807    /// The accept batch.
808    AcceptBatch,
809    /// A connection's read buffer (full and not consumed).
810    ReadBuffer,
811    /// The UDP amplification guard.
812    Amplification,
813    /// A UDP source budget.
814    SourceBudget,
815    /// The process's descriptor limit.
816    Descriptors,
817}
818
819impl LimitKind {
820    /// A stable snake_case name for logs.
821    #[must_use]
822    pub fn as_str(&self) -> &'static str {
823        match self {
824            LimitKind::Connections => "connections",
825            LimitKind::PerListener => "per_listener",
826            LimitKind::PerSource => "per_source",
827            LimitKind::BufferBudget => "buffer_budget",
828            LimitKind::OutputBudget => "output_budget",
829            LimitKind::CompletionQueue => "completion_queue",
830            LimitKind::ProxyHeader => "proxy_header",
831            LimitKind::AcceptBatch => "accept_batch",
832            LimitKind::ReadBuffer => "read_buffer",
833            LimitKind::Amplification => "amplification",
834            LimitKind::SourceBudget => "source_budget",
835            LimitKind::Descriptors => "descriptors",
836        }
837    }
838}
839
840/// Which deadline passed.
841///
842/// ```
843/// assert_eq!(net_io::TimeoutKind::Idle.as_str(), "idle");
844/// ```
845#[non_exhaustive]
846#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
847pub enum TimeoutKind {
848    /// The handshake (PROXY and TLS).
849    Handshake,
850    /// Waiting for the next exchange.
851    Idle,
852    /// Waiting for bytes during an exchange.
853    Read,
854    /// Waiting for the peer to take bytes.
855    Write,
856    /// The session's total deadline for the exchange.
857    Exchange,
858    /// A parked connection.
859    Park,
860    /// An outbound connect.
861    Connect,
862    /// A drain.
863    Drain,
864}
865
866impl TimeoutKind {
867    /// A stable snake_case name for logs.
868    #[must_use]
869    pub fn as_str(&self) -> &'static str {
870        match self {
871            TimeoutKind::Handshake => "handshake",
872            TimeoutKind::Idle => "idle",
873            TimeoutKind::Read => "read",
874            TimeoutKind::Write => "write",
875            TimeoutKind::Exchange => "exchange",
876            TimeoutKind::Park => "park",
877            TimeoutKind::Connect => "connect",
878            TimeoutKind::Drain => "drain",
879        }
880    }
881}
882
883/// A caller-defined code for a kill.
884///
885/// ```
886/// let reason = net_io::KillReason(7);
887/// assert_eq!(reason.0, 7);
888/// ```
889#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
890pub struct KillReason(pub u16);
891
892/// A direction of transfer.
893///
894/// ```
895/// assert_eq!(net_io::Direction::In.as_str(), "in");
896/// ```
897#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
898pub enum Direction {
899    /// From the peer to us.
900    In,
901    /// From us to the peer.
902    Out,
903}
904
905impl Direction {
906    /// `"in"` or `"out"`.
907    #[must_use]
908    pub fn as_str(&self) -> &'static str {
909        match self {
910            Direction::In => "in",
911            Direction::Out => "out",
912        }
913    }
914}
915
916/// The class of an OS error, without its message text.
917///
918/// ```
919/// use net_io::OsErrorClass;
920/// let class = OsErrorClass::from_io(&std::io::Error::from(std::io::ErrorKind::ConnectionReset));
921/// assert_eq!(class, OsErrorClass::Reset);
922/// ```
923#[non_exhaustive]
924#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
925pub enum OsErrorClass {
926    /// The peer reset the connection.
927    Reset,
928    /// The connection was aborted locally.
929    Aborted,
930    /// The OS timed the connection out (for example failed keepalive probes).
931    TimedOut,
932    /// The connection was refused.
933    Refused,
934    /// No route.
935    Unreachable,
936    /// A file ended while it was being sent.
937    FileTruncated,
938    /// Any other OS error, by number.
939    Other(i32),
940}
941
942impl OsErrorClass {
943    /// Classifies an `io::Error`.
944    #[must_use]
945    pub fn from_io(e: &io::Error) -> OsErrorClass {
946        match e.kind() {
947            io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe => OsErrorClass::Reset,
948            io::ErrorKind::ConnectionAborted => OsErrorClass::Aborted,
949            io::ErrorKind::TimedOut => OsErrorClass::TimedOut,
950            io::ErrorKind::ConnectionRefused => OsErrorClass::Refused,
951            io::ErrorKind::HostUnreachable | io::ErrorKind::NetworkUnreachable => {
952                OsErrorClass::Unreachable
953            }
954            _ => OsErrorClass::Other(e.raw_os_error().unwrap_or(0)),
955        }
956    }
957}
958
959/// The rule a configuration value broke.
960///
961/// ```
962/// use net_io::ConfigReason;
963/// assert_eq!(ConfigReason::Zero.to_string(), "zero is not allowed here");
964/// ```
965#[non_exhaustive]
966#[derive(Clone, Copy, Debug, PartialEq, Eq)]
967pub enum ConfigReason {
968    /// Zero where zero is meaningless.
969    Zero,
970    /// Above the maximum.
971    TooLarge {
972        /// The maximum.
973        max: u64,
974    },
975    /// Below the minimum.
976    TooSmall {
977        /// The minimum.
978        min: u64,
979    },
980    /// A per-listener or per-source cap above the global cap.
981    AboveGlobal,
982    /// A deadline shorter than one timer tick.
983    BelowTick,
984    /// The minimum-rate window is shorter than its check interval.
985    WindowBelowCheck,
986    /// The buffer budget is below workers times the largest class times two.
987    BudgetBelowWorkers {
988        /// The smallest valid budget in bytes.
989        need: u64,
990    },
991    /// The caps exceed the process's descriptor limit.
992    DescriptorLimit {
993        /// The descriptor limit.
994        have: u64,
995        /// Descriptors the configuration needs.
996        need: u64,
997    },
998    /// A required part is not set.
999    Missing,
1000    /// An adopted socket is not a listening socket.
1001    NotListening,
1002    /// Values that contradict each other.
1003    Inconsistent,
1004    /// A byte or character the value cannot contain (a NUL in a socket path).
1005    Malformed,
1006}
1007
1008impl fmt::Display for ConfigReason {
1009    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1010        match self {
1011            ConfigReason::Zero => f.write_str("zero is not allowed here"),
1012            ConfigReason::TooLarge { max } => write!(f, "above the maximum of {max}"),
1013            ConfigReason::TooSmall { min } => write!(f, "below the minimum of {min}"),
1014            ConfigReason::AboveGlobal => f.write_str("above the global connection cap"),
1015            ConfigReason::BelowTick => f.write_str("shorter than one timer tick"),
1016            ConfigReason::WindowBelowCheck => {
1017                f.write_str("the minimum-rate window is shorter than its check interval")
1018            }
1019            ConfigReason::BudgetBelowWorkers { need } => write!(
1020                f,
1021                "the buffer budget must be at least {need} bytes (workers x largest class x 2)"
1022            ),
1023            ConfigReason::DescriptorLimit { have, need } => write!(
1024                f,
1025                "needs {need} descriptors but the process limit is {have}; raise the limit or lower the caps"
1026            ),
1027            ConfigReason::Missing => f.write_str("a required part is not set"),
1028            ConfigReason::NotListening => {
1029                f.write_str("the socket is not a listening stream socket")
1030            }
1031            ConfigReason::Inconsistent => f.write_str("values contradict each other"),
1032            ConfigReason::Malformed => f.write_str("contains a byte or character it cannot"),
1033        }
1034    }
1035}
1036
1037#[cfg(test)]
1038mod tests {
1039    use super::*;
1040
1041    #[test]
1042    fn test_by_index_tables_match_index() {
1043        for (i, r) in CloseReason::BY_INDEX.iter().enumerate() {
1044            assert_eq!(r.index(), i, "{r:?}");
1045        }
1046        for (i, c) in RefusalCause::BY_INDEX.iter().enumerate() {
1047            assert_eq!(c.index(), i, "{c:?}");
1048        }
1049    }
1050
1051    #[test]
1052    fn test_close_reason_index_covers_every_variant_once() {
1053        let all = [
1054            CloseReason::PeerClosed,
1055            CloseReason::PeerReset,
1056            CloseReason::Idle,
1057            CloseReason::HandshakeTimeout,
1058            CloseReason::ReadTimeout,
1059            CloseReason::WriteTimeout,
1060            CloseReason::ExchangeDeadline,
1061            CloseReason::ParkDeadline,
1062            CloseReason::SlowPeer {
1063                direction: Direction::Out,
1064            },
1065            CloseReason::Refused(RefusalCause::Busy),
1066            CloseReason::ProxyUntrusted,
1067            CloseReason::ProxyMissing,
1068            CloseReason::ProxyInvalid,
1069            CloseReason::TlsFailed,
1070            CloseReason::HelloRefused,
1071            CloseReason::Killed(KillReason(1)),
1072            CloseReason::EgressDenied(1),
1073            CloseReason::Drained,
1074            CloseReason::DrainDeadline,
1075            CloseReason::Shed,
1076            CloseReason::Shutdown,
1077            CloseReason::SessionClosed,
1078            CloseReason::SessionError,
1079            CloseReason::SessionPanicked,
1080            CloseReason::Limit(LimitKind::ReadBuffer),
1081            CloseReason::Io(OsErrorClass::Reset),
1082        ];
1083        let mut seen = [false; CloseReason::COUNT];
1084        for r in all {
1085            assert!(!seen[r.index()], "duplicate index for {r:?}");
1086            seen[r.index()] = true;
1087        }
1088        assert!(seen.iter().all(|s| *s));
1089    }
1090
1091    #[test]
1092    fn test_close_reason_size_is_eight_bytes() {
1093        assert!(std::mem::size_of::<CloseReason>() <= 8);
1094    }
1095
1096    #[test]
1097    fn test_close_style_graceful_reasons_are_not_abortive() {
1098        for r in [
1099            CloseReason::PeerClosed,
1100            CloseReason::Idle,
1101            CloseReason::Drained,
1102            CloseReason::SessionClosed,
1103        ] {
1104            assert!(!r.is_abortive());
1105        }
1106        assert!(CloseReason::DrainDeadline.is_abortive());
1107        assert!(CloseReason::Killed(KillReason(0)).is_abortive());
1108    }
1109
1110    #[test]
1111    fn test_from_io_timed_out_maps_to_read_timeout() {
1112        let e: NetError = io::Error::from(io::ErrorKind::TimedOut).into();
1113        assert!(matches!(
1114            e,
1115            NetError::TimedOut {
1116                kind: TimeoutKind::Read
1117            }
1118        ));
1119    }
1120
1121    #[test]
1122    fn test_from_io_reset_maps_to_connection_reset() {
1123        let e: NetError = io::Error::from(io::ErrorKind::ConnectionReset).into();
1124        assert!(matches!(e, NetError::ConnectionReset));
1125        assert_eq!(e.class(), ErrorClass::Peer);
1126    }
1127
1128    #[test]
1129    fn test_into_io_keeps_kind_and_source() {
1130        let io_err: io::Error = NetError::BrokenPipe.into();
1131        assert_eq!(io_err.kind(), io::ErrorKind::BrokenPipe);
1132        assert!(io_err.get_ref().is_some());
1133    }
1134
1135    #[test]
1136    fn test_retryable_connect_timeout_true_reset_false() {
1137        let addr: SocketAddr = "127.0.0.1:1".parse().unwrap();
1138        assert!(
1139            NetError::ConnectTimeout {
1140                addr,
1141                after: Duration::from_secs(1)
1142            }
1143            .is_retryable()
1144        );
1145        assert!(!NetError::ConnectionReset.is_retryable());
1146        assert!(
1147            NetError::Limit {
1148                which: LimitKind::BufferBudget
1149            }
1150            .is_retryable()
1151        );
1152    }
1153
1154    #[test]
1155    fn test_display_says_what_to_do() {
1156        let addr: SocketAddr = "0.0.0.0:443".parse().unwrap();
1157        let text = NetError::AddrInUse { addr }.to_string();
1158        assert!(text.contains("0.0.0.0:443"));
1159        assert!(text.contains("hand-off"));
1160    }
1161
1162    #[test]
1163    fn test_os_error_code_present_only_for_os_errors() {
1164        let e = NetError::Os {
1165            op: "x",
1166            source: io::Error::from_raw_os_error(5),
1167        };
1168        assert_eq!(e.os_error_code(), Some(5));
1169        assert_eq!(NetError::NotFound.os_error_code(), None);
1170    }
1171
1172    #[test]
1173    fn test_timeout_kind_maps_to_close_reason() {
1174        assert_eq!(
1175            CloseReason::for_timeout(TimeoutKind::Idle),
1176            CloseReason::Idle
1177        );
1178        assert_eq!(
1179            CloseReason::for_timeout(TimeoutKind::Write),
1180            CloseReason::WriteTimeout
1181        );
1182    }
1183}