Skip to main content

goose_http/common/
mod.rs

1//! Core HTTP types shared across request and response handling.
2//!
3//! Implements strongly typed representations of methods, status codes, and
4//! protocol versions in line with RFC 9110.
5
6use std::fmt;
7use std::str::FromStr;
8
9use thiserror::Error;
10
11/// HTTP method token (RFC 9110 Section 9).
12#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub enum Method {
14    Get,
15    Head,
16    Post,
17    Put,
18    Delete,
19    Connect,
20    Options,
21    Trace,
22    Patch,
23    Extension(Box<str>),
24}
25
26impl Method {
27    /// Returns the canonical string representation of the method.
28    pub fn as_str(&self) -> &str {
29        match self {
30            Method::Get => "GET",
31            Method::Head => "HEAD",
32            Method::Post => "POST",
33            Method::Put => "PUT",
34            Method::Delete => "DELETE",
35            Method::Connect => "CONNECT",
36            Method::Options => "OPTIONS",
37            Method::Trace => "TRACE",
38            Method::Patch => "PATCH",
39            Method::Extension(token) => token,
40        }
41    }
42
43    /// True if the method is request-body safe by default (spec semantics).
44    pub fn is_idempotent(&self) -> bool {
45        matches!(
46            self,
47            Method::Get
48                | Method::Head
49                | Method::Put
50                | Method::Delete
51                | Method::Options
52                | Method::Trace
53        )
54    }
55
56    /// Determine if the method is cacheable by default.
57    pub fn is_cacheable(&self) -> bool {
58        matches!(self, Method::Get | Method::Head)
59    }
60
61    /// Determine if the method is defined to be safe (no state change).
62    pub fn is_safe(&self) -> bool {
63        matches!(
64            self,
65            Method::Get | Method::Head | Method::Options | Method::Trace
66        )
67    }
68
69    fn from_token(token: &str) -> Result<Self, MethodError> {
70        match token {
71            "GET" => Ok(Method::Get),
72            "HEAD" => Ok(Method::Head),
73            "POST" => Ok(Method::Post),
74            "PUT" => Ok(Method::Put),
75            "DELETE" => Ok(Method::Delete),
76            "CONNECT" => Ok(Method::Connect),
77            "OPTIONS" => Ok(Method::Options),
78            "TRACE" => Ok(Method::Trace),
79            "PATCH" => Ok(Method::Patch),
80            _ => {
81                if is_token(token) {
82                    Ok(Method::Extension(token.into()))
83                } else {
84                    Err(MethodError::InvalidToken)
85                }
86            }
87        }
88    }
89}
90
91impl fmt::Display for Method {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        f.write_str(self.as_str())
94    }
95}
96
97impl FromStr for Method {
98    type Err = MethodError;
99
100    fn from_str(s: &str) -> Result<Self, Self::Err> {
101        Method::from_token(s)
102    }
103}
104
105/// Errors that can occur while parsing methods.
106#[derive(Debug, Error, PartialEq, Eq)]
107pub enum MethodError {
108    #[error("invalid method token")]
109    InvalidToken,
110}
111
112/// HTTP protocol version representation (RFC 9112 Section 2.3).
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum HttpVersion {
115    Http10,
116    Http11,
117    Other(u8, u8),
118}
119
120impl HttpVersion {
121    /// Returns the major component of the version.
122    pub fn major(self) -> u8 {
123        match self {
124            HttpVersion::Http10 => 1,
125            HttpVersion::Http11 => 1,
126            HttpVersion::Other(major, _) => major,
127        }
128    }
129
130    /// Returns the minor component of the version.
131    pub fn minor(self) -> u8 {
132        match self {
133            HttpVersion::Http10 => 0,
134            HttpVersion::Http11 => 1,
135            HttpVersion::Other(_, minor) => minor,
136        }
137    }
138
139    /// Returns a static HTTP/1.1 variant.
140    pub const HTTP_1_1: HttpVersion = HttpVersion::Http11;
141}
142
143impl fmt::Display for HttpVersion {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            HttpVersion::Http10 => f.write_str("HTTP/1.0"),
147            HttpVersion::Http11 => f.write_str("HTTP/1.1"),
148            HttpVersion::Other(major, minor) => write!(f, "HTTP/{major}.{minor}"),
149        }
150    }
151}
152
153impl FromStr for HttpVersion {
154    type Err = VersionError;
155
156    fn from_str(s: &str) -> Result<Self, Self::Err> {
157        if !s.starts_with("HTTP/") {
158            return Err(VersionError::InvalidPrefix);
159        }
160        let remainder = &s[5..];
161        let mut parts = remainder.split('.');
162        let major = parts
163            .next()
164            .ok_or(VersionError::InvalidFormat)?
165            .parse::<u8>()
166            .map_err(|_| VersionError::InvalidNumber)?;
167        let minor = parts
168            .next()
169            .ok_or(VersionError::InvalidFormat)?
170            .parse::<u8>()
171            .map_err(|_| VersionError::InvalidNumber)?;
172        if parts.next().is_some() {
173            return Err(VersionError::InvalidFormat);
174        }
175        Ok(match (major, minor) {
176            (1, 0) => HttpVersion::Http10,
177            (1, 1) => HttpVersion::Http11,
178            (maj, min) => HttpVersion::Other(maj, min),
179        })
180    }
181}
182
183/// Errors while parsing HTTP versions.
184#[derive(Debug, Error, PartialEq, Eq)]
185pub enum VersionError {
186    #[error("invalid HTTP version prefix")]
187    InvalidPrefix,
188    #[error("invalid HTTP version format")]
189    InvalidFormat,
190    #[error("invalid HTTP version number")]
191    InvalidNumber,
192}
193
194/// Status codes (RFC 9110 Section 15).
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
196pub struct StatusCode(u16);
197
198impl StatusCode {
199    /// Creates a new status code if it falls within the valid range 100-599.
200    pub fn from_u16(code: u16) -> Result<Self, StatusCodeError> {
201        if (100..=599).contains(&code) {
202            Ok(StatusCode(code))
203        } else {
204            Err(StatusCodeError::OutOfRange(code))
205        }
206    }
207
208    /// Returns the inner numeric code.
209    pub fn as_u16(self) -> u16 {
210        self.0
211    }
212
213    /// Returns the canonical reason phrase if known.
214    pub fn canonical_reason(self) -> Option<&'static str> {
215        match self.0 {
216            100 => Some("Continue"),
217            101 => Some("Switching Protocols"),
218            102 => Some("Processing"),
219            103 => Some("Early Hints"),
220            200 => Some("OK"),
221            201 => Some("Created"),
222            202 => Some("Accepted"),
223            203 => Some("Non-Authoritative Information"),
224            204 => Some("No Content"),
225            205 => Some("Reset Content"),
226            206 => Some("Partial Content"),
227            300 => Some("Multiple Choices"),
228            301 => Some("Moved Permanently"),
229            302 => Some("Found"),
230            303 => Some("See Other"),
231            304 => Some("Not Modified"),
232            305 => Some("Use Proxy"),
233            307 => Some("Temporary Redirect"),
234            308 => Some("Permanent Redirect"),
235            400 => Some("Bad Request"),
236            401 => Some("Unauthorized"),
237            402 => Some("Payment Required"),
238            403 => Some("Forbidden"),
239            404 => Some("Not Found"),
240            405 => Some("Method Not Allowed"),
241            406 => Some("Not Acceptable"),
242            407 => Some("Proxy Authentication Required"),
243            408 => Some("Request Timeout"),
244            409 => Some("Conflict"),
245            410 => Some("Gone"),
246            411 => Some("Length Required"),
247            412 => Some("Precondition Failed"),
248            413 => Some("Content Too Large"),
249            414 => Some("URI Too Long"),
250            415 => Some("Unsupported Media Type"),
251            416 => Some("Range Not Satisfiable"),
252            417 => Some("Expectation Failed"),
253            418 => Some("I'm a teapot"),
254            421 => Some("Misdirected Request"),
255            422 => Some("Unprocessable Content"),
256            426 => Some("Upgrade Required"),
257            428 => Some("Precondition Required"),
258            429 => Some("Too Many Requests"),
259            431 => Some("Request Header Fields Too Large"),
260            451 => Some("Unavailable For Legal Reasons"),
261            500 => Some("Internal Server Error"),
262            501 => Some("Not Implemented"),
263            502 => Some("Bad Gateway"),
264            503 => Some("Service Unavailable"),
265            504 => Some("Gateway Timeout"),
266            505 => Some("HTTP Version Not Supported"),
267            511 => Some("Network Authentication Required"),
268            _ => None,
269        }
270    }
271
272    pub const CONTINUE: StatusCode = StatusCode(100);
273    pub const OK: StatusCode = StatusCode(200);
274    pub const CREATED: StatusCode = StatusCode(201);
275    pub const PARTIAL_CONTENT: StatusCode = StatusCode(206);
276    pub const NOT_FOUND: StatusCode = StatusCode(404);
277    pub const NOT_MODIFIED: StatusCode = StatusCode(304);
278    pub const BAD_REQUEST: StatusCode = StatusCode(400);
279    pub const REQUEST_TIMEOUT: StatusCode = StatusCode(408);
280    pub const METHOD_NOT_ALLOWED: StatusCode = StatusCode(405);
281    pub const PRECONDITION_FAILED: StatusCode = StatusCode(412);
282    pub const RANGE_NOT_SATISFIABLE: StatusCode = StatusCode(416);
283    pub const EXPECTATION_FAILED: StatusCode = StatusCode(417);
284    pub const INTERNAL_SERVER_ERROR: StatusCode = StatusCode(500);
285    pub const NOT_IMPLEMENTED: StatusCode = StatusCode(501);
286}
287
288impl fmt::Display for StatusCode {
289    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290        write!(f, "{}", self.0)
291    }
292}
293
294impl TryFrom<u16> for StatusCode {
295    type Error = StatusCodeError;
296
297    fn try_from(value: u16) -> Result<Self, Self::Error> {
298        StatusCode::from_u16(value)
299    }
300}
301
302impl From<StatusCode> for u16 {
303    fn from(code: StatusCode) -> Self {
304        code.0
305    }
306}
307
308/// Errors that occur when constructing status codes.
309#[derive(Debug, Error, PartialEq, Eq)]
310pub enum StatusCodeError {
311    #[error("status code {0} is outside the valid range 100-599")]
312    OutOfRange(u16),
313}
314
315fn is_token(value: &str) -> bool {
316    !value.is_empty() && value.bytes().all(is_tchar)
317}
318
319const fn is_tchar(byte: u8) -> bool {
320    matches!(
321        byte,
322        b'!' | b'#' | b'$' | b'%' | b'&' | b'\'' | b'*' | b'+' | b'-' | b'.' | b'^' | b'_' | b'`'
323            | b'|' | b'~'
324            | b'0'..=b'9'
325            | b'A'..=b'Z'
326            | b'a'..=b'z'
327    )
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn method_parsing_standard() {
336        assert_eq!(Method::from_str("GET").unwrap(), Method::Get);
337        assert_eq!(Method::from_str("POST").unwrap(), Method::Post);
338    }
339
340    #[test]
341    fn method_parsing_extension() {
342        let ext = Method::from_str("FOO").unwrap();
343        assert!(matches!(ext, Method::Extension(_)));
344        assert_eq!(ext.as_str(), "FOO");
345    }
346
347    #[test]
348    fn method_invalid_token() {
349        assert_eq!(Method::from_str("inv alid"), Err(MethodError::InvalidToken));
350        assert_eq!(Method::from_str(""), Err(MethodError::InvalidToken));
351    }
352
353    #[test]
354    fn version_parsing() {
355        assert_eq!(
356            HttpVersion::from_str("HTTP/1.1").unwrap(),
357            HttpVersion::Http11
358        );
359        assert_eq!(
360            HttpVersion::from_str("HTTP/1.0").unwrap(),
361            HttpVersion::Http10
362        );
363        assert_eq!(
364            HttpVersion::from_str("HTTP/2.0").unwrap(),
365            HttpVersion::Other(2, 0)
366        );
367        assert!(HttpVersion::from_str("HTTP/1").is_err());
368    }
369
370    #[test]
371    fn status_code_bounds() {
372        assert!(StatusCode::from_u16(99).is_err());
373        assert!(StatusCode::from_u16(600).is_err());
374        assert_eq!(StatusCode::from_u16(200).unwrap().as_u16(), 200);
375    }
376}