Skip to main content

rama_net/uri/
error.rs

1//! Error types for the [`crate::uri`] module.
2//!
3//! Two distinct enums:
4//!
5//! - [`ParseError`] — surfaced when bytes coming in cannot be turned into a
6//!   valid [`Uri`](super::Uri). Carries enough information to point at *what*
7//!   went wrong (offset, component) for diagnostics.
8//! - [`UriError`] — surfaced when an operation on an already-parsed Uri
9//!   cannot be applied (e.g. setting an invalid path).
10
11use core::fmt;
12
13use rama_core::error::BoxError;
14
15/// Reasons parsing a byte string into a [`Uri`](super::Uri) can fail.
16///
17/// **Graceful by default**: the regular `Uri::parse` entry point accepts
18/// inputs that browsers and curl tolerate. Only inputs in the
19/// "differential-parse hazard" set (control chars, backslash-as-slash,
20/// alternate IPv4 forms, etc.) are unconditionally rejected — and those are
21/// the variants below that can fire even from `parse`.
22///
23/// `Uri::parse_strict` additionally rejects everything outside
24/// RFC 3986 with [`ParseError::StrictViolation`].
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ParseError {
27    /// The input was empty.
28    Empty,
29
30    /// A URI component is structurally invalid: bad delimiter placement,
31    /// disallowed character, empty where forbidden, etc. The wrapped
32    /// [`Component`] identifies which one. More specific failure modes
33    /// (control chars, percent-encoding, etc.) have their own variants
34    /// below.
35    InvalidComponent(Component),
36
37    /// A `\0`, `\r`, `\n`, `\t`, or other ASCII control character was found
38    /// inside a URI component. Always rejected — these are header-injection
39    /// and request-smuggling vectors.
40    ControlCharInUri { at: usize, byte: u8 },
41
42    /// A percent-encoded escape was malformed — `%` not followed by two hex
43    /// digits, or (after decoding) the resulting byte is itself disallowed
44    /// in the component where it appeared.
45    InvalidPercentEncoding { at: usize },
46
47    /// An IPv6 literal carried a zone identifier (`%25en0` on the wire).
48    /// Not currently supported — see module-level docs for the path forward.
49    IPv6ZoneNotSupported,
50
51    /// Non-ASCII host bytes were supplied without the `idna` feature
52    /// enabled. Only present when the `idna` feature is **off** — when it
53    /// is on, non-ASCII hosts are processed and either succeed or surface
54    /// as [`ParseError::InvalidComponent`] for [`Component::Host`].
55    #[cfg(not(feature = "idna"))]
56    #[cfg_attr(docsrs, doc(cfg(not(feature = "idna"))))]
57    IdnaNotEnabled,
58
59    /// Strict-mode-only rejection: input parsed under graceful rules but
60    /// violates RFC 3986. Only produced by `Uri::parse_strict`.
61    StrictViolation,
62
63    /// The URI exceeded the maximum representable length.
64    TooLong { len: usize },
65
66    /// Input bytes were not valid UTF-8.
67    ///
68    /// Graceful mode tolerates raw UTF-8 in path / query / fragment
69    /// (browsers and curl do too), but the bytes must still *be* valid
70    /// UTF-8 — every component accessor returns `&str`, and the
71    /// presence of a stray continuation byte or truncated multi-byte
72    /// sequence would otherwise be UB at access time.
73    ///
74    /// Always rejected. Strict mode rejects more aggressively (per-byte
75    /// ASCII grammar checks), but this is the floor.
76    NonUtf8 { at: usize },
77}
78
79impl fmt::Display for ParseError {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match self {
82            Self::Empty => f.write_str("uri is empty"),
83            Self::InvalidComponent(c) => write!(f, "invalid {c} component"),
84            Self::ControlCharInUri { at, byte } => {
85                write!(f, "control character 0x{byte:02X} at byte {at}")
86            }
87            Self::InvalidPercentEncoding { at } => {
88                write!(f, "invalid percent-encoded escape at byte {at}")
89            }
90            Self::IPv6ZoneNotSupported => f.write_str(
91                "IPv6 zone identifiers are not currently supported in uri host literals",
92            ),
93            #[cfg(not(feature = "idna"))]
94            Self::IdnaNotEnabled => {
95                f.write_str("non-ASCII host requires the `idna` feature to be enabled")
96            }
97            Self::StrictViolation => f.write_str("input does not satisfy RFC 3986 strict syntax"),
98            Self::TooLong { len } => write!(f, "uri is {len} bytes long, exceeds the maximum"),
99            Self::NonUtf8 { at } => {
100                write!(f, "invalid UTF-8 byte sequence starting at byte {at}")
101            }
102        }
103    }
104}
105
106impl core::error::Error for ParseError {}
107
108/// Reasons an operation on an already-parsed [`Uri`](super::Uri) can fail.
109///
110/// Distinct from [`ParseError`] because mutation has different diagnostic
111/// needs from parsing — when a setter fails, callers want to know which
112/// component they were touching, even if the underlying cause didn't tag
113/// it (e.g. a control-char rejection during `set_path` is more useful as
114/// `InvalidComponent { component: Path, cause: ControlCharInUri }` than
115/// as a bare `ControlCharInUri`).
116#[derive(Debug)]
117pub enum UriError {
118    /// A setter received an input that fails component validation. The
119    /// `cause` is the underlying parse-level failure.
120    InvalidComponent {
121        /// Which component the setter targeted.
122        component: Component,
123        /// Underlying parse-level cause.
124        cause: ParseError,
125    },
126
127    /// A typed-input conversion into a URI component failed before the
128    /// value reached the setter. The boxed cause is whatever the
129    /// upstream `TryInto` impl returned — typically from
130    /// [`Host::try_from`](crate::address::Host) or
131    /// [`Domain::try_from`](crate::address::Domain) for inputs like raw
132    /// UTF-8 hosts when the `idna` feature is disabled.
133    ///
134    /// Distinct from [`UriError::InvalidComponent`] because the failure
135    /// originates outside the URI parser, so the cause cannot always be
136    /// expressed as a [`ParseError`].
137    ComponentConversion {
138        /// Which component the setter targeted.
139        component: Component,
140        /// Underlying conversion cause (boxed because typed input
141        /// converters live across crate boundaries).
142        cause: BoxError,
143    },
144
145    /// An operation was attempted that is meaningless for the asterisk-form
146    /// URI (e.g. iterating path segments of `*`). Most setters auto-upgrade
147    /// asterisk to a reference; the few that cannot return this error.
148    AsteriskOperation,
149}
150
151/// Which URI component a [`ParseError`] or [`UriError`] refers to.
152///
153/// `Authority` is the umbrella for `UserInfo` + `Host` + `Port`; a parse
154/// failure *inside* a sub-component is reported against the sub-component,
155/// not against `Authority` itself. The variant still surfaces for
156/// structural failures that aren't attributable to one sub-component
157/// — e.g. IP-literal bracket mismatches in `parser::authority`, and
158/// path / query / fragment delimiters appearing in a CONNECT
159/// authority-form input (`parse_authority_form`).
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum Component {
162    Scheme,
163    Authority,
164    UserInfo,
165    Host,
166    Port,
167    Path,
168    Query,
169    Fragment,
170}
171
172impl fmt::Display for Component {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        f.write_str(match self {
175            Self::Scheme => "scheme",
176            Self::Authority => "authority",
177            Self::UserInfo => "userinfo",
178            Self::Host => "host",
179            Self::Port => "port",
180            Self::Path => "path",
181            Self::Query => "query",
182            Self::Fragment => "fragment",
183        })
184    }
185}
186
187impl fmt::Display for UriError {
188    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
189        match self {
190            Self::InvalidComponent { component, cause } => {
191                write!(f, "invalid {component} component: {cause}")
192            }
193            Self::ComponentConversion { component, cause } => {
194                write!(f, "{component} component conversion failed: {cause}")
195            }
196            Self::AsteriskOperation => {
197                f.write_str("operation is not valid on the asterisk-form uri")
198            }
199        }
200    }
201}
202
203impl core::error::Error for UriError {
204    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
205        match self {
206            Self::InvalidComponent { cause, .. } => Some(cause),
207            Self::ComponentConversion { cause, .. } => Some(cause.as_ref()),
208            Self::AsteriskOperation => None,
209        }
210    }
211}