Skip to main content

xocomil/
error.rs

1//! Unified error type for all xocomil operations.
2//!
3//! [`Error`] wraps every failure mode — I/O errors, parse errors, and body
4//! errors — into a single type so callers can use one `?` chain for an
5//! entire request–response cycle.
6//!
7//! Pattern-match on [`Error`] directly to distinguish error
8//! classes via the inner kind. The [`ParseErrorKind`] and [`BodyErrorKind`] enums are
9//! `#[non_exhaustive]` so new variants can be added without breaking
10//! downstream code.
11
12use std::fmt;
13use std::io;
14
15// ---------------------------------------------------------------------------
16// ParseErrorKind
17// ---------------------------------------------------------------------------
18
19/// What went wrong when parsing an HTTP request.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[non_exhaustive]
22pub enum ParseErrorKind {
23    /// Headers exceed the maximum allowed size.
24    HeadersTooLarge,
25    /// Connection closed before headers were complete.
26    ConnectionClosed,
27    /// No request line found.
28    NoRequestLine,
29    /// Headers not terminated by `\r\n\r\n`.
30    IncompleteHeaders,
31    /// Request line is malformed.
32    MalformedRequestLine,
33    /// Request target contains invalid bytes.
34    MalformedRequestTarget,
35    /// A header line is malformed.
36    MalformedHeader,
37    /// HTTP method is not supported.
38    UnsupportedMethod,
39    /// HTTP version is not supported.
40    UnsupportedHttpVersion,
41    /// Too many headers.
42    TooManyHeaders,
43    /// Missing required Host header (HTTP/1.1).
44    MissingHostHeader,
45    /// Duplicate header that must be unique.
46    DuplicateHeader,
47    /// Conflicting Transfer-Encoding and Content-Length headers.
48    ConflictingHeaders,
49    /// Content-Length value is not a valid integer.
50    InvalidContentLength,
51    /// Transfer-Encoding value is not supported.
52    UnsupportedTransferEncoding,
53}
54
55impl fmt::Display for ParseErrorKind {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::HeadersTooLarge => f.write_str("headers too large"),
59            Self::ConnectionClosed => f.write_str("connection closed before headers complete"),
60            Self::NoRequestLine => f.write_str("no request line"),
61            Self::IncompleteHeaders => f.write_str("headers not terminated by \\r\\n\\r\\n"),
62            Self::MalformedRequestLine => f.write_str("malformed request line"),
63            Self::MalformedRequestTarget => f.write_str("request target contains invalid byte"),
64            Self::MalformedHeader => f.write_str("malformed header line"),
65            Self::UnsupportedMethod => f.write_str("unsupported method"),
66            Self::UnsupportedHttpVersion => f.write_str("unsupported HTTP version"),
67            Self::TooManyHeaders => f.write_str("too many headers"),
68            Self::MissingHostHeader => f.write_str("missing required Host header"),
69            Self::DuplicateHeader => f.write_str("duplicate header not allowed"),
70            Self::ConflictingHeaders => {
71                f.write_str("conflicting Transfer-Encoding and Content-Length")
72            }
73            Self::InvalidContentLength => f.write_str("invalid Content-Length value"),
74            Self::UnsupportedTransferEncoding => {
75                f.write_str("unsupported Transfer-Encoding (only chunked is supported)")
76            }
77        }
78    }
79}
80
81// ---------------------------------------------------------------------------
82// BodyErrorKind
83// ---------------------------------------------------------------------------
84
85/// What went wrong when reading or decoding a request body.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87#[non_exhaustive]
88pub enum BodyErrorKind {
89    /// Body exceeds the maximum allowed size.
90    BodyTooLarge,
91    /// Connection closed before the complete body was received.
92    UnexpectedEof,
93    /// Chunk size line contains invalid hex digits.
94    InvalidChunkSize,
95    /// Chunked encoding is structurally invalid.
96    MalformedChunkedEncoding,
97}
98
99impl fmt::Display for BodyErrorKind {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        match self {
102            Self::BodyTooLarge => f.write_str("body exceeds buffer size"),
103            Self::UnexpectedEof => f.write_str("connection closed before body complete"),
104            Self::InvalidChunkSize => f.write_str("invalid chunk size"),
105            Self::MalformedChunkedEncoding => f.write_str("malformed chunked encoding"),
106        }
107    }
108}
109
110// ---------------------------------------------------------------------------
111// PctErrorKind
112// ---------------------------------------------------------------------------
113
114/// What went wrong when percent-decoding a byte slice.
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116#[non_exhaustive]
117pub enum PctErrorKind {
118    /// A `%` was not followed by two hex digits.
119    InvalidEscape,
120    /// The output buffer was too small to hold the decoded result.
121    BufferTooSmall,
122}
123
124impl fmt::Display for PctErrorKind {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        match self {
127            Self::InvalidEscape => f.write_str("invalid percent-encoded escape"),
128            Self::BufferTooSmall => f.write_str("decode buffer too small"),
129        }
130    }
131}
132
133// ---------------------------------------------------------------------------
134// MediaErrorKind
135// ---------------------------------------------------------------------------
136
137/// What went wrong when parsing a media-type header value.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[non_exhaustive]
140pub enum MediaErrorKind {
141    /// The input was empty or whitespace-only.
142    Empty,
143    /// The input did not contain a `/` separating type and subtype.
144    MissingSlash,
145    /// The type or subtype was empty after trimming whitespace.
146    InvalidToken,
147}
148
149impl fmt::Display for MediaErrorKind {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        match self {
152            Self::Empty => f.write_str("empty media type"),
153            Self::MissingSlash => f.write_str("media type missing '/'"),
154            Self::InvalidToken => f.write_str("media type has empty token"),
155        }
156    }
157}
158
159// ---------------------------------------------------------------------------
160// ResponseErrorKind
161// ---------------------------------------------------------------------------
162
163/// What went wrong when building an HTTP response.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165#[non_exhaustive]
166pub enum ResponseErrorKind {
167    /// Response header capacity exceeded.
168    HeaderCapacityExceeded,
169    /// Response header name contains invalid bytes.
170    InvalidHeaderName,
171    /// Response header value contains forbidden bytes (NUL, CR, or LF).
172    InvalidHeaderValue,
173    /// Duplicate Content-Length header.
174    DuplicateContentLength,
175}
176
177impl fmt::Display for ResponseErrorKind {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::HeaderCapacityExceeded => f.write_str("response header capacity exceeded"),
181            Self::InvalidHeaderName => f.write_str("response header name contains invalid byte"),
182            Self::InvalidHeaderValue => {
183                f.write_str("response header value contains forbidden byte (NUL, CR, or LF)")
184            }
185            Self::DuplicateContentLength => f.write_str("duplicate Content-Length header"),
186        }
187    }
188}
189
190// ---------------------------------------------------------------------------
191// Error
192// ---------------------------------------------------------------------------
193
194/// Unified error type for all xocomil operations.
195///
196/// Wraps I/O errors, request-parse errors, and body errors into a single
197/// enum so callers need only one error type in their signatures.
198///
199/// # Matching specific error kinds
200///
201/// ```
202/// use xocomil::error::{Error, ParseErrorKind};
203///
204/// fn status_for(err: &Error) -> u16 {
205///     match err {
206///         Error::Parse(ParseErrorKind::HeadersTooLarge) => 431,
207///         Error::Parse(_) => 400,
208///         Error::Body(_) => 400,
209///         Error::Response(_) => 400,
210///         Error::Pct(_) => 400,
211///         Error::Media(_) => 400,
212///         Error::Io(_) => 500,
213///     }
214/// }
215/// ```
216#[derive(Debug)]
217pub enum Error {
218    /// An I/O error from the underlying reader or writer.
219    Io(io::Error),
220    /// A request parsing error.
221    Parse(ParseErrorKind),
222    /// A body reading or decoding error.
223    Body(BodyErrorKind),
224    /// A response building error.
225    Response(ResponseErrorKind),
226    /// A percent-decoding error.
227    Pct(PctErrorKind),
228    /// A media-type parsing error.
229    Media(MediaErrorKind),
230}
231
232impl fmt::Display for Error {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        match self {
235            Self::Io(e) => fmt::Display::fmt(e, f),
236            Self::Parse(k) => fmt::Display::fmt(k, f),
237            Self::Body(k) => fmt::Display::fmt(k, f),
238            Self::Response(k) => fmt::Display::fmt(k, f),
239            Self::Pct(k) => fmt::Display::fmt(k, f),
240            Self::Media(k) => fmt::Display::fmt(k, f),
241        }
242    }
243}
244
245impl std::error::Error for Error {
246    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
247        match self {
248            Self::Io(e) => Some(e),
249            _ => None,
250        }
251    }
252}
253
254impl From<io::Error> for Error {
255    fn from(e: io::Error) -> Self {
256        Self::Io(e)
257    }
258}
259
260impl From<ParseErrorKind> for Error {
261    fn from(k: ParseErrorKind) -> Self {
262        Self::Parse(k)
263    }
264}
265
266impl From<BodyErrorKind> for Error {
267    fn from(k: BodyErrorKind) -> Self {
268        Self::Body(k)
269    }
270}
271
272impl From<ResponseErrorKind> for Error {
273    fn from(k: ResponseErrorKind) -> Self {
274        Self::Response(k)
275    }
276}
277
278impl From<PctErrorKind> for Error {
279    fn from(k: PctErrorKind) -> Self {
280        Self::Pct(k)
281    }
282}
283
284impl From<MediaErrorKind> for Error {
285    fn from(k: MediaErrorKind) -> Self {
286        Self::Media(k)
287    }
288}
289
290impl From<Error> for io::Error {
291    fn from(e: Error) -> Self {
292        match e {
293            Error::Io(e) => e,
294            Error::Response(_) | Error::Pct(_) | Error::Media(_) => {
295                Self::new(io::ErrorKind::InvalidInput, e)
296            }
297            other => Self::new(io::ErrorKind::InvalidData, other),
298        }
299    }
300}
301
302// ---------------------------------------------------------------------------
303// ReadError
304// ---------------------------------------------------------------------------
305
306/// Error from [`Request::read`](crate::request::Request::read), preserving
307/// how many bytes were read into the buffer before the failure.
308///
309/// This allows callers to recover partial data (e.g. for logging or
310/// connection reuse).
311#[derive(Debug)]
312pub struct ReadError {
313    /// The underlying error.
314    pub error: Error,
315    /// Number of bytes written into the buffer before the error.
316    /// The buffer contents `buf[..bytes_read]` are valid but may
317    /// represent an incomplete or malformed request.
318    pub bytes_read: usize,
319}
320
321impl fmt::Display for ReadError {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        self.error.fmt(f)
324    }
325}
326
327impl std::error::Error for ReadError {
328    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
329        self.error.source()
330    }
331}
332
333impl From<ReadError> for io::Error {
334    fn from(e: ReadError) -> Self {
335        e.error.into()
336    }
337}