axol_http/
status.rs

1use std::{convert::Infallible, fmt};
2
3use strum::EnumProperty;
4use thiserror::Error;
5
6/// An error from creating a status code from a u16.
7#[derive(Error, Debug)]
8pub enum StatusCodeError {
9    #[error("infallible")]
10    Infallible(#[from] Infallible),
11    #[error("status code '{0}' not between 100 and 999 inclusive")]
12    OutOfRange(u16),
13}
14
15/// An HTTP status code (`status-code` in RFC 7230 et al.).
16///
17/// Constants are provided for known status codes, including those in the IANA
18/// [HTTP Status Code Registry](
19/// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml).
20///
21/// Status code values in the range 100-999 (inclusive) are supported by this
22/// type. Values in the range 100-599 are semantically classified by the most
23/// significant digit. See [`StatusCode::is_success`], etc. Values above 599
24/// are unclassified but allowed for legacy compatibility, though their use is
25/// discouraged. Applications may interpret such values as protocol errors.
26///
27/// # Examples
28///
29/// ```
30/// use axol_http::StatusCode;
31///
32/// assert_eq!(StatusCode::from_u16(200).unwrap(), StatusCode::Ok);
33/// assert_eq!(StatusCode::NotFound.as_u16(), 404);
34/// assert!(StatusCode::Ok.is_success());
35/// ```
36#[repr(u16)]
37#[derive(
38    Debug,
39    Clone,
40    Copy,
41    Default,
42    PartialEq,
43    Eq,
44    PartialOrd,
45    Ord,
46    Hash,
47    strum::EnumProperty,
48    strum::FromRepr,
49)]
50pub enum StatusCode {
51    /// 100 Continue
52    /// [[RFC7231, Section 6.2.1](https://tools.ietf.org/html/rfc7231#section-6.2.1)]
53    #[strum(props(canonical_reason = "Continue"))]
54    Continue = 100,
55    /// 101 Switching Protocols
56    /// [[RFC7231, Section 6.2.2](https://tools.ietf.org/html/rfc7231#section-6.2.2)]
57    #[strum(props(canonical_reason = "Switching Protocols"))]
58    SwitchingProtocols = 101,
59    /// 102 Processing
60    /// [[RFC2518](https://tools.ietf.org/html/rfc2518)]
61    #[strum(props(canonical_reason = "Processing"))]
62    Processing = 102,
63    /// 200 OK
64    /// [[RFC7231, Section 6.3.1](https://tools.ietf.org/html/rfc7231#section-6.3.1)]
65    #[strum(props(canonical_reason = "OK"))]
66    #[default]
67    Ok = 200,
68    /// 201 Created
69    /// [[RFC7231, Section 6.3.2](https://tools.ietf.org/html/rfc7231#section-6.3.2)]
70    #[strum(props(canonical_reason = "Created"))]
71    Created = 201,
72    /// 202 Accepted
73    /// [[RFC7231, Section 6.3.3](https://tools.ietf.org/html/rfc7231#section-6.3.3)]
74    #[strum(props(canonical_reason = "Accepted"))]
75    Accepted = 202,
76    /// 203 Non-Authoritative Information
77    /// [[RFC7231, Section 6.3.4](https://tools.ietf.org/html/rfc7231#section-6.3.4)]
78    #[strum(props(canonical_reason = "Non Authoritative Information"))]
79    NonAuthoritativeInformation = 203,
80    /// 204 No Content
81    /// [[RFC7231, Section 6.3.5](https://tools.ietf.org/html/rfc7231#section-6.3.5)]
82    #[strum(props(canonical_reason = "No Content"))]
83    NoContent = 204,
84    /// 205 Reset Content
85    /// [[RFC7231, Section 6.3.6](https://tools.ietf.org/html/rfc7231#section-6.3.6)]
86    #[strum(props(canonical_reason = "Reset Content"))]
87    ResetContent = 205,
88    /// 206 Partial Content
89    /// [[RFC7233, Section 4.1](https://tools.ietf.org/html/rfc7233#section-4.1)]
90    #[strum(props(canonical_reason = "Partial Content"))]
91    PartialContent = 206,
92    /// 207 Multi-Status
93    /// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
94    #[strum(props(canonical_reason = "Multi-Status"))]
95    MultiStatus = 207,
96    /// 208 Already Reported
97    /// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
98    #[strum(props(canonical_reason = "Already Reported"))]
99    AlreadyReported = 208,
100
101    /// 226 IM Used
102    /// [[RFC3229](https://tools.ietf.org/html/rfc3229)]
103    #[strum(props(canonical_reason = "IM Used"))]
104    ImUsed = 226,
105
106    /// 300 Multiple Choices
107    /// [[RFC7231, Section 6.4.1](https://tools.ietf.org/html/rfc7231#section-6.4.1)]
108    #[strum(props(canonical_reason = "Multiple Choices"))]
109    MultipleChoices = 300,
110    /// 301 Moved Permanently
111    /// [[RFC7231, Section 6.4.2](https://tools.ietf.org/html/rfc7231#section-6.4.2)]
112    #[strum(props(canonical_reason = "Moved Permanently"))]
113    MovedPermanently = 301,
114    /// 302 Found
115    /// [[RFC7231, Section 6.4.3](https://tools.ietf.org/html/rfc7231#section-6.4.3)]
116    #[strum(props(canonical_reason = "Found"))]
117    Found = 302,
118    /// 303 See Other
119    /// [[RFC7231, Section 6.4.4](https://tools.ietf.org/html/rfc7231#section-6.4.4)]
120    #[strum(props(canonical_reason = "See Other"))]
121    SeeOther = 303,
122    /// 304 Not Modified
123    /// [[RFC7232, Section 4.1](https://tools.ietf.org/html/rfc7232#section-4.1)]
124    #[strum(props(canonical_reason = "Not Modified"))]
125    NotModified = 304,
126    /// 305 Use Proxy
127    /// [[RFC7231, Section 6.4.5](https://tools.ietf.org/html/rfc7231#section-6.4.5)]
128    #[strum(props(canonical_reason = "Use Proxy"))]
129    UseProxy = 305,
130    /// 307 Temporary Redirect
131    /// [[RFC7231, Section 6.4.7](https://tools.ietf.org/html/rfc7231#section-6.4.7)]
132    #[strum(props(canonical_reason = "Temporary Redirect"))]
133    TemporaryRedirect = 307,
134    /// 308 Permanent Redirect
135    /// [[RFC7238](https://tools.ietf.org/html/rfc7238)]
136    #[strum(props(canonical_reason = "Permanent Redirect"))]
137    PermanentRedirect = 308,
138
139    /// 400 Bad Request
140    /// [[RFC7231, Section 6.5.1](https://tools.ietf.org/html/rfc7231#section-6.5.1)]
141    #[strum(props(canonical_reason = "Bad Request"))]
142    BadRequest = 400,
143    /// 401 Unauthorized
144    /// [[RFC7235, Section 3.1](https://tools.ietf.org/html/rfc7235#section-3.1)]
145    #[strum(props(canonical_reason = "Unauthorized"))]
146    Unauthorized = 401,
147    /// 402 Payment Required
148    /// [[RFC7231, Section 6.5.2](https://tools.ietf.org/html/rfc7231#section-6.5.2)]
149    #[strum(props(canonical_reason = "Payment Required"))]
150    PaymentRequired = 402,
151    /// 403 Forbidden
152    /// [[RFC7231, Section 6.5.3](https://tools.ietf.org/html/rfc7231#section-6.5.3)]
153    #[strum(props(canonical_reason = "Forbidden"))]
154    Forbidden = 403,
155    /// 404 Not Found
156    /// [[RFC7231, Section 6.5.4](https://tools.ietf.org/html/rfc7231#section-6.5.4)]
157    #[strum(props(canonical_reason = "Not Found"))]
158    NotFound = 404,
159    /// 405 Method Not Allowed
160    /// [[RFC7231, Section 6.5.5](https://tools.ietf.org/html/rfc7231#section-6.5.5)]
161    #[strum(props(canonical_reason = "Method Not Allowed"))]
162    MethodNotAllowed = 405,
163    /// 406 Not Acceptable
164    /// [[RFC7231, Section 6.5.6](https://tools.ietf.org/html/rfc7231#section-6.5.6)]
165    #[strum(props(canonical_reason = "Not Acceptable"))]
166    NotAcceptable = 406,
167    /// 407 Proxy Authentication Required
168    /// [[RFC7235, Section 3.2](https://tools.ietf.org/html/rfc7235#section-3.2)]
169    #[strum(props(canonical_reason = "Proxy Authentication Required"))]
170    ProxyAuthenticationRequired = 407,
171    /// 408 Request Timeout
172    /// [[RFC7231, Section 6.5.7](https://tools.ietf.org/html/rfc7231#section-6.5.7)]
173    #[strum(props(canonical_reason = "Request Timeout"))]
174    RequestTimeout = 408,
175    /// 409 Conflict
176    /// [[RFC7231, Section 6.5.8](https://tools.ietf.org/html/rfc7231#section-6.5.8)]
177    #[strum(props(canonical_reason = "Conflict"))]
178    Conflict = 409,
179    /// 410 Gone
180    /// [[RFC7231, Section 6.5.9](https://tools.ietf.org/html/rfc7231#section-6.5.9)]
181    #[strum(props(canonical_reason = "Gone"))]
182    Gone = 410,
183    /// 411 Length Required
184    /// [[RFC7231, Section 6.5.10](https://tools.ietf.org/html/rfc7231#section-6.5.10)]
185    #[strum(props(canonical_reason = "Length Required"))]
186    LengthRequired = 411,
187    /// 412 Precondition Failed
188    /// [[RFC7232, Section 4.2](https://tools.ietf.org/html/rfc7232#section-4.2)]
189    #[strum(props(canonical_reason = "Precondition Failed"))]
190    PreconditionFailed = 412,
191    /// 413 Payload Too Large
192    /// [[RFC7231, Section 6.5.11](https://tools.ietf.org/html/rfc7231#section-6.5.11)]
193    #[strum(props(canonical_reason = "Payload Too Large"))]
194    PayloadTooLarge = 413,
195    /// 414 URI Too Long
196    /// [[RFC7231, Section 6.5.12](https://tools.ietf.org/html/rfc7231#section-6.5.12)]
197    #[strum(props(canonical_reason = "URI Too Long"))]
198    UriTooLong = 414,
199    /// 415 Unsupported Media Type
200    /// [[RFC7231, Section 6.5.13](https://tools.ietf.org/html/rfc7231#section-6.5.13)]
201    #[strum(props(canonical_reason = "Unsupported Media Type"))]
202    UnsupportedMediaType = 415,
203    /// 416 Range Not Satisfiable
204    /// [[RFC7233, Section 4.4](https://tools.ietf.org/html/rfc7233#section-4.4)]
205    #[strum(props(canonical_reason = "Range Not Satisfiable"))]
206    RangeNotSatisfiable = 416,
207    /// 417 Expectation Failed
208    /// [[RFC7231, Section 6.5.14](https://tools.ietf.org/html/rfc7231#section-6.5.14)]
209    #[strum(props(canonical_reason = "Expectation Failed"))]
210    ExpectationFailed = 417,
211    /// 418 I'm a teapot
212    /// [curiously not registered by IANA but [RFC2324](https://tools.ietf.org/html/rfc2324)]
213    #[strum(props(canonical_reason = "I'm a teapot"))]
214    ImATeapot = 418,
215
216    /// 421 Misdirected Request
217    /// [RFC7540, Section 9.1.2](http://tools.ietf.org/html/rfc7540#section-9.1.2)
218    #[strum(props(canonical_reason = "Misdirected Request"))]
219    MisdirectedRequest = 421,
220    /// 422 Unprocessable Entity
221    /// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
222    #[strum(props(canonical_reason = "Unprocessable Entity"))]
223    UnprocessableEntity = 422,
224    /// 423 Locked
225    /// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
226    #[strum(props(canonical_reason = "Locked"))]
227    Locked = 423,
228    /// 424 Failed Dependency
229    /// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
230    #[strum(props(canonical_reason = "Failed Dependency"))]
231    FailedDependency = 424,
232
233    /// 426 Upgrade Required
234    /// [[RFC7231, Section 6.5.15](https://tools.ietf.org/html/rfc7231#section-6.5.15)]
235    #[strum(props(canonical_reason = "Upgrade Required"))]
236    UpgradeRequired = 426,
237
238    /// 428 Precondition Required
239    /// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
240    #[strum(props(canonical_reason = "Precondition Required"))]
241    PreconditionRequired = 428,
242    /// 429 Too Many Requests
243    /// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
244    #[strum(props(canonical_reason = "Too Many Requests"))]
245    TooManyRequests = 429,
246
247    /// 431 Request Header Fields Too Large
248    /// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
249    #[strum(props(canonical_reason = "Request Header Fields Too Large"))]
250    RequestHeaderFieldsTooLarge = 431,
251
252    /// 451 Unavailable For Legal Reasons
253    /// [[RFC7725](http://tools.ietf.org/html/rfc7725)]
254    #[strum(props(canonical_reason = "Unavailable For Legal Reasons"))]
255    UnavailableForLegalReasons = 451,
256
257    /// 500 Internal Server Error
258    /// [[RFC7231, Section 6.6.1](https://tools.ietf.org/html/rfc7231#section-6.6.1)]
259    #[strum(props(canonical_reason = "Internal Server Error"))]
260    InternalServerError = 500,
261    /// 501 Not Implemented
262    /// [[RFC7231, Section 6.6.2](https://tools.ietf.org/html/rfc7231#section-6.6.2)]
263    #[strum(props(canonical_reason = "Not Implemented"))]
264    NotImplemented = 501,
265    /// 502 Bad Gateway
266    /// [[RFC7231, Section 6.6.3](https://tools.ietf.org/html/rfc7231#section-6.6.3)]
267    #[strum(props(canonical_reason = "Bad Gateway"))]
268    BadGateway = 502,
269    /// 503 Service Unavailable
270    /// [[RFC7231, Section 6.6.4](https://tools.ietf.org/html/rfc7231#section-6.6.4)]
271    #[strum(props(canonical_reason = "Service Unavailable"))]
272    ServiceUnavailable = 503,
273    /// 504 Gateway Timeout
274    /// [[RFC7231, Section 6.6.5](https://tools.ietf.org/html/rfc7231#section-6.6.5)]
275    #[strum(props(canonical_reason = "Gateway Timeout"))]
276    GatewayTimeout = 504,
277    /// 505 HTTP Version Not Supported
278    /// [[RFC7231, Section 6.6.6](https://tools.ietf.org/html/rfc7231#section-6.6.6)]
279    #[strum(props(canonical_reason = "HTTP Version Not Supported"))]
280    HttpVersionNotSupported = 505,
281    /// 506 Variant Also Negotiates
282    /// [[RFC2295](https://tools.ietf.org/html/rfc2295)]
283    #[strum(props(canonical_reason = "Variant Also Negotiates"))]
284    VariantAlsoNegotiates = 506,
285    /// 507 Insufficient Storage
286    /// [[RFC4918](https://tools.ietf.org/html/rfc4918)]
287    #[strum(props(canonical_reason = "Insufficient Storage"))]
288    InsufficientStorage = 507,
289    /// 508 Loop Detected
290    /// [[RFC5842](https://tools.ietf.org/html/rfc5842)]
291    #[strum(props(canonical_reason = "Loop Detected"))]
292    LoopDetected = 508,
293
294    /// 510 Not Extended
295    /// [[RFC2774](https://tools.ietf.org/html/rfc2774)]
296    #[strum(props(canonical_reason = "Not Extended"))]
297    NotExtended = 510,
298    /// 511 Network Authentication Required
299    /// [[RFC6585](https://tools.ietf.org/html/rfc6585)]
300    #[strum(props(canonical_reason = "Network Authentication Required"))]
301    NetworkAuthenticationRequired = 511,
302
303    /// An unknown (invalid) status code. It is a logic error that will do unexpected things if this is initialized to a valid status code.
304    Other(u16) = u16::MAX,
305}
306
307impl Into<http::StatusCode> for StatusCode {
308    fn into(self) -> http::StatusCode {
309        http::StatusCode::from_u16(self.as_u16())
310            .expect("out of range status code where not expected")
311    }
312}
313
314impl From<http::StatusCode> for StatusCode {
315    fn from(value: http::StatusCode) -> Self {
316        Self::from_u16(value.as_u16()).expect("out of range status code where not expected")
317    }
318}
319
320impl TryFrom<u16> for StatusCode {
321    type Error = StatusCodeError;
322
323    fn try_from(value: u16) -> Result<Self, Self::Error> {
324        Self::from_u16(value)
325    }
326}
327
328impl Into<u16> for StatusCode {
329    fn into(self) -> u16 {
330        self.as_u16()
331    }
332}
333
334impl fmt::Display for StatusCode {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        write!(f, "{} {}", self.as_str(), self.canonical_reason())
337    }
338}
339
340impl StatusCode {
341    /// Converts a u16 to a status code.
342    ///
343    /// The function validates the correctness of the supplied u16. It must be
344    /// greater or equal to 100 and less than 1000.
345    ///
346    /// # Example
347    ///
348    /// ```
349    /// use axol_http::StatusCode;
350    ///
351    /// let ok = StatusCode::from_u16(200).unwrap();
352    /// assert!(matches!(ok, StatusCode::Ok);
353    ///
354    /// let err = StatusCode::from_u16(99);
355    /// assert!(err.is_err());
356    /// ```
357    pub fn from_u16(value: u16) -> Result<Self, StatusCodeError> {
358        if value >= 1000 || value < 100 {
359            return Err(StatusCodeError::OutOfRange(value));
360        }
361        Ok(match Self::from_repr(value) {
362            Some(StatusCode::Other(_)) => StatusCode::Other(u16::MAX),
363            Some(x) => x,
364            None => StatusCode::Other(value),
365        })
366    }
367
368    /// Returns the `u16` corresponding to this `StatusCode`.
369    ///
370    /// # Note
371    ///
372    /// This is the same as the `From<StatusCode>` implementation, but
373    /// included as an inherent method because that implementation doesn't
374    /// appear in rustdocs, as well as a way to force the type instead of
375    /// relying on inference.
376    ///
377    /// # Example
378    ///
379    /// ```
380    /// let status = axol_http::StatusCode::Ok;
381    /// assert_eq!(status.as_u16(), 200);
382    /// ```
383    pub fn as_u16(&self) -> u16 {
384        match self {
385            Self::Other(x) => *x,
386            x => x.discriminant(),
387        }
388    }
389
390    /// Returns a &str representation of the `StatusCode`
391    ///
392    /// The return value only includes a numerical representation of the
393    /// status code. The canonical reason is not included.
394    ///
395    /// # Example
396    ///
397    /// ```
398    /// let status = axol_http::StatusCode::Ok;
399    /// assert_eq!(status.as_str(), "200");
400    /// ```
401    pub fn as_str(&self) -> &'static str {
402        let inner: http::StatusCode = (*self).into();
403        // SAFETY: http::StatusCode::as_str is returning a reference to a const str.
404        // to maintain safety, we must pin http crate version
405        unsafe { std::mem::transmute(inner.as_str()) }
406    }
407
408    /// Get the standardised `reason-phrase` for this status code.
409    ///
410    /// This is mostly here for servers writing responses, but could potentially have application
411    /// at other times.
412    ///
413    /// The reason phrase is defined as being exclusively for human readers. You should avoid
414    /// deriving any meaning from it at all costs.
415    ///
416    /// Bear in mind also that in HTTP/2.0 and HTTP/3.0 the reason phrase is abolished from
417    /// transmission, and so this canonical reason phrase really is the only reason phrase you’ll
418    /// find.
419    ///
420    /// This is empty for `Other` or invalid status codes.
421    ///
422    /// # Example
423    ///
424    /// ```
425    /// let status = axol_http::StatusCode::OK;
426    /// assert_eq!(status.canonical_reason(), "OK");
427    /// ```
428    pub fn canonical_reason(&self) -> &'static str {
429        self.get_str("canonical_reason").unwrap_or_default()
430    }
431
432    /// Check if status is within 100-199.
433    pub fn is_informational(&self) -> bool {
434        200 > self.discriminant() && self.discriminant() >= 100
435    }
436
437    /// Check if status is within 200-299.
438    pub fn is_success(&self) -> bool {
439        300 > self.discriminant() && self.discriminant() >= 200
440    }
441
442    /// Check if status is within 300-399.
443    pub fn is_redirection(&self) -> bool {
444        400 > self.discriminant() && self.discriminant() >= 300
445    }
446
447    /// Check if status is within 400-499.
448    pub fn is_client_error(&self) -> bool {
449        500 > self.discriminant() && self.discriminant() >= 400
450    }
451
452    /// Check if status is within 500-599.
453    pub fn is_server_error(&self) -> bool {
454        600 > self.discriminant() && self.discriminant() >= 500
455    }
456
457    // https://doc.rust-lang.org/reference/items/enumerations.html#pointer-casting
458    fn discriminant(&self) -> u16 {
459        unsafe { *(self as *const Self as *const u16) }
460    }
461}