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