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#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn test_reason_phrase_registry_has_every_iana_code() {
180 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}