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/// Whether a response with `status` to a request with `request` method can
175/// carry a body (RFC 9112 section 6.3, rules 1 and 2): false for 1xx, 204,
176/// 304, any response to `HEAD`, and a 2xx to `CONNECT` (which turns the
177/// connection into a tunnel). For proxies and clients deciding how to read
178/// or forward a response.
179///
180/// # Examples
181///
182/// ```
183/// use http_net::{Method, status_allows_body};
184///
185/// assert!(status_allows_body(200, Method::Get));
186/// assert!(!status_allows_body(200, Method::Head));
187/// assert!(!status_allows_body(304, Method::Get));
188/// assert!(!status_allows_body(200, Method::Connect));
189/// assert!(status_allows_body(407, Method::Connect));
190/// ```
191#[must_use]
192pub const fn status_allows_body(status: u16, request: crate::Method) -> bool {
193    !(matches!(request, crate::Method::Head)
194        || (status >= 100 && status < 200)
195        || status == 204
196        || status == 304
197        || (matches!(request, crate::Method::Connect) && status >= 200 && status < 300))
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_status_allows_body_follows_rfc_9112_6_3() {
206        use crate::Method;
207        for status in 100..200 {
208            assert!(!status_allows_body(status, Method::Get));
209        }
210        for m in [Method::Get, Method::Post, Method::Other] {
211            assert!(status_allows_body(200, m));
212            assert!(!status_allows_body(204, m));
213            assert!(!status_allows_body(304, m));
214            assert!(status_allows_body(500, m));
215        }
216        assert!(!status_allows_body(500, Method::Head));
217        assert!(!status_allows_body(299, Method::Connect));
218        assert!(status_allows_body(300, Method::Connect));
219    }
220
221    #[test]
222    fn test_reason_phrase_registry_has_every_iana_code() {
223        // The 0.3.0 additions, each checked by value.
224        let added = [
225            (102, "Processing"),
226            (103, "Early Hints"),
227            (207, "Multi-Status"),
228            (208, "Already Reported"),
229            (226, "IM Used"),
230            (305, "Use Proxy"),
231            (402, "Payment Required"),
232            (407, "Proxy Authentication Required"),
233            (423, "Locked"),
234            (424, "Failed Dependency"),
235            (425, "Too Early"),
236            (451, "Unavailable For Legal Reasons"),
237            (506, "Variant Also Negotiates"),
238            (507, "Insufficient Storage"),
239            (508, "Loop Detected"),
240            (510, "Not Extended"),
241            (511, "Network Authentication Required"),
242        ];
243        for (code, phrase) in added {
244            assert_eq!(reason_phrase(code), phrase, "{code}");
245        }
246        assert_eq!(REGISTERED.len(), 61);
247        for &(code, phrase) in REGISTERED {
248            assert_eq!(reason_phrase(code), phrase);
249            assert!(status_class(code).is_some());
250        }
251    }
252
253    #[test]
254    fn test_reason_phrase_unassigned_and_reserved_are_empty() {
255        for code in [
256            0, 99, 104, 199, 209, 299, 306, 399, 418, 420, 499, 509, 599, 600, 999,
257        ] {
258            assert_eq!(reason_phrase(code), "", "{code}");
259        }
260    }
261
262    #[test]
263    fn test_status_class_boundaries() {
264        assert_eq!(status_class(100), Some(StatusClass::Informational));
265        assert_eq!(status_class(199), Some(StatusClass::Informational));
266        assert_eq!(status_class(200), Some(StatusClass::Success));
267        assert_eq!(status_class(399), Some(StatusClass::Redirection));
268        assert_eq!(status_class(400), Some(StatusClass::ClientError));
269        assert_eq!(status_class(599), Some(StatusClass::ServerError));
270        assert_eq!(status_class(0), None);
271    }
272}