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 #[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#[non_exhaustive]
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
150pub enum StatusClass {
151 Informational,
153 Success,
155 Redirection,
157 ClientError,
159 ServerError,
161}
162
163#[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#[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#[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::scan::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 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}