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        /// The whole status line, `HTTP/1.1 <code> <phrase>` CRLF, for a
58        /// registered code, built at compile time so a response head starts
59        /// with one copy.
60        #[inline]
61        pub(crate) const fn status_line(status: u16) -> Option<&'static [u8]> {
62            match status {
63                $($code => Some(
64                    concat!("HTTP/1.1 ", stringify!($code), " ", $phrase, "\r\n").as_bytes()
65                ),)*
66                _ => None,
67            }
68        }
69
70        #[cfg(test)]
71        const REGISTERED: &[(u16, &str)] = &[$(($code, $phrase)),*];
72    };
73}
74
75registry! {
76    (100, CONTINUE, "Continue"),
77    (101, SWITCHING_PROTOCOLS, "Switching Protocols"),
78    (102, PROCESSING, "Processing"),
79    (103, EARLY_HINTS, "Early Hints"),
80    (200, OK, "OK"),
81    (201, CREATED, "Created"),
82    (202, ACCEPTED, "Accepted"),
83    (203, NON_AUTHORITATIVE_INFORMATION, "Non-Authoritative Information"),
84    (204, NO_CONTENT, "No Content"),
85    (205, RESET_CONTENT, "Reset Content"),
86    (206, PARTIAL_CONTENT, "Partial Content"),
87    (207, MULTI_STATUS, "Multi-Status"),
88    (208, ALREADY_REPORTED, "Already Reported"),
89    (226, IM_USED, "IM Used"),
90    (300, MULTIPLE_CHOICES, "Multiple Choices"),
91    (301, MOVED_PERMANENTLY, "Moved Permanently"),
92    (302, FOUND, "Found"),
93    (303, SEE_OTHER, "See Other"),
94    (304, NOT_MODIFIED, "Not Modified"),
95    (305, USE_PROXY, "Use Proxy"),
96    (307, TEMPORARY_REDIRECT, "Temporary Redirect"),
97    (308, PERMANENT_REDIRECT, "Permanent Redirect"),
98    (400, BAD_REQUEST, "Bad Request"),
99    (401, UNAUTHORIZED, "Unauthorized"),
100    (402, PAYMENT_REQUIRED, "Payment Required"),
101    (403, FORBIDDEN, "Forbidden"),
102    (404, NOT_FOUND, "Not Found"),
103    (405, METHOD_NOT_ALLOWED, "Method Not Allowed"),
104    (406, NOT_ACCEPTABLE, "Not Acceptable"),
105    (407, PROXY_AUTHENTICATION_REQUIRED, "Proxy Authentication Required"),
106    (408, REQUEST_TIMEOUT, "Request Timeout"),
107    (409, CONFLICT, "Conflict"),
108    (410, GONE, "Gone"),
109    (411, LENGTH_REQUIRED, "Length Required"),
110    (412, PRECONDITION_FAILED, "Precondition Failed"),
111    (413, CONTENT_TOO_LARGE, "Content Too Large"),
112    (414, URI_TOO_LONG, "URI Too Long"),
113    (415, UNSUPPORTED_MEDIA_TYPE, "Unsupported Media Type"),
114    (416, RANGE_NOT_SATISFIABLE, "Range Not Satisfiable"),
115    (417, EXPECTATION_FAILED, "Expectation Failed"),
116    (421, MISDIRECTED_REQUEST, "Misdirected Request"),
117    (422, UNPROCESSABLE_CONTENT, "Unprocessable Content"),
118    (423, LOCKED, "Locked"),
119    (424, FAILED_DEPENDENCY, "Failed Dependency"),
120    (425, TOO_EARLY, "Too Early"),
121    (426, UPGRADE_REQUIRED, "Upgrade Required"),
122    (428, PRECONDITION_REQUIRED, "Precondition Required"),
123    (429, TOO_MANY_REQUESTS, "Too Many Requests"),
124    (431, REQUEST_HEADER_FIELDS_TOO_LARGE, "Request Header Fields Too Large"),
125    (451, UNAVAILABLE_FOR_LEGAL_REASONS, "Unavailable For Legal Reasons"),
126    (500, INTERNAL_SERVER_ERROR, "Internal Server Error"),
127    (501, NOT_IMPLEMENTED, "Not Implemented"),
128    (502, BAD_GATEWAY, "Bad Gateway"),
129    (503, SERVICE_UNAVAILABLE, "Service Unavailable"),
130    (504, GATEWAY_TIMEOUT, "Gateway Timeout"),
131    (505, HTTP_VERSION_NOT_SUPPORTED, "HTTP Version Not Supported"),
132    (506, VARIANT_ALSO_NEGOTIATES, "Variant Also Negotiates"),
133    (507, INSUFFICIENT_STORAGE, "Insufficient Storage"),
134    (508, LOOP_DETECTED, "Loop Detected"),
135    (510, NOT_EXTENDED, "Not Extended"),
136    (511, NETWORK_AUTHENTICATION_REQUIRED, "Network Authentication Required"),
137}
138
139/// The class of a status code (RFC 9110 section 15).
140///
141/// # Examples
142///
143/// ```
144/// use http_net::{StatusClass, status_class};
145///
146/// assert_eq!(status_class(302), Some(StatusClass::Redirection));
147/// ```
148#[non_exhaustive]
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
150pub enum StatusClass {
151    /// `1xx`: interim, the request was received and processing continues.
152    Informational,
153    /// `2xx`: the request was successfully received, understood, and accepted.
154    Success,
155    /// `3xx`: further action is needed to complete the request.
156    Redirection,
157    /// `4xx`: the request is wrong or cannot be fulfilled.
158    ClientError,
159    /// `5xx`: the server failed to fulfil a valid request.
160    ServerError,
161}
162
163/// The class of `status`, or `None` outside `100..=599`.
164///
165/// # Examples
166///
167/// ```
168/// use http_net::{StatusClass, status_class};
169///
170/// assert_eq!(status_class(204), Some(StatusClass::Success));
171/// assert_eq!(status_class(503), Some(StatusClass::ServerError));
172/// assert_eq!(status_class(99), None);
173/// assert_eq!(status_class(600), None);
174/// ```
175#[must_use]
176pub const fn status_class(status: u16) -> Option<StatusClass> {
177    Some(match status {
178        100..=199 => StatusClass::Informational,
179        200..=299 => StatusClass::Success,
180        300..=399 => StatusClass::Redirection,
181        400..=499 => StatusClass::ClientError,
182        500..=599 => StatusClass::ServerError,
183        _ => return None,
184    })
185}
186
187/// Whether a response with `status` to a request with `request` method can
188/// carry a body (RFC 9112 section 6.3, rules 1 and 2): false for 1xx, 204,
189/// 304, any response to `HEAD`, and a 2xx to `CONNECT` (which turns the
190/// connection into a tunnel). For proxies and clients deciding how to read
191/// or forward a response.
192///
193/// # Examples
194///
195/// ```
196/// use http_net::{Method, status_allows_body};
197///
198/// assert!(status_allows_body(200, Method::Get));
199/// assert!(!status_allows_body(200, Method::Head));
200/// assert!(!status_allows_body(304, Method::Get));
201/// assert!(!status_allows_body(200, Method::Connect));
202/// assert!(status_allows_body(407, Method::Connect));
203/// ```
204#[must_use]
205pub const fn status_allows_body(status: u16, request: crate::Method) -> bool {
206    !(matches!(request, crate::Method::Head)
207        || (status >= 100 && status < 200)
208        || status == 204
209        || status == 304
210        || (matches!(request, crate::Method::Connect) && status >= 200 && status < 300))
211}
212
213/// Writes `HTTP/1.1 <code> <phrase>` CRLF: one copy from the compile-time
214/// table for a registered code, the parts otherwise (an unregistered code
215/// still gets the space after it, RFC 9112 section 4).
216#[inline]
217pub(crate) fn write_status_line<O: crate::Output + ?Sized>(out: &mut O, status: u16) {
218    if let Some(line) = status_line(status) {
219        out.put(line);
220    } else {
221        out.put(b"HTTP/1.1 ");
222        crate::syntax::push_decimal(out, u64::from(status));
223        out.put(b" \r\n");
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_status_line_table_matches_the_parts_for_every_code() {
233        use alloc::{format, vec::Vec};
234        for status in 100..=599u16 {
235            let mut out = Vec::new();
236            write_status_line(&mut out, status);
237            let expected = format!("HTTP/1.1 {status} {}\r\n", reason_phrase(status));
238            assert_eq!(out, expected.as_bytes(), "status {status}");
239            assert_eq!(
240                status_line(status).is_some(),
241                !reason_phrase(status).is_empty()
242            );
243        }
244    }
245
246    #[test]
247    fn test_status_allows_body_follows_rfc_9112_6_3() {
248        use crate::Method;
249        for status in 100..200 {
250            assert!(!status_allows_body(status, Method::Get));
251        }
252        for m in [Method::Get, Method::Post, Method::Other] {
253            assert!(status_allows_body(200, m));
254            assert!(!status_allows_body(204, m));
255            assert!(!status_allows_body(304, m));
256            assert!(status_allows_body(500, m));
257        }
258        assert!(!status_allows_body(500, Method::Head));
259        assert!(!status_allows_body(299, Method::Connect));
260        assert!(status_allows_body(300, Method::Connect));
261    }
262
263    #[test]
264    fn test_reason_phrase_registry_has_every_iana_code() {
265        // The 0.3.0 additions, each checked by value.
266        let added = [
267            (102, "Processing"),
268            (103, "Early Hints"),
269            (207, "Multi-Status"),
270            (208, "Already Reported"),
271            (226, "IM Used"),
272            (305, "Use Proxy"),
273            (402, "Payment Required"),
274            (407, "Proxy Authentication Required"),
275            (423, "Locked"),
276            (424, "Failed Dependency"),
277            (425, "Too Early"),
278            (451, "Unavailable For Legal Reasons"),
279            (506, "Variant Also Negotiates"),
280            (507, "Insufficient Storage"),
281            (508, "Loop Detected"),
282            (510, "Not Extended"),
283            (511, "Network Authentication Required"),
284        ];
285        for (code, phrase) in added {
286            assert_eq!(reason_phrase(code), phrase, "{code}");
287        }
288        assert_eq!(REGISTERED.len(), 61);
289        for &(code, phrase) in REGISTERED {
290            assert_eq!(reason_phrase(code), phrase);
291            assert!(status_class(code).is_some());
292        }
293    }
294
295    #[test]
296    fn test_reason_phrase_unassigned_and_reserved_are_empty() {
297        for code in [
298            0, 99, 104, 199, 209, 299, 306, 399, 418, 420, 499, 509, 599, 600, 999,
299        ] {
300            assert_eq!(reason_phrase(code), "", "{code}");
301        }
302    }
303
304    #[test]
305    fn test_status_class_boundaries() {
306        assert_eq!(status_class(100), Some(StatusClass::Informational));
307        assert_eq!(status_class(199), Some(StatusClass::Informational));
308        assert_eq!(status_class(200), Some(StatusClass::Success));
309        assert_eq!(status_class(399), Some(StatusClass::Redirection));
310        assert_eq!(status_class(400), Some(StatusClass::ClientError));
311        assert_eq!(status_class(599), Some(StatusClass::ServerError));
312        assert_eq!(status_class(0), None);
313    }
314}