Skip to main content

http_net/
status.rs

1//! The HTTP status code registry: a named constant for every registered
2//! code, the canonical reason phrases, and status classes.
3//!
4//! Status codes are plain `u16` values throughout the crate; these
5//! constants give them names. The registry is the IANA HTTP Status Code
6//! Registry, with the names RFC 9110 section 15 uses (`413 Content Too
7//! Large`, `422 Unprocessable Content`). `306` and `418` are reserved and
8//! unused there, so they have no constant and no phrase.
9//!
10//! # Examples
11//!
12//! ```
13//! use http_net::{Response, reason_phrase, status};
14//!
15//! let r = Response::new(status::NO_CONTENT);
16//! assert_eq!(r.status(), 204);
17//! assert_eq!(reason_phrase(status::TOO_EARLY), "Too Early");
18//! assert_eq!(status::REQUEST_HEADER_FIELDS_TOO_LARGE, 431);
19//! ```
20
21/// One table drives both the constants and `reason_phrase`, so the two can
22/// never disagree.
23macro_rules! registry {
24    ($(($code:literal, $name:ident, $phrase:literal)),* $(,)?) => {
25        $(
26            #[doc = concat!("`", stringify!($code), " ", $phrase, "`")]
27            pub const $name: u16 = $code;
28        )*
29
30        /// The canonical reason phrase for a status code: every code in
31        /// the IANA HTTP Status Code Registry, with the RFC 9110 names;
32        /// `""` for an unassigned code and for the reserved `306` and
33        /// `418`.
34        ///
35        /// When the phrase is empty the serializer still writes the space
36        /// after the code (`HTTP/1.1 299 ` CRLF), as RFC 9112 section 4
37        /// requires.
38        ///
39        /// # Examples
40        ///
41        /// ```
42        /// use http_net::reason_phrase;
43        ///
44        /// assert_eq!(reason_phrase(404), "Not Found");
45        /// assert_eq!(reason_phrase(103), "Early Hints");
46        /// assert_eq!(reason_phrase(418), "");
47        /// assert_eq!(reason_phrase(299), "");
48        /// ```
49        #[must_use]
50        pub const fn reason_phrase(status: u16) -> &'static str {
51            match status {
52                $($code => $phrase,)*
53                _ => "",
54            }
55        }
56
57        #[cfg(test)]
58        const REGISTERED: &[(u16, &str)] = &[$(($code, $phrase)),*];
59    };
60}
61
62registry! {
63    (100, CONTINUE, "Continue"),
64    (101, SWITCHING_PROTOCOLS, "Switching Protocols"),
65    (102, PROCESSING, "Processing"),
66    (103, EARLY_HINTS, "Early Hints"),
67    (200, OK, "OK"),
68    (201, CREATED, "Created"),
69    (202, ACCEPTED, "Accepted"),
70    (203, NON_AUTHORITATIVE_INFORMATION, "Non-Authoritative Information"),
71    (204, NO_CONTENT, "No Content"),
72    (205, RESET_CONTENT, "Reset Content"),
73    (206, PARTIAL_CONTENT, "Partial Content"),
74    (207, MULTI_STATUS, "Multi-Status"),
75    (208, ALREADY_REPORTED, "Already Reported"),
76    (226, IM_USED, "IM Used"),
77    (300, MULTIPLE_CHOICES, "Multiple Choices"),
78    (301, MOVED_PERMANENTLY, "Moved Permanently"),
79    (302, FOUND, "Found"),
80    (303, SEE_OTHER, "See Other"),
81    (304, NOT_MODIFIED, "Not Modified"),
82    (305, USE_PROXY, "Use Proxy"),
83    (307, TEMPORARY_REDIRECT, "Temporary Redirect"),
84    (308, PERMANENT_REDIRECT, "Permanent Redirect"),
85    (400, BAD_REQUEST, "Bad Request"),
86    (401, UNAUTHORIZED, "Unauthorized"),
87    (402, PAYMENT_REQUIRED, "Payment Required"),
88    (403, FORBIDDEN, "Forbidden"),
89    (404, NOT_FOUND, "Not Found"),
90    (405, METHOD_NOT_ALLOWED, "Method Not Allowed"),
91    (406, NOT_ACCEPTABLE, "Not Acceptable"),
92    (407, PROXY_AUTHENTICATION_REQUIRED, "Proxy Authentication Required"),
93    (408, REQUEST_TIMEOUT, "Request Timeout"),
94    (409, CONFLICT, "Conflict"),
95    (410, GONE, "Gone"),
96    (411, LENGTH_REQUIRED, "Length Required"),
97    (412, PRECONDITION_FAILED, "Precondition Failed"),
98    (413, CONTENT_TOO_LARGE, "Content Too Large"),
99    (414, URI_TOO_LONG, "URI Too Long"),
100    (415, UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"),
101    (416, RANGE_NOT_SATISFIABLE, "Range Not Satisfiable"),
102    (417, EXPECTATION_FAILED, "Expectation Failed"),
103    (421, MISDIRECTED_REQUEST, "Misdirected Request"),
104    (422, UNPROCESSABLE_CONTENT, "Unprocessable Content"),
105    (423, LOCKED, "Locked"),
106    (424, FAILED_DEPENDENCY, "Failed Dependency"),
107    (425, TOO_EARLY, "Too Early"),
108    (426, UPGRADE_REQUIRED, "Upgrade Required"),
109    (428, PRECONDITION_REQUIRED, "Precondition Required"),
110    (429, TOO_MANY_REQUESTS, "Too Many Requests"),
111    (431, REQUEST_HEADER_FIELDS_TOO_LARGE, "Request Header Fields Too Large"),
112    (451, UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable For Legal Reasons"),
113    (500, INTERNAL_SERVER_ERROR, "Internal Server Error"),
114    (501, NOT_IMPLEMENTED, "Not Implemented"),
115    (502, BAD_GATEWAY, "Bad Gateway"),
116    (503, SERVICE_UNAVAILABLE, "Service Unavailable"),
117    (504, GATEWAY_TIMEOUT, "Gateway Timeout"),
118    (505, HTTP_VERSION_NOT_SUPPORTED, "HTTP Version Not Supported"),
119    (506, VARIANT_ALSO_NEGOTIATES, "Variant Also Negotiates"),
120    (507, INSUFFICIENT_STORAGE, "Insufficient Storage"),
121    (508, LOOP_DETECTED, "Loop Detected"),
122    (510, NOT_EXTENDED, "Not Extended"),
123    (511, NETWORK_AUTHENTICATION_REQUIRED, "Network Authentication Required"),
124}
125
126/// The class of a status code (RFC 9110 section 15).
127///
128/// # Examples
129///
130/// ```
131/// use http_net::{StatusClass, status_class};
132///
133/// assert_eq!(status_class(302), Some(StatusClass::Redirection));
134/// ```
135#[non_exhaustive]
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
137pub enum StatusClass {
138    /// `1xx`: interim, the request was received and processing continues.
139    Informational,
140    /// `2xx`: the request was successfully received, understood, and accepted.
141    Success,
142    /// `3xx`: further action is needed to complete the request.
143    Redirection,
144    /// `4xx`: the request is wrong or cannot be fulfilled.
145    ClientError,
146    /// `5xx`: the server failed to fulfil a valid request.
147    ServerError,
148}
149
150/// The class of `status`, or `None` outside `100..=599`.
151///
152/// # Examples
153///
154/// ```
155/// use http_net::{StatusClass, status_class};
156///
157/// assert_eq!(status_class(204), Some(StatusClass::Success));
158/// assert_eq!(status_class(503), Some(StatusClass::ServerError));
159/// assert_eq!(status_class(99), None);
160/// assert_eq!(status_class(600), None);
161/// ```
162#[must_use]
163pub const fn status_class(status: u16) -> Option<StatusClass> {
164    Some(match status {
165        100..=199 => StatusClass::Informational,
166        200..=299 => StatusClass::Success,
167        300..=399 => StatusClass::Redirection,
168        400..=499 => StatusClass::ClientError,
169        500..=599 => StatusClass::ServerError,
170        _ => return None,
171    })
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_reason_phrase_registry_has_every_iana_code() {
180        // The 0.3.0 additions, each checked by value.
181        let added = [
182            (102, "Processing"),
183            (103, "Early Hints"),
184            (207, "Multi-Status"),
185            (208, "Already Reported"),
186            (226, "IM Used"),
187            (305, "Use Proxy"),
188            (402, "Payment Required"),
189            (407, "Proxy Authentication Required"),
190            (423, "Locked"),
191            (424, "Failed Dependency"),
192            (425, "Too Early"),
193            (451, "Unavailable For Legal Reasons"),
194            (506, "Variant Also Negotiates"),
195            (507, "Insufficient Storage"),
196            (508, "Loop Detected"),
197            (510, "Not Extended"),
198            (511, "Network Authentication Required"),
199        ];
200        for (code, phrase) in added {
201            assert_eq!(reason_phrase(code), phrase, "{code}");
202        }
203        assert_eq!(REGISTERED.len(), 61);
204        for &(code, phrase) in REGISTERED {
205            assert_eq!(reason_phrase(code), phrase);
206            assert!(status_class(code).is_some());
207        }
208    }
209
210    #[test]
211    fn test_reason_phrase_unassigned_and_reserved_are_empty() {
212        for code in [
213            0, 99, 104, 199, 209, 299, 306, 399, 418, 420, 499, 509, 599, 600, 999,
214        ] {
215            assert_eq!(reason_phrase(code), "", "{code}");
216        }
217    }
218
219    #[test]
220    fn test_status_class_boundaries() {
221        assert_eq!(status_class(100), Some(StatusClass::Informational));
222        assert_eq!(status_class(199), Some(StatusClass::Informational));
223        assert_eq!(status_class(200), Some(StatusClass::Success));
224        assert_eq!(status_class(399), Some(StatusClass::Redirection));
225        assert_eq!(status_class(400), Some(StatusClass::ClientError));
226        assert_eq!(status_class(599), Some(StatusClass::ServerError));
227        assert_eq!(status_class(0), None);
228    }
229}