1macro_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 #[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#[non_exhaustive]
136#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
137pub enum StatusClass {
138 Informational,
140 Success,
142 Redirection,
144 ClientError,
146 ServerError,
148}
149
150#[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#[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 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}