Skip to main content

rustlavel_http/
status.rs

1/// An HTTP status code, kept as a plain `u16` so any code can be expressed.
2#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
3pub struct Status(pub u16);
4
5impl Status {
6    /// The handshake that hands a connection to another protocol.
7    pub const SWITCHING_PROTOCOLS: Status = Status(101);
8    pub const OK: Status = Status(200);
9    pub const CREATED: Status = Status(201);
10    pub const NO_CONTENT: Status = Status(204);
11    pub const FOUND: Status = Status(302);
12    pub const SEE_OTHER: Status = Status(303);
13    pub const NOT_MODIFIED: Status = Status(304);
14    pub const BAD_REQUEST: Status = Status(400);
15    pub const UNAUTHORIZED: Status = Status(401);
16    pub const FORBIDDEN: Status = Status(403);
17    pub const NOT_FOUND: Status = Status(404);
18    pub const METHOD_NOT_ALLOWED: Status = Status(405);
19    pub const PAYLOAD_TOO_LARGE: Status = Status(413);
20    /// Laravel's status for a rejected CSRF token: the form was rendered too
21    /// long ago, or in another session.
22    pub const PAGE_EXPIRED: Status = Status(419);
23    pub const UNPROCESSABLE: Status = Status(422);
24    pub const TOO_MANY_REQUESTS: Status = Status(429);
25    pub const INTERNAL_ERROR: Status = Status(500);
26    pub const SERVICE_UNAVAILABLE: Status = Status(503);
27
28    pub fn code(self) -> u16 {
29        self.0
30    }
31
32    pub fn is_success(self) -> bool {
33        (200..300).contains(&self.0)
34    }
35
36    pub fn is_redirect(self) -> bool {
37        (300..400).contains(&self.0)
38    }
39
40    pub fn is_error(self) -> bool {
41        self.0 >= 400
42    }
43
44    /// Responses with these statuses must not carry a body.
45    pub fn is_bodyless(self) -> bool {
46        matches!(self.0, 204 | 304) || (100..200).contains(&self.0)
47    }
48
49    pub fn reason(self) -> &'static str {
50        match self.0 {
51            100 => "Continue",
52            101 => "Switching Protocols",
53            200 => "OK",
54            201 => "Created",
55            202 => "Accepted",
56            204 => "No Content",
57            301 => "Moved Permanently",
58            302 => "Found",
59            303 => "See Other",
60            304 => "Not Modified",
61            307 => "Temporary Redirect",
62            308 => "Permanent Redirect",
63            400 => "Bad Request",
64            401 => "Unauthorized",
65            403 => "Forbidden",
66            404 => "Not Found",
67            405 => "Method Not Allowed",
68            408 => "Request Timeout",
69            409 => "Conflict",
70            413 => "Payload Too Large",
71            415 => "Unsupported Media Type",
72            419 => "Page Expired",
73            422 => "Unprocessable Content",
74            429 => "Too Many Requests",
75            500 => "Internal Server Error",
76            501 => "Not Implemented",
77            502 => "Bad Gateway",
78            503 => "Service Unavailable",
79            504 => "Gateway Timeout",
80            _ => "Unknown",
81        }
82    }
83}
84
85impl From<u16> for Status {
86    fn from(code: u16) -> Self {
87        Status(code)
88    }
89}
90
91impl std::fmt::Display for Status {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{} {}", self.0, self.reason())
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn every_status_the_framework_sends_has_a_reason_phrase() {
103        // A missing arm renders as "101 Unknown" on the wire, which is what a
104        // WebSocket handshake looked like before this was noticed.
105        for status in [
106            Status::SWITCHING_PROTOCOLS,
107            Status::OK,
108            Status::CREATED,
109            Status::NO_CONTENT,
110            Status::FOUND,
111            Status::SEE_OTHER,
112            Status::NOT_MODIFIED,
113            Status::BAD_REQUEST,
114            Status::UNAUTHORIZED,
115            Status::FORBIDDEN,
116            Status::NOT_FOUND,
117            Status::METHOD_NOT_ALLOWED,
118            Status::PAYLOAD_TOO_LARGE,
119            Status::PAGE_EXPIRED,
120            Status::UNPROCESSABLE,
121            Status::TOO_MANY_REQUESTS,
122            Status::INTERNAL_ERROR,
123            Status::SERVICE_UNAVAILABLE,
124        ] {
125            assert_ne!(status.reason(), "Unknown", "{} has no reason phrase", status.code());
126        }
127    }
128
129    #[test]
130    fn an_informational_status_carries_no_body() {
131        assert!(Status::SWITCHING_PROTOCOLS.is_bodyless());
132        assert!(Status::NO_CONTENT.is_bodyless());
133        assert!(!Status::OK.is_bodyless());
134    }
135}