Skip to main content

http/
method.rs

1//! The HTTP request method
2//!
3//! This module contains HTTP-method related structs and errors and such. The
4//! main type of this module, `Method`, is also reexported at the root of the
5//! crate as `http::Method` and is intended for import through that location
6//! primarily.
7//!
8//! # Examples
9//!
10//! ```
11//! use http::Method;
12//!
13//! assert_eq!(Method::GET, Method::from_bytes(b"GET").unwrap());
14//! assert!(Method::GET.is_idempotent());
15//! assert_eq!(Method::POST.as_str(), "POST");
16//! ```
17
18use self::extension::{AllocatedExtension, InlineExtension};
19use self::Inner::*;
20
21use std::convert::TryFrom;
22use std::error::Error;
23use std::str::FromStr;
24use std::{fmt, str};
25
26/// The Request Method (VERB)
27///
28/// This type also contains constants for a number of common HTTP methods such
29/// as GET, POST, etc.
30///
31/// Currently includes 8 variants representing the 8 methods defined in
32/// [RFC 7230](https://tools.ietf.org/html/rfc7231#section-4.1), plus PATCH,
33/// and an Extension variant for all extensions.
34///
35/// # Examples
36///
37/// ```
38/// use http::Method;
39///
40/// assert_eq!(Method::GET, Method::from_bytes(b"GET").unwrap());
41/// assert!(Method::GET.is_idempotent());
42/// assert_eq!(Method::POST.as_str(), "POST");
43/// ```
44#[derive(Clone, PartialEq, Eq, Hash)]
45pub struct Method(Inner);
46
47/// A possible error value when converting `Method` from bytes.
48pub struct InvalidMethod {
49    _priv: (),
50}
51
52#[derive(Clone, PartialEq, Eq, Hash)]
53enum Inner {
54    Options,
55    Get,
56    Post,
57    Put,
58    Delete,
59    Head,
60    Trace,
61    Connect,
62    Patch,
63    Query,
64    // If the extension is short enough, store it inline
65    ExtensionInline(InlineExtension),
66    // Otherwise, allocate it
67    ExtensionAllocated(AllocatedExtension),
68}
69
70impl Method {
71    /// GET
72    pub const GET: Method = Method(Get);
73
74    /// POST
75    pub const POST: Method = Method(Post);
76
77    /// PUT
78    pub const PUT: Method = Method(Put);
79
80    /// DELETE
81    pub const DELETE: Method = Method(Delete);
82
83    /// HEAD
84    pub const HEAD: Method = Method(Head);
85
86    /// OPTIONS
87    pub const OPTIONS: Method = Method(Options);
88
89    /// CONNECT
90    pub const CONNECT: Method = Method(Connect);
91
92    /// PATCH
93    pub const PATCH: Method = Method(Patch);
94
95    /// TRACE
96    pub const TRACE: Method = Method(Trace);
97
98    /// QUERY
99    pub const QUERY: Method = Method(Query);
100
101    /// Converts a slice of bytes to an HTTP method.
102    pub fn from_bytes(src: &[u8]) -> Result<Method, InvalidMethod> {
103        match src.len() {
104            0 => Err(InvalidMethod::new()),
105            3 => match src {
106                b"GET" => Ok(Method(Get)),
107                b"PUT" => Ok(Method(Put)),
108                _ => Method::extension_inline(src),
109            },
110            4 => match src {
111                b"POST" => Ok(Method(Post)),
112                b"HEAD" => Ok(Method(Head)),
113                _ => Method::extension_inline(src),
114            },
115            5 => match src {
116                b"PATCH" => Ok(Method(Patch)),
117                b"TRACE" => Ok(Method(Trace)),
118                b"QUERY" => Ok(Method(Query)),
119                _ => Method::extension_inline(src),
120            },
121            6 => match src {
122                b"DELETE" => Ok(Method(Delete)),
123                _ => Method::extension_inline(src),
124            },
125            7 => match src {
126                b"OPTIONS" => Ok(Method(Options)),
127                b"CONNECT" => Ok(Method(Connect)),
128                _ => Method::extension_inline(src),
129            },
130            _ => {
131                if src.len() <= InlineExtension::MAX {
132                    Method::extension_inline(src)
133                } else {
134                    let allocated = AllocatedExtension::new(src)?;
135
136                    Ok(Method(ExtensionAllocated(allocated)))
137                }
138            }
139        }
140    }
141
142    fn extension_inline(src: &[u8]) -> Result<Method, InvalidMethod> {
143        let inline = InlineExtension::new(src)?;
144
145        Ok(Method(ExtensionInline(inline)))
146    }
147
148    /// Whether a method is considered "safe", meaning the request is
149    /// essentially read-only.
150    ///
151    /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.1)
152    /// for more words.
153    pub fn is_safe(&self) -> bool {
154        matches!(self.0, Get | Head | Options | Trace | Query)
155    }
156
157    /// Whether a method is considered "idempotent", meaning the request has
158    /// the same result if executed multiple times.
159    ///
160    /// See [the spec](https://tools.ietf.org/html/rfc7231#section-4.2.2) for
161    /// more words.
162    pub fn is_idempotent(&self) -> bool {
163        match self.0 {
164            Put | Delete => true,
165            _ => self.is_safe(),
166        }
167    }
168
169    /// Return a &str representation of the HTTP method
170    #[inline]
171    pub fn as_str(&self) -> &str {
172        match self.0 {
173            Options => "OPTIONS",
174            Get => "GET",
175            Post => "POST",
176            Put => "PUT",
177            Delete => "DELETE",
178            Head => "HEAD",
179            Trace => "TRACE",
180            Connect => "CONNECT",
181            Patch => "PATCH",
182            Query => "QUERY",
183            ExtensionInline(ref inline) => inline.as_str(),
184            ExtensionAllocated(ref allocated) => allocated.as_str(),
185        }
186    }
187}
188
189impl AsRef<str> for Method {
190    #[inline]
191    fn as_ref(&self) -> &str {
192        self.as_str()
193    }
194}
195
196impl Ord for Method {
197    #[inline]
198    fn cmp(&self, other: &Method) -> std::cmp::Ordering {
199        self.as_ref().cmp(other.as_ref())
200    }
201}
202
203impl PartialOrd for Method {
204    #[inline]
205    fn partial_cmp(&self, other: &Method) -> Option<std::cmp::Ordering> {
206        Some(self.cmp(other))
207    }
208}
209
210impl PartialEq<&Method> for Method {
211    #[inline]
212    fn eq(&self, other: &&Method) -> bool {
213        self == *other
214    }
215}
216
217impl PartialEq<Method> for &Method {
218    #[inline]
219    fn eq(&self, other: &Method) -> bool {
220        *self == other
221    }
222}
223
224impl PartialEq<str> for Method {
225    #[inline]
226    fn eq(&self, other: &str) -> bool {
227        self.as_ref() == other
228    }
229}
230
231impl PartialEq<Method> for str {
232    #[inline]
233    fn eq(&self, other: &Method) -> bool {
234        self == other.as_ref()
235    }
236}
237
238impl PartialEq<&str> for Method {
239    #[inline]
240    fn eq(&self, other: &&str) -> bool {
241        self.as_ref() == *other
242    }
243}
244
245impl PartialEq<Method> for &str {
246    #[inline]
247    fn eq(&self, other: &Method) -> bool {
248        *self == other.as_ref()
249    }
250}
251
252impl fmt::Debug for Method {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        f.write_str(self.as_ref())
255    }
256}
257
258impl fmt::Display for Method {
259    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
260        fmt.write_str(self.as_ref())
261    }
262}
263
264impl Default for Method {
265    #[inline]
266    fn default() -> Method {
267        Method::GET
268    }
269}
270
271impl From<&Method> for Method {
272    #[inline]
273    fn from(t: &Method) -> Self {
274        t.clone()
275    }
276}
277
278impl TryFrom<&[u8]> for Method {
279    type Error = InvalidMethod;
280
281    #[inline]
282    fn try_from(t: &[u8]) -> Result<Self, Self::Error> {
283        Method::from_bytes(t)
284    }
285}
286
287impl TryFrom<&str> for Method {
288    type Error = InvalidMethod;
289
290    #[inline]
291    fn try_from(t: &str) -> Result<Self, Self::Error> {
292        TryFrom::try_from(t.as_bytes())
293    }
294}
295
296impl FromStr for Method {
297    type Err = InvalidMethod;
298
299    #[inline]
300    fn from_str(t: &str) -> Result<Self, Self::Err> {
301        TryFrom::try_from(t)
302    }
303}
304
305impl InvalidMethod {
306    fn new() -> InvalidMethod {
307        InvalidMethod { _priv: () }
308    }
309}
310
311impl fmt::Debug for InvalidMethod {
312    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
313        f.debug_struct("InvalidMethod")
314            // skip _priv noise
315            .finish()
316    }
317}
318
319impl fmt::Display for InvalidMethod {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        f.write_str("invalid HTTP method")
322    }
323}
324
325impl Error for InvalidMethod {}
326
327mod extension {
328    use super::InvalidMethod;
329    use std::str;
330
331    #[derive(Clone, PartialEq, Eq, Hash)]
332    // Invariant: the first self.1 bytes of self.0 are valid UTF-8.
333    pub struct InlineExtension([u8; InlineExtension::MAX], u8);
334
335    #[derive(Clone, PartialEq, Eq, Hash)]
336    // Invariant: self.0 contains valid UTF-8.
337    pub struct AllocatedExtension(Box<[u8]>);
338
339    impl InlineExtension {
340        // Method::from_bytes() assumes this is at least 7
341        pub const MAX: usize = 15;
342
343        pub fn new(src: &[u8]) -> Result<InlineExtension, InvalidMethod> {
344            let mut data: [u8; InlineExtension::MAX] = Default::default();
345
346            write_checked(src, &mut data)?;
347
348            // Invariant: write_checked ensures that the first src.len() bytes
349            // of data are valid UTF-8.
350            Ok(InlineExtension(data, src.len() as u8))
351        }
352
353        pub fn as_str(&self) -> &str {
354            let InlineExtension(ref data, len) = self;
355            // Safety: the invariant of InlineExtension ensures that the first
356            // len bytes of data contain valid UTF-8.
357            unsafe { str::from_utf8_unchecked(&data[..*len as usize]) }
358        }
359    }
360
361    impl AllocatedExtension {
362        pub fn new(src: &[u8]) -> Result<AllocatedExtension, InvalidMethod> {
363            let mut data: Vec<u8> = vec![0; src.len()];
364
365            write_checked(src, &mut data)?;
366
367            // Invariant: data is exactly src.len() long and write_checked
368            // ensures that the first src.len() bytes of data are valid UTF-8.
369            Ok(AllocatedExtension(data.into_boxed_slice()))
370        }
371
372        pub fn as_str(&self) -> &str {
373            // Safety: the invariant of AllocatedExtension ensures that self.0
374            // contains valid UTF-8.
375            unsafe { str::from_utf8_unchecked(&self.0) }
376        }
377    }
378
379    // From the RFC 9110 HTTP Semantics, section 9.1, the HTTP method is case-sensitive and can
380    // contain the following characters:
381    //
382    // ```
383    // method = token
384    // token = 1*tchar
385    // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
386    //     "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA
387    // ```
388    //
389    // https://datatracker.ietf.org/doc/html/rfc9110#section-9.1
390    //
391    // Note that this definition means that any &[u8] that consists solely of valid
392    // characters is also valid UTF-8 because the valid method characters are a
393    // subset of the valid 1 byte UTF-8 encoding.
394    #[rustfmt::skip]
395    const METHOD_CHARS: [u8; 256] = [
396        //  0      1      2      3      4      5      6      7      8      9
397        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', //   x
398        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', //  1x
399        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', //  2x
400        b'\0', b'\0', b'\0',  b'!', b'\0',  b'#',  b'$',  b'%',  b'&', b'\'', //  3x
401        b'\0', b'\0',  b'*',  b'+', b'\0',  b'-',  b'.', b'\0',  b'0',  b'1', //  4x
402         b'2',  b'3',  b'4',  b'5',  b'6',  b'7',  b'8',  b'9', b'\0', b'\0', //  5x
403        b'\0', b'\0', b'\0', b'\0', b'\0',  b'A',  b'B',  b'C',  b'D',  b'E', //  6x
404         b'F',  b'G',  b'H',  b'I',  b'J',  b'K',  b'L',  b'M',  b'N',  b'O', //  7x
405         b'P',  b'Q',  b'R',  b'S',  b'T',  b'U',  b'V',  b'W',  b'X',  b'Y', //  8x
406         b'Z', b'\0', b'\0', b'\0',  b'^',  b'_',  b'`',  b'a',  b'b',  b'c', //  9x
407         b'd',  b'e',  b'f',  b'g',  b'h',  b'i',  b'j',  b'k',  b'l',  b'm', // 10x
408         b'n',  b'o',  b'p',  b'q',  b'r',  b's',  b't',  b'u',  b'v',  b'w', // 11x
409         b'x',  b'y',  b'z', b'\0',  b'|', b'\0',  b'~', b'\0', b'\0', b'\0', // 12x
410        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 13x
411        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 14x
412        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 15x
413        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 16x
414        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 17x
415        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 18x
416        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 19x
417        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 20x
418        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 21x
419        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 22x
420        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 23x
421        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', b'\0', // 24x
422        b'\0', b'\0', b'\0', b'\0', b'\0', b'\0'                              // 25x
423    ];
424
425    // write_checked ensures (among other things) that the first src.len() bytes
426    // of dst are valid UTF-8
427    fn write_checked(src: &[u8], dst: &mut [u8]) -> Result<(), InvalidMethod> {
428        for (i, &b) in src.iter().enumerate() {
429            let b = METHOD_CHARS[b as usize];
430
431            if b == 0 {
432                return Err(InvalidMethod::new());
433            }
434
435            dst[i] = b;
436        }
437
438        Ok(())
439    }
440}
441
442#[cfg(test)]
443mod test {
444    use super::*;
445
446    #[test]
447    fn test_method_eq() {
448        assert_eq!(Method::GET, Method::GET);
449        assert_eq!(Method::GET, "GET");
450        assert_eq!(&Method::GET, "GET");
451
452        assert_eq!("GET", Method::GET);
453        assert_eq!("GET", &Method::GET);
454
455        assert_eq!(&Method::GET, Method::GET);
456        assert_eq!(Method::GET, &Method::GET);
457    }
458
459    #[test]
460    fn test_invalid_method() {
461        assert!(Method::from_str("").is_err());
462        assert!(Method::from_bytes(b"").is_err());
463        assert!(Method::from_bytes(&[0xC0]).is_err()); // invalid utf-8
464        assert!(Method::from_bytes(&[0x10]).is_err()); // invalid method characters
465    }
466
467    #[test]
468    fn test_is_idempotent() {
469        assert!(Method::OPTIONS.is_idempotent());
470        assert!(Method::GET.is_idempotent());
471        assert!(Method::PUT.is_idempotent());
472        assert!(Method::DELETE.is_idempotent());
473        assert!(Method::HEAD.is_idempotent());
474        assert!(Method::TRACE.is_idempotent());
475        assert!(Method::QUERY.is_idempotent());
476
477        assert!(!Method::POST.is_idempotent());
478        assert!(!Method::CONNECT.is_idempotent());
479        assert!(!Method::PATCH.is_idempotent());
480    }
481
482    #[test]
483    fn test_extension_method() {
484        assert_eq!(Method::from_str("WOW").unwrap(), "WOW");
485        assert_eq!(Method::from_str("wOw!!").unwrap(), "wOw!!");
486
487        let long_method = "This_is_a_very_long_method.It_is_valid_but_unlikely.";
488        assert_eq!(Method::from_str(long_method).unwrap(), long_method);
489
490        let longest_inline_method = [b'A'; InlineExtension::MAX];
491        assert_eq!(
492            Method::from_bytes(&longest_inline_method).unwrap(),
493            Method(ExtensionInline(
494                InlineExtension::new(&longest_inline_method).unwrap()
495            ))
496        );
497        let shortest_allocated_method = [b'A'; InlineExtension::MAX + 1];
498        assert_eq!(
499            Method::from_bytes(&shortest_allocated_method).unwrap(),
500            Method(ExtensionAllocated(
501                AllocatedExtension::new(&shortest_allocated_method).unwrap()
502            ))
503        );
504    }
505
506    #[test]
507    fn test_extension_method_chars() {
508        const VALID_METHOD_CHARS: &str =
509            "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
510
511        for c in VALID_METHOD_CHARS.chars() {
512            let c = c.to_string();
513
514            assert_eq!(
515                Method::from_str(&c).unwrap(),
516                c.as_str(),
517                "testing {c} is a valid method character"
518            );
519        }
520    }
521}