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}