use core::{fmt, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::error::ErrorCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "proptest", derive(proptest_derive::Arbitrary))]
#[non_exhaustive]
pub enum StatusCode {
Continue,
SwitchingProtocols,
Processing,
EarlyHints,
Ok,
Created,
Accepted,
NonAuthoritativeInformation,
NoContent,
ResetContent,
PartialContent,
MultiStatus,
AlreadyReported,
ImUsed,
MultipleChoices,
MovedPermanently,
Found,
SeeOther,
NotModified,
UseProxy,
TemporaryRedirect,
PermanentRedirect,
BadRequest,
Unauthorized,
PaymentRequired,
Forbidden,
NotFound,
MethodNotAllowed,
NotAcceptable,
ProxyAuthenticationRequired,
RequestTimeout,
Conflict,
Gone,
LengthRequired,
PreconditionFailed,
ContentTooLarge,
UriTooLong,
UnsupportedMediaType,
RangeNotSatisfiable,
ExpectationFailed,
ImATeapot,
MisdirectedRequest,
UnprocessableContent,
Locked,
FailedDependency,
TooEarly,
UpgradeRequired,
PreconditionRequired,
TooManyRequests,
RequestHeaderFieldsTooLarge,
UnavailableForLegalReasons,
InternalServerError,
NotImplemented,
BadGateway,
ServiceUnavailable,
GatewayTimeout,
HttpVersionNotSupported,
VariantAlsoNegotiates,
InsufficientStorage,
LoopDetected,
NotExtended,
NetworkAuthenticationRequired,
}
impl StatusCode {
#[must_use]
pub const fn as_u16(&self) -> u16 {
match self {
Self::Continue => 100,
Self::SwitchingProtocols => 101,
Self::Processing => 102,
Self::EarlyHints => 103,
Self::Ok => 200,
Self::Created => 201,
Self::Accepted => 202,
Self::NonAuthoritativeInformation => 203,
Self::NoContent => 204,
Self::ResetContent => 205,
Self::PartialContent => 206,
Self::MultiStatus => 207,
Self::AlreadyReported => 208,
Self::ImUsed => 226,
Self::MultipleChoices => 300,
Self::MovedPermanently => 301,
Self::Found => 302,
Self::SeeOther => 303,
Self::NotModified => 304,
Self::UseProxy => 305,
Self::TemporaryRedirect => 307,
Self::PermanentRedirect => 308,
Self::BadRequest => 400,
Self::Unauthorized => 401,
Self::PaymentRequired => 402,
Self::Forbidden => 403,
Self::NotFound => 404,
Self::MethodNotAllowed => 405,
Self::NotAcceptable => 406,
Self::ProxyAuthenticationRequired => 407,
Self::RequestTimeout => 408,
Self::Conflict => 409,
Self::Gone => 410,
Self::LengthRequired => 411,
Self::PreconditionFailed => 412,
Self::ContentTooLarge => 413,
Self::UriTooLong => 414,
Self::UnsupportedMediaType => 415,
Self::RangeNotSatisfiable => 416,
Self::ExpectationFailed => 417,
Self::ImATeapot => 418,
Self::MisdirectedRequest => 421,
Self::UnprocessableContent => 422,
Self::Locked => 423,
Self::FailedDependency => 424,
Self::TooEarly => 425,
Self::UpgradeRequired => 426,
Self::PreconditionRequired => 428,
Self::TooManyRequests => 429,
Self::RequestHeaderFieldsTooLarge => 431,
Self::UnavailableForLegalReasons => 451,
Self::InternalServerError => 500,
Self::NotImplemented => 501,
Self::BadGateway => 502,
Self::ServiceUnavailable => 503,
Self::GatewayTimeout => 504,
Self::HttpVersionNotSupported => 505,
Self::VariantAlsoNegotiates => 506,
Self::InsufficientStorage => 507,
Self::LoopDetected => 508,
Self::NotExtended => 510,
Self::NetworkAuthenticationRequired => 511,
}
}
#[must_use]
pub const fn reason_phrase(&self) -> &'static str {
match self {
Self::Continue => "Continue",
Self::SwitchingProtocols => "Switching Protocols",
Self::Processing => "Processing",
Self::EarlyHints => "Early Hints",
Self::Ok => "OK",
Self::Created => "Created",
Self::Accepted => "Accepted",
Self::NonAuthoritativeInformation => "Non-Authoritative Information",
Self::NoContent => "No Content",
Self::ResetContent => "Reset Content",
Self::PartialContent => "Partial Content",
Self::MultiStatus => "Multi-Status",
Self::AlreadyReported => "Already Reported",
Self::ImUsed => "IM Used",
Self::MultipleChoices => "Multiple Choices",
Self::MovedPermanently => "Moved Permanently",
Self::Found => "Found",
Self::SeeOther => "See Other",
Self::NotModified => "Not Modified",
Self::UseProxy => "Use Proxy",
Self::TemporaryRedirect => "Temporary Redirect",
Self::PermanentRedirect => "Permanent Redirect",
Self::BadRequest => "Bad Request",
Self::Unauthorized => "Unauthorized",
Self::PaymentRequired => "Payment Required",
Self::Forbidden => "Forbidden",
Self::NotFound => "Not Found",
Self::MethodNotAllowed => "Method Not Allowed",
Self::NotAcceptable => "Not Acceptable",
Self::ProxyAuthenticationRequired => "Proxy Authentication Required",
Self::RequestTimeout => "Request Timeout",
Self::Conflict => "Conflict",
Self::Gone => "Gone",
Self::LengthRequired => "Length Required",
Self::PreconditionFailed => "Precondition Failed",
Self::ContentTooLarge => "Content Too Large",
Self::UriTooLong => "URI Too Long",
Self::UnsupportedMediaType => "Unsupported Media Type",
Self::RangeNotSatisfiable => "Range Not Satisfiable",
Self::ExpectationFailed => "Expectation Failed",
Self::ImATeapot => "I'm a teapot",
Self::MisdirectedRequest => "Misdirected Request",
Self::UnprocessableContent => "Unprocessable Content",
Self::Locked => "Locked",
Self::FailedDependency => "Failed Dependency",
Self::TooEarly => "Too Early",
Self::UpgradeRequired => "Upgrade Required",
Self::PreconditionRequired => "Precondition Required",
Self::TooManyRequests => "Too Many Requests",
Self::RequestHeaderFieldsTooLarge => "Request Header Fields Too Large",
Self::UnavailableForLegalReasons => "Unavailable For Legal Reasons",
Self::InternalServerError => "Internal Server Error",
Self::NotImplemented => "Not Implemented",
Self::BadGateway => "Bad Gateway",
Self::ServiceUnavailable => "Service Unavailable",
Self::GatewayTimeout => "Gateway Timeout",
Self::HttpVersionNotSupported => "HTTP Version Not Supported",
Self::VariantAlsoNegotiates => "Variant Also Negotiates",
Self::InsufficientStorage => "Insufficient Storage",
Self::LoopDetected => "Loop Detected",
Self::NotExtended => "Not Extended",
Self::NetworkAuthenticationRequired => "Network Authentication Required",
}
}
#[must_use]
pub const fn is_informational(&self) -> bool {
self.as_u16() / 100 == 1
}
#[must_use]
pub const fn is_success(&self) -> bool {
self.as_u16() / 100 == 2
}
#[must_use]
pub const fn is_redirection(&self) -> bool {
self.as_u16() / 100 == 3
}
#[must_use]
pub const fn is_client_error(&self) -> bool {
self.as_u16() / 100 == 4
}
#[must_use]
pub const fn is_server_error(&self) -> bool {
self.as_u16() / 100 == 5
}
#[must_use]
pub const fn is_error(&self) -> bool {
self.is_client_error() || self.is_server_error()
}
}
impl fmt::Display for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} {}", self.as_u16(), self.reason_phrase())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnknownStatusCode(pub u16);
impl fmt::Display for UnknownStatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "unknown HTTP status code: {}", self.0)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnknownStatusCode {}
impl TryFrom<u16> for StatusCode {
type Error = UnknownStatusCode;
fn try_from(code: u16) -> Result<Self, Self::Error> {
match code {
100 => Ok(Self::Continue),
101 => Ok(Self::SwitchingProtocols),
102 => Ok(Self::Processing),
103 => Ok(Self::EarlyHints),
200 => Ok(Self::Ok),
201 => Ok(Self::Created),
202 => Ok(Self::Accepted),
203 => Ok(Self::NonAuthoritativeInformation),
204 => Ok(Self::NoContent),
205 => Ok(Self::ResetContent),
206 => Ok(Self::PartialContent),
207 => Ok(Self::MultiStatus),
208 => Ok(Self::AlreadyReported),
226 => Ok(Self::ImUsed),
300 => Ok(Self::MultipleChoices),
301 => Ok(Self::MovedPermanently),
302 => Ok(Self::Found),
303 => Ok(Self::SeeOther),
304 => Ok(Self::NotModified),
305 => Ok(Self::UseProxy),
307 => Ok(Self::TemporaryRedirect),
308 => Ok(Self::PermanentRedirect),
400 => Ok(Self::BadRequest),
401 => Ok(Self::Unauthorized),
402 => Ok(Self::PaymentRequired),
403 => Ok(Self::Forbidden),
404 => Ok(Self::NotFound),
405 => Ok(Self::MethodNotAllowed),
406 => Ok(Self::NotAcceptable),
407 => Ok(Self::ProxyAuthenticationRequired),
408 => Ok(Self::RequestTimeout),
409 => Ok(Self::Conflict),
410 => Ok(Self::Gone),
411 => Ok(Self::LengthRequired),
412 => Ok(Self::PreconditionFailed),
413 => Ok(Self::ContentTooLarge),
414 => Ok(Self::UriTooLong),
415 => Ok(Self::UnsupportedMediaType),
416 => Ok(Self::RangeNotSatisfiable),
417 => Ok(Self::ExpectationFailed),
418 => Ok(Self::ImATeapot),
421 => Ok(Self::MisdirectedRequest),
422 => Ok(Self::UnprocessableContent),
423 => Ok(Self::Locked),
424 => Ok(Self::FailedDependency),
425 => Ok(Self::TooEarly),
426 => Ok(Self::UpgradeRequired),
428 => Ok(Self::PreconditionRequired),
429 => Ok(Self::TooManyRequests),
431 => Ok(Self::RequestHeaderFieldsTooLarge),
451 => Ok(Self::UnavailableForLegalReasons),
500 => Ok(Self::InternalServerError),
501 => Ok(Self::NotImplemented),
502 => Ok(Self::BadGateway),
503 => Ok(Self::ServiceUnavailable),
504 => Ok(Self::GatewayTimeout),
505 => Ok(Self::HttpVersionNotSupported),
506 => Ok(Self::VariantAlsoNegotiates),
507 => Ok(Self::InsufficientStorage),
508 => Ok(Self::LoopDetected),
510 => Ok(Self::NotExtended),
511 => Ok(Self::NetworkAuthenticationRequired),
other => Err(UnknownStatusCode(other)),
}
}
}
impl FromStr for StatusCode {
type Err = UnknownStatusCode;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let n: u16 = s.trim().parse().map_err(|_| UnknownStatusCode(0))?;
Self::try_from(n)
}
}
impl From<ErrorCode> for StatusCode {
fn from(code: ErrorCode) -> Self {
match code {
ErrorCode::BadRequest | ErrorCode::ValidationFailed => Self::BadRequest,
ErrorCode::Unauthorized
| ErrorCode::InvalidCredentials
| ErrorCode::TokenExpired
| ErrorCode::TokenInvalid => Self::Unauthorized,
ErrorCode::Forbidden
| ErrorCode::InsufficientPermissions
| ErrorCode::OrgOutsideSubtree
| ErrorCode::AncestorRequired
| ErrorCode::CrossSubtreeAccess => Self::Forbidden,
ErrorCode::ResourceNotFound => Self::NotFound,
ErrorCode::MethodNotAllowed => Self::MethodNotAllowed,
ErrorCode::NotAcceptable => Self::NotAcceptable,
ErrorCode::RequestTimeout => Self::RequestTimeout,
ErrorCode::Conflict | ErrorCode::ResourceAlreadyExists => Self::Conflict,
ErrorCode::Gone => Self::Gone,
ErrorCode::PreconditionFailed => Self::PreconditionFailed,
ErrorCode::PayloadTooLarge => Self::ContentTooLarge,
ErrorCode::UnsupportedMediaType => Self::UnsupportedMediaType,
ErrorCode::UnprocessableEntity => Self::UnprocessableContent,
ErrorCode::PreconditionRequired => Self::PreconditionRequired,
ErrorCode::RateLimited => Self::TooManyRequests,
ErrorCode::RequestHeaderFieldsTooLarge => Self::RequestHeaderFieldsTooLarge,
ErrorCode::InternalServerError => Self::InternalServerError,
ErrorCode::NotImplemented => Self::NotImplemented,
ErrorCode::BadGateway => Self::BadGateway,
ErrorCode::ServiceUnavailable => Self::ServiceUnavailable,
ErrorCode::GatewayTimeout => Self::GatewayTimeout,
}
}
}
#[cfg(feature = "http")]
mod http_interop {
use super::{StatusCode, UnknownStatusCode};
impl TryFrom<http::StatusCode> for StatusCode {
type Error = UnknownStatusCode;
fn try_from(sc: http::StatusCode) -> Result<Self, Self::Error> {
Self::try_from(sc.as_u16())
}
}
impl TryFrom<StatusCode> for http::StatusCode {
type Error = http::status::InvalidStatusCode;
fn try_from(sc: StatusCode) -> Result<Self, Self::Error> {
Self::from_u16(sc.as_u16())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numeric_round_trip() {
let codes: &[u16] = &[
100, 101, 102, 103, 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302,
303, 304, 305, 307, 308, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411,
412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424, 425, 426, 428, 429, 431, 451,
500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
];
for &code in codes {
let sc = StatusCode::try_from(code).expect("should be known");
assert_eq!(sc.as_u16(), code, "round-trip failed for {code}");
}
}
#[test]
fn unknown_status_errors() {
assert!(StatusCode::try_from(0u16).is_err());
assert!(StatusCode::try_from(600u16).is_err());
let err = StatusCode::try_from(999u16).unwrap_err();
assert_eq!(err.0, 999);
assert!(err.to_string().contains("999"));
}
#[test]
fn from_str_round_trip() {
assert_eq!("200".parse::<StatusCode>().unwrap(), StatusCode::Ok);
assert_eq!(" 404 ".parse::<StatusCode>().unwrap(), StatusCode::NotFound);
assert!("abc".parse::<StatusCode>().is_err());
assert!("999".parse::<StatusCode>().is_err());
}
#[test]
fn reason_phrases_spot_check() {
assert_eq!(StatusCode::Continue.reason_phrase(), "Continue");
assert_eq!(StatusCode::ImUsed.reason_phrase(), "IM Used");
assert_eq!(StatusCode::ImATeapot.reason_phrase(), "I'm a teapot");
assert_eq!(
StatusCode::NetworkAuthenticationRequired.reason_phrase(),
"Network Authentication Required"
);
}
#[test]
fn category_predicates_all_ranges() {
assert!(StatusCode::Processing.is_informational());
assert!(StatusCode::EarlyHints.is_informational());
assert!(StatusCode::Accepted.is_success());
assert!(StatusCode::ImUsed.is_success());
assert!(StatusCode::PermanentRedirect.is_redirection());
assert!(StatusCode::Gone.is_client_error());
assert!(StatusCode::UnavailableForLegalReasons.is_client_error());
assert!(StatusCode::GatewayTimeout.is_server_error());
assert!(StatusCode::NetworkAuthenticationRequired.is_server_error());
}
#[test]
fn category_predicates() {
assert!(StatusCode::Continue.is_informational());
assert!(!StatusCode::Continue.is_success());
assert!(StatusCode::Ok.is_success());
assert!(StatusCode::Created.is_success());
assert!(!StatusCode::Ok.is_error());
assert!(StatusCode::MovedPermanently.is_redirection());
assert!(!StatusCode::Ok.is_redirection());
assert!(StatusCode::NotFound.is_client_error());
assert!(StatusCode::NotFound.is_error());
assert!(!StatusCode::NotFound.is_server_error());
assert!(StatusCode::InternalServerError.is_server_error());
assert!(StatusCode::InternalServerError.is_error());
}
#[test]
fn display_format() {
assert_eq!(StatusCode::Ok.to_string(), "200 OK");
assert_eq!(StatusCode::NotFound.to_string(), "404 Not Found");
}
#[test]
fn from_error_code() {
assert_eq!(
StatusCode::from(ErrorCode::ResourceNotFound),
StatusCode::NotFound
);
assert_eq!(
StatusCode::from(ErrorCode::RateLimited),
StatusCode::TooManyRequests
);
assert_eq!(
StatusCode::from(ErrorCode::InternalServerError),
StatusCode::InternalServerError
);
}
#[cfg(feature = "serde")]
#[test]
fn serde_round_trip() {
let sc = StatusCode::Created;
let json = serde_json::to_string(&sc).unwrap();
let back: StatusCode = serde_json::from_str(&json).unwrap();
assert_eq!(back, sc);
}
#[cfg(feature = "http")]
#[test]
fn http_crate_round_trip() {
let sc = StatusCode::Ok;
let hsc: http::StatusCode = sc.try_into().unwrap();
assert_eq!(hsc, http::StatusCode::OK);
let back: StatusCode = hsc.try_into().unwrap();
assert_eq!(back, StatusCode::Ok);
}
}