use std::{collections::BTreeMap, fmt, str::FromStr, sync::Arc};
use as_variant::as_variant;
use bytes::{BufMut, Bytes};
use ruma_common::{
api::{
error::{
FromHttpResponseError, HeaderDeserializationError, HeaderSerializationError,
IntoHttpError, MatrixErrorBody,
},
EndpointError, OutgoingResponse,
},
serde::StringEnum,
RoomVersionId,
};
use serde::{Deserialize, Serialize};
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
use web_time::{Duration, SystemTime};
use crate::{
http_headers::{http_date_to_system_time, system_time_to_http_date},
PrivOwnedStr,
};
mod kind_serde;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ErrorKind {
#[non_exhaustive]
Forbidden {
#[cfg(feature = "unstable-msc2967")]
authenticate: Option<AuthenticateError>,
},
UnknownToken {
soft_logout: bool,
},
MissingToken,
BadJson,
NotJson,
NotFound,
LimitExceeded {
retry_after: Option<RetryAfter>,
},
Unknown,
Unrecognized,
Unauthorized,
UserDeactivated,
UserInUse,
InvalidUsername,
RoomInUse,
InvalidRoomState,
ThreepidInUse,
ThreepidNotFound,
ThreepidAuthFailed,
ThreepidDenied,
ThreepidMediumNotSupported,
ServerNotTrusted,
UnsupportedRoomVersion,
IncompatibleRoomVersion {
room_version: RoomVersionId,
},
BadState,
GuestAccessForbidden,
CaptchaNeeded,
CaptchaInvalid,
MissingParam,
InvalidParam,
TooLarge,
Exclusive,
ResourceLimitExceeded {
admin_contact: String,
},
CannotLeaveServerNoticeRoom,
WeakPassword,
UnableToAuthorizeJoin,
UnableToGrantJoin,
BadAlias,
DuplicateAnnotation,
NotYetUploaded,
CannotOverwriteMedia,
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
UnknownPos,
UrlNotSet,
BadStatus {
status: Option<http::StatusCode>,
body: Option<String>,
},
ConnectionFailed,
ConnectionTimeout,
WrongRoomKeysVersion {
current_version: Option<String>,
},
#[cfg(feature = "unstable-msc3843")]
Unactionable,
UserLocked,
UserSuspended,
#[doc(hidden)]
_Custom { errcode: PrivOwnedStr, extra: Extra },
}
impl ErrorKind {
pub fn forbidden() -> Self {
Self::Forbidden {
#[cfg(feature = "unstable-msc2967")]
authenticate: None,
}
}
#[cfg(feature = "unstable-msc2967")]
pub fn forbidden_with_authenticate(authenticate: AuthenticateError) -> Self {
Self::Forbidden { authenticate: Some(authenticate) }
}
pub fn errcode(&self) -> ErrorCode {
match self {
ErrorKind::Forbidden { .. } => ErrorCode::Forbidden,
ErrorKind::UnknownToken { .. } => ErrorCode::UnknownToken,
ErrorKind::MissingToken => ErrorCode::MissingToken,
ErrorKind::BadJson => ErrorCode::BadJson,
ErrorKind::NotJson => ErrorCode::NotJson,
ErrorKind::NotFound => ErrorCode::NotFound,
ErrorKind::LimitExceeded { .. } => ErrorCode::LimitExceeded,
ErrorKind::Unknown => ErrorCode::Unknown,
ErrorKind::Unrecognized => ErrorCode::Unrecognized,
ErrorKind::Unauthorized => ErrorCode::Unauthorized,
ErrorKind::UserDeactivated => ErrorCode::UserDeactivated,
ErrorKind::UserInUse => ErrorCode::UserInUse,
ErrorKind::InvalidUsername => ErrorCode::InvalidUsername,
ErrorKind::RoomInUse => ErrorCode::RoomInUse,
ErrorKind::InvalidRoomState => ErrorCode::InvalidRoomState,
ErrorKind::ThreepidInUse => ErrorCode::ThreepidInUse,
ErrorKind::ThreepidNotFound => ErrorCode::ThreepidNotFound,
ErrorKind::ThreepidAuthFailed => ErrorCode::ThreepidAuthFailed,
ErrorKind::ThreepidDenied => ErrorCode::ThreepidDenied,
ErrorKind::ThreepidMediumNotSupported => ErrorCode::ThreepidMediumNotSupported,
ErrorKind::ServerNotTrusted => ErrorCode::ServerNotTrusted,
ErrorKind::UnsupportedRoomVersion => ErrorCode::UnsupportedRoomVersion,
ErrorKind::IncompatibleRoomVersion { .. } => ErrorCode::IncompatibleRoomVersion,
ErrorKind::BadState => ErrorCode::BadState,
ErrorKind::GuestAccessForbidden => ErrorCode::GuestAccessForbidden,
ErrorKind::CaptchaNeeded => ErrorCode::CaptchaNeeded,
ErrorKind::CaptchaInvalid => ErrorCode::CaptchaInvalid,
ErrorKind::MissingParam => ErrorCode::MissingParam,
ErrorKind::InvalidParam => ErrorCode::InvalidParam,
ErrorKind::TooLarge => ErrorCode::TooLarge,
ErrorKind::Exclusive => ErrorCode::Exclusive,
ErrorKind::ResourceLimitExceeded { .. } => ErrorCode::ResourceLimitExceeded,
ErrorKind::CannotLeaveServerNoticeRoom => ErrorCode::CannotLeaveServerNoticeRoom,
ErrorKind::WeakPassword => ErrorCode::WeakPassword,
ErrorKind::UnableToAuthorizeJoin => ErrorCode::UnableToAuthorizeJoin,
ErrorKind::UnableToGrantJoin => ErrorCode::UnableToGrantJoin,
ErrorKind::BadAlias => ErrorCode::BadAlias,
ErrorKind::DuplicateAnnotation => ErrorCode::DuplicateAnnotation,
ErrorKind::NotYetUploaded => ErrorCode::NotYetUploaded,
ErrorKind::CannotOverwriteMedia => ErrorCode::CannotOverwriteMedia,
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
ErrorKind::UnknownPos => ErrorCode::UnknownPos,
ErrorKind::UrlNotSet => ErrorCode::UrlNotSet,
ErrorKind::BadStatus { .. } => ErrorCode::BadStatus,
ErrorKind::ConnectionFailed => ErrorCode::ConnectionFailed,
ErrorKind::ConnectionTimeout => ErrorCode::ConnectionTimeout,
ErrorKind::WrongRoomKeysVersion { .. } => ErrorCode::WrongRoomKeysVersion,
#[cfg(feature = "unstable-msc3843")]
ErrorKind::Unactionable => ErrorCode::Unactionable,
ErrorKind::UserLocked => ErrorCode::UserLocked,
ErrorKind::UserSuspended => ErrorCode::UserSuspended,
ErrorKind::_Custom { errcode, .. } => errcode.0.clone().into(),
}
}
}
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Extra(BTreeMap<String, JsonValue>);
#[derive(Clone, StringEnum)]
#[non_exhaustive]
#[ruma_enum(rename_all = "M_MATRIX_ERROR_CASE")]
pub enum ErrorCode {
Forbidden,
UnknownToken,
MissingToken,
BadJson,
NotJson,
NotFound,
LimitExceeded,
Unknown,
Unrecognized,
Unauthorized,
UserDeactivated,
UserInUse,
InvalidUsername,
RoomInUse,
InvalidRoomState,
ThreepidInUse,
ThreepidNotFound,
ThreepidAuthFailed,
ThreepidDenied,
ThreepidMediumNotSupported,
ServerNotTrusted,
UnsupportedRoomVersion,
IncompatibleRoomVersion,
BadState,
GuestAccessForbidden,
CaptchaNeeded,
CaptchaInvalid,
MissingParam,
InvalidParam,
TooLarge,
Exclusive,
ResourceLimitExceeded,
CannotLeaveServerNoticeRoom,
WeakPassword,
#[ruma_enum(rename = "M_UNABLE_TO_AUTHORISE_JOIN")]
UnableToAuthorizeJoin,
UnableToGrantJoin,
BadAlias,
DuplicateAnnotation,
NotYetUploaded,
CannotOverwriteMedia,
#[cfg(any(feature = "unstable-msc3575", feature = "unstable-msc4186"))]
UnknownPos,
UrlNotSet,
BadStatus,
ConnectionFailed,
ConnectionTimeout,
WrongRoomKeysVersion,
#[cfg(feature = "unstable-msc3843")]
Unactionable,
UserLocked,
UserSuspended,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
#[derive(Debug, Clone)]
#[allow(clippy::exhaustive_enums)]
pub enum ErrorBody {
Standard {
kind: ErrorKind,
message: String,
},
Json(JsonValue),
NotJson {
bytes: Bytes,
deserialization_error: Arc<serde_json::Error>,
},
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct StandardErrorBody {
#[serde(flatten)]
pub kind: ErrorKind,
#[serde(rename = "error")]
pub message: String,
}
#[derive(Debug, Clone)]
#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
pub struct Error {
pub status_code: http::StatusCode,
pub body: ErrorBody,
}
impl Error {
pub fn new(status_code: http::StatusCode, body: ErrorBody) -> Self {
Self { status_code, body }
}
pub fn error_kind(&self) -> Option<&ErrorKind> {
as_variant!(&self.body, ErrorBody::Standard { kind, .. } => kind)
}
}
impl EndpointError for Error {
fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self {
let status = response.status();
let body_bytes = &response.body().as_ref();
let error_body: ErrorBody = match from_json_slice(body_bytes) {
Ok(StandardErrorBody { mut kind, message }) => {
let headers = response.headers();
match &mut kind {
#[cfg(feature = "unstable-msc2967")]
ErrorKind::Forbidden { authenticate } => {
*authenticate = headers
.get(http::header::WWW_AUTHENTICATE)
.and_then(|val| val.to_str().ok())
.and_then(AuthenticateError::from_str);
}
ErrorKind::LimitExceeded { retry_after } => {
if let Some(Ok(retry_after_header)) =
headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
{
*retry_after = Some(retry_after_header);
}
}
_ => {}
}
ErrorBody::Standard { kind, message }
}
Err(_) => match MatrixErrorBody::from_bytes(body_bytes) {
MatrixErrorBody::Json(json) => ErrorBody::Json(json),
MatrixErrorBody::NotJson { bytes, deserialization_error, .. } => {
ErrorBody::NotJson { bytes, deserialization_error }
}
},
};
error_body.into_error(status)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status_code = self.status_code.as_u16();
match &self.body {
ErrorBody::Standard { kind, message } => {
let errcode = kind.errcode();
write!(f, "[{status_code} / {errcode}] {message}")
}
ErrorBody::Json(json) => write!(f, "[{status_code}] {json}"),
ErrorBody::NotJson { .. } => write!(f, "[{status_code}] <non-json bytes>"),
}
}
}
impl std::error::Error for Error {}
impl ErrorBody {
pub fn into_error(self, status_code: http::StatusCode) -> Error {
Error { status_code, body: self }
}
}
impl OutgoingResponse for Error {
fn try_into_http_response<T: Default + BufMut>(
self,
) -> Result<http::Response<T>, IntoHttpError> {
let mut builder = http::Response::builder()
.header(http::header::CONTENT_TYPE, "application/json")
.status(self.status_code);
#[allow(clippy::collapsible_match)]
if let ErrorBody::Standard { kind, .. } = &self.body {
match kind {
#[cfg(feature = "unstable-msc2967")]
ErrorKind::Forbidden { authenticate: Some(auth_error) } => {
builder = builder.header(http::header::WWW_AUTHENTICATE, auth_error);
}
ErrorKind::LimitExceeded { retry_after: Some(retry_after) } => {
let header_value = http::HeaderValue::try_from(retry_after)?;
builder = builder.header(http::header::RETRY_AFTER, header_value);
}
_ => {}
}
}
builder
.body(match self.body {
ErrorBody::Standard { kind, message } => {
ruma_common::serde::json_to_buf(&StandardErrorBody { kind, message })?
}
ErrorBody::Json(json) => ruma_common::serde::json_to_buf(&json)?,
ErrorBody::NotJson { .. } => {
return Err(IntoHttpError::Json(serde::ser::Error::custom(
"attempted to serialize ErrorBody::NotJson",
)));
}
})
.map_err(Into::into)
}
}
#[cfg(feature = "unstable-msc2967")]
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthenticateError {
InsufficientScope {
scope: String,
},
#[doc(hidden)]
_Custom { errcode: PrivOwnedStr, attributes: AuthenticateAttrs },
}
#[cfg(feature = "unstable-msc2967")]
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthenticateAttrs(BTreeMap<String, String>);
#[cfg(feature = "unstable-msc2967")]
impl AuthenticateError {
fn from_str(s: &str) -> Option<Self> {
if let Some(val) = s.strip_prefix("Bearer").map(str::trim) {
let mut errcode = None;
let mut attrs = BTreeMap::new();
for (key, value) in val
.split(',')
.filter_map(|attr| attr.trim().split_once('='))
.map(|(key, value)| (key, value.trim_matches('"')))
{
if key == "error" {
errcode = Some(value);
} else {
attrs.insert(key.to_owned(), value.to_owned());
}
}
if let Some(errcode) = errcode {
let error = if let Some(scope) =
attrs.get("scope").filter(|_| errcode == "insufficient_scope")
{
AuthenticateError::InsufficientScope { scope: scope.to_owned() }
} else {
AuthenticateError::_Custom {
errcode: PrivOwnedStr(errcode.into()),
attributes: AuthenticateAttrs(attrs),
}
};
return Some(error);
}
}
None
}
}
#[cfg(feature = "unstable-msc2967")]
impl TryFrom<&AuthenticateError> for http::HeaderValue {
type Error = http::header::InvalidHeaderValue;
fn try_from(error: &AuthenticateError) -> Result<Self, Self::Error> {
let s = match error {
AuthenticateError::InsufficientScope { scope } => {
format!("Bearer error=\"insufficient_scope\", scope=\"{scope}\"")
}
AuthenticateError::_Custom { errcode, attributes } => {
let mut s = format!("Bearer error=\"{}\"", errcode.0);
for (key, value) in attributes.0.iter() {
s.push_str(&format!(", {key}=\"{value}\""));
}
s
}
};
s.try_into()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum RetryAfter {
Delay(Duration),
DateTime(SystemTime),
}
impl TryFrom<&http::HeaderValue> for RetryAfter {
type Error = HeaderDeserializationError;
fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
if value.as_bytes().iter().all(|b| b.is_ascii_digit()) {
Ok(Self::Delay(Duration::from_secs(u64::from_str(value.to_str()?)?)))
} else {
Ok(Self::DateTime(http_date_to_system_time(value)?))
}
}
}
impl TryFrom<&RetryAfter> for http::HeaderValue {
type Error = HeaderSerializationError;
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
match value {
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
RetryAfter::DateTime(time) => system_time_to_http_date(time),
}
}
}
pub trait FromHttpResponseErrorExt {
fn error_kind(&self) -> Option<&ErrorKind>;
}
impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
fn error_kind(&self) -> Option<&ErrorKind> {
as_variant!(self, Self::Server)?.error_kind()
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use ruma_common::api::{EndpointError, OutgoingResponse};
use serde_json::{
from_slice as from_json_slice, from_value as from_json_value, json, Value as JsonValue,
};
use web_time::{Duration, UNIX_EPOCH};
use super::{Error, ErrorBody, ErrorKind, RetryAfter, StandardErrorBody};
#[test]
fn deserialize_forbidden() {
let deserialized: StandardErrorBody = from_json_value(json!({
"errcode": "M_FORBIDDEN",
"error": "You are not authorized to ban users in this room.",
}))
.unwrap();
assert_eq!(
deserialized.kind,
ErrorKind::Forbidden {
#[cfg(feature = "unstable-msc2967")]
authenticate: None
}
);
assert_eq!(deserialized.message, "You are not authorized to ban users in this room.");
}
#[test]
fn deserialize_wrong_room_key_version() {
let deserialized: StandardErrorBody = from_json_value(json!({
"current_version": "42",
"errcode": "M_WRONG_ROOM_KEYS_VERSION",
"error": "Wrong backup version."
}))
.expect("We should be able to deserialize a wrong room keys version error");
assert_matches!(deserialized.kind, ErrorKind::WrongRoomKeysVersion { current_version });
assert_eq!(current_version.as_deref(), Some("42"));
assert_eq!(deserialized.message, "Wrong backup version.");
}
#[cfg(feature = "unstable-msc2967")]
#[test]
fn custom_authenticate_error_sanity() {
use super::AuthenticateError;
let s = "Bearer error=\"custom_error\", misc=\"some content\"";
let error = AuthenticateError::from_str(s).unwrap();
let error_header = http::HeaderValue::try_from(&error).unwrap();
assert_eq!(error_header.to_str().unwrap(), s);
}
#[cfg(feature = "unstable-msc2967")]
#[test]
fn serialize_insufficient_scope() {
use super::AuthenticateError;
let error =
AuthenticateError::InsufficientScope { scope: "something_privileged".to_owned() };
let error_header = http::HeaderValue::try_from(&error).unwrap();
assert_eq!(
error_header.to_str().unwrap(),
"Bearer error=\"insufficient_scope\", scope=\"something_privileged\""
);
}
#[cfg(feature = "unstable-msc2967")]
#[test]
fn deserialize_insufficient_scope() {
use super::AuthenticateError;
let response = http::Response::builder()
.header(
http::header::WWW_AUTHENTICATE,
"Bearer error=\"insufficient_scope\", scope=\"something_privileged\"",
)
.status(http::StatusCode::UNAUTHORIZED)
.body(
serde_json::to_string(&json!({
"errcode": "M_FORBIDDEN",
"error": "Insufficient privilege",
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::UNAUTHORIZED);
assert_matches!(error.body, ErrorBody::Standard { kind, message });
assert_matches!(kind, ErrorKind::Forbidden { authenticate });
assert_eq!(message, "Insufficient privilege");
assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
assert_eq!(scope, "something_privileged");
}
#[test]
fn deserialize_limit_exceeded_no_retry_after() {
let response = http::Response::builder()
.status(http::StatusCode::TOO_MANY_REQUESTS)
.body(
serde_json::to_string(&json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
assert_matches!(
error.body,
ErrorBody::Standard { kind: ErrorKind::LimitExceeded { retry_after: None }, message }
);
assert_eq!(message, "Too many requests");
}
#[test]
fn deserialize_limit_exceeded_retry_after_body() {
let response = http::Response::builder()
.status(http::StatusCode::TOO_MANY_REQUESTS)
.body(
serde_json::to_string(&json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
"retry_after_ms": 2000,
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
assert_matches!(
error.body,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
message
}
);
assert_matches!(retry_after, RetryAfter::Delay(delay));
assert_eq!(delay.as_millis(), 2000);
assert_eq!(message, "Too many requests");
}
#[test]
fn deserialize_limit_exceeded_retry_after_header_delay() {
let response = http::Response::builder()
.status(http::StatusCode::TOO_MANY_REQUESTS)
.header(http::header::RETRY_AFTER, "2")
.body(
serde_json::to_string(&json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
assert_matches!(
error.body,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
message
}
);
assert_matches!(retry_after, RetryAfter::Delay(delay));
assert_eq!(delay.as_millis(), 2000);
assert_eq!(message, "Too many requests");
}
#[test]
fn deserialize_limit_exceeded_retry_after_header_datetime() {
let response = http::Response::builder()
.status(http::StatusCode::TOO_MANY_REQUESTS)
.header(http::header::RETRY_AFTER, "Fri, 15 May 2015 15:34:21 GMT")
.body(
serde_json::to_string(&json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
assert_matches!(
error.body,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
message
}
);
assert_matches!(retry_after, RetryAfter::DateTime(time));
assert_eq!(time.duration_since(UNIX_EPOCH).unwrap().as_secs(), 1_431_704_061);
assert_eq!(message, "Too many requests");
}
#[test]
fn deserialize_limit_exceeded_retry_after_header_over_body() {
let response = http::Response::builder()
.status(http::StatusCode::TOO_MANY_REQUESTS)
.header(http::header::RETRY_AFTER, "2")
.body(
serde_json::to_string(&json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
"retry_after_ms": 3000,
}))
.unwrap(),
)
.unwrap();
let error = Error::from_http_response(response);
assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
assert_matches!(
error.body,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
message
}
);
assert_matches!(retry_after, RetryAfter::Delay(delay));
assert_eq!(delay.as_millis(), 2000);
assert_eq!(message, "Too many requests");
}
#[test]
fn serialize_limit_exceeded_retry_after_none() {
let error = Error::new(
http::StatusCode::TOO_MANY_REQUESTS,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded { retry_after: None },
message: "Too many requests".to_owned(),
},
);
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
assert_eq!(response.headers().get(http::header::RETRY_AFTER), None);
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
assert_eq!(
json_body,
json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
})
);
}
#[test]
fn serialize_limit_exceeded_retry_after_delay() {
let error = Error::new(
http::StatusCode::TOO_MANY_REQUESTS,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded {
retry_after: Some(RetryAfter::Delay(Duration::from_secs(3))),
},
message: "Too many requests".to_owned(),
},
);
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
assert_eq!(retry_after_header.to_str().unwrap(), "3");
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
assert_eq!(
json_body,
json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
"retry_after_ms": 3000,
})
);
}
#[test]
fn serialize_limit_exceeded_retry_after_datetime() {
let error = Error::new(
http::StatusCode::TOO_MANY_REQUESTS,
ErrorBody::Standard {
kind: ErrorKind::LimitExceeded {
retry_after: Some(RetryAfter::DateTime(
UNIX_EPOCH + Duration::from_secs(1_431_704_061),
)),
},
message: "Too many requests".to_owned(),
},
);
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
assert_eq!(retry_after_header.to_str().unwrap(), "Fri, 15 May 2015 15:34:21 GMT");
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
assert_eq!(
json_body,
json!({
"errcode": "M_LIMIT_EXCEEDED",
"error": "Too many requests",
})
);
}
#[test]
fn serialize_user_locked() {
let error = Error::new(
http::StatusCode::UNAUTHORIZED,
ErrorBody::Standard {
kind: ErrorKind::UserLocked,
message: "This account has been locked".to_owned(),
},
);
let response = error.try_into_http_response::<Vec<u8>>().unwrap();
assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
let json_body: JsonValue = from_json_slice(response.body()).unwrap();
assert_eq!(
json_body,
json!({
"errcode": "M_USER_LOCKED",
"error": "This account has been locked",
"soft_logout": true,
})
);
}
}