client-api: Add support for the Retry-After header
According to MSC4041 / Matrix 1.10 Co-authored-by: Jonas Platte <jplatte+git@posteo.de>
This commit is contained in:
parent
1e185ddb9d
commit
31dc4a2dfb
@ -16,6 +16,9 @@ Breaking changes:
|
|||||||
- `Error` is now non-exhaustive.
|
- `Error` is now non-exhaustive.
|
||||||
- `ErrorKind::Forbidden` is now a non-exhaustive struct variant that can be
|
- `ErrorKind::Forbidden` is now a non-exhaustive struct variant that can be
|
||||||
constructed with `ErrorKind::forbidden()`.
|
constructed with `ErrorKind::forbidden()`.
|
||||||
|
- The `retry_after_ms` field of `ErrorKind::LimitExceeded` was renamed to
|
||||||
|
`retry_after` and is now an `Option<RetryAfter>`, to add support for the
|
||||||
|
Retry-After header, according to MSC4041 / Matrix 1.10
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
|
@ -52,6 +52,7 @@ unstable-msc3983 = []
|
|||||||
as_variant = { workspace = true }
|
as_variant = { workspace = true }
|
||||||
assign = { workspace = true }
|
assign = { workspace = true }
|
||||||
bytes = "1.0.1"
|
bytes = "1.0.1"
|
||||||
|
date_header = "1.0.5"
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
js_int = { workspace = true, features = ["serde"] }
|
js_int = { workspace = true, features = ["serde"] }
|
||||||
js_option = "0.1.1"
|
js_option = "0.1.1"
|
||||||
@ -61,6 +62,8 @@ ruma-events = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_html_form = { workspace = true }
|
serde_html_form = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
web-time = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_matches2 = { workspace = true }
|
assert_matches2 = { workspace = true }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//! Errors that can be sent from the homeserver.
|
//! Errors that can be sent from the homeserver.
|
||||||
|
|
||||||
use std::{collections::BTreeMap, fmt, sync::Arc, time::Duration};
|
use std::{collections::BTreeMap, fmt, str::FromStr, sync::Arc};
|
||||||
|
|
||||||
use as_variant::as_variant;
|
use as_variant::as_variant;
|
||||||
use bytes::{BufMut, Bytes};
|
use bytes::{BufMut, Bytes};
|
||||||
@ -13,6 +13,8 @@ use ruma_common::{
|
|||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
||||||
|
use thiserror::Error;
|
||||||
|
use web_time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use crate::PrivOwnedStr;
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
@ -59,8 +61,8 @@ pub enum ErrorKind {
|
|||||||
|
|
||||||
/// M_LIMIT_EXCEEDED
|
/// M_LIMIT_EXCEEDED
|
||||||
LimitExceeded {
|
LimitExceeded {
|
||||||
/// How long a client should wait in milliseconds before they can try again.
|
/// How long a client should wait before they can try again.
|
||||||
retry_after_ms: Option<Duration>,
|
retry_after: Option<RetryAfter>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// M_UNKNOWN
|
/// M_UNKNOWN
|
||||||
@ -350,19 +352,29 @@ impl EndpointError for Error {
|
|||||||
|
|
||||||
let body_bytes = &response.body().as_ref();
|
let body_bytes = &response.body().as_ref();
|
||||||
let error_body: ErrorBody = match from_json_slice(body_bytes) {
|
let error_body: ErrorBody = match from_json_slice(body_bytes) {
|
||||||
Ok(StandardErrorBody { kind, message }) => {
|
Ok(StandardErrorBody { mut kind, message }) => {
|
||||||
#[cfg(feature = "unstable-msc2967")]
|
let headers = response.headers();
|
||||||
let kind = if let ErrorKind::Forbidden { .. } = kind {
|
|
||||||
let authenticate = response
|
|
||||||
.headers()
|
|
||||||
.get(http::header::WWW_AUTHENTICATE)
|
|
||||||
.and_then(|val| val.to_str().ok())
|
|
||||||
.and_then(AuthenticateError::from_str);
|
|
||||||
|
|
||||||
ErrorKind::Forbidden { authenticate }
|
match &mut kind {
|
||||||
} else {
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
kind
|
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 } => {
|
||||||
|
// The Retry-After header takes precedence over the retry_after_ms field in
|
||||||
|
// the body.
|
||||||
|
if let Some(retry_after_header) = headers
|
||||||
|
.get(http::header::RETRY_AFTER)
|
||||||
|
.and_then(RetryAfter::from_header_value)
|
||||||
|
{
|
||||||
|
*retry_after = Some(retry_after_header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
ErrorBody::Standard { kind, message }
|
ErrorBody::Standard { kind, message }
|
||||||
}
|
}
|
||||||
@ -406,20 +418,24 @@ impl OutgoingResponse for Error {
|
|||||||
fn try_into_http_response<T: Default + BufMut>(
|
fn try_into_http_response<T: Default + BufMut>(
|
||||||
self,
|
self,
|
||||||
) -> Result<http::Response<T>, IntoHttpError> {
|
) -> Result<http::Response<T>, IntoHttpError> {
|
||||||
let builder = http::Response::builder()
|
let mut builder = http::Response::builder()
|
||||||
.header(http::header::CONTENT_TYPE, "application/json")
|
.header(http::header::CONTENT_TYPE, "application/json")
|
||||||
.status(self.status_code);
|
.status(self.status_code);
|
||||||
|
|
||||||
#[cfg(feature = "unstable-msc2967")]
|
#[allow(clippy::collapsible_match)]
|
||||||
let builder = if let ErrorBody::Standard {
|
if let ErrorBody::Standard { kind, .. } = &self.body {
|
||||||
kind: ErrorKind::Forbidden { authenticate: Some(auth_error) },
|
match kind {
|
||||||
..
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
} = &self.body
|
ErrorKind::Forbidden { authenticate: Some(auth_error) } => {
|
||||||
{
|
builder = builder.header(http::header::WWW_AUTHENTICATE, auth_error);
|
||||||
builder.header(http::header::WWW_AUTHENTICATE, auth_error)
|
}
|
||||||
} else {
|
ErrorKind::LimitExceeded { retry_after: Some(retry_after) } => {
|
||||||
builder
|
let header_value = http::HeaderValue::try_from(retry_after)?;
|
||||||
};
|
builder = builder.header(http::header::RETRY_AFTER, header_value);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
builder
|
builder
|
||||||
.body(match self.body {
|
.body(match self.body {
|
||||||
@ -532,6 +548,72 @@ impl TryFrom<&AuthenticateError> for http::HeaderValue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// How long a client should wait before it tries again.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[allow(clippy::exhaustive_enums)]
|
||||||
|
pub enum RetryAfter {
|
||||||
|
/// The client should wait for the given duration.
|
||||||
|
///
|
||||||
|
/// This variant should be preferred for backwards compatibility, as it will also populate the
|
||||||
|
/// `retry_after_ms` field in the body of the response.
|
||||||
|
Delay(Duration),
|
||||||
|
/// The client should wait for the given date and time.
|
||||||
|
DateTime(SystemTime),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RetryAfter {
|
||||||
|
fn from_header_value(value: &http::HeaderValue) -> Option<Self> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
if bytes.iter().all(|b| b.is_ascii_digit()) {
|
||||||
|
// It should be a duration.
|
||||||
|
Some(Self::Delay(Duration::from_secs(u64::from_str(value.to_str().ok()?).ok()?)))
|
||||||
|
} else {
|
||||||
|
// It should be a date.
|
||||||
|
let ts = date_header::parse(bytes).ok()?;
|
||||||
|
Some(Self::DateTime(UNIX_EPOCH.checked_add(Duration::from_secs(ts))?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&RetryAfter> for http::HeaderValue {
|
||||||
|
type Error = RetryAfterInvalidDateTime;
|
||||||
|
|
||||||
|
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
|
||||||
|
RetryAfter::DateTime(time) => {
|
||||||
|
let mut buffer = [0; 29];
|
||||||
|
let duration =
|
||||||
|
time.duration_since(UNIX_EPOCH).map_err(|_| RetryAfterInvalidDateTime)?;
|
||||||
|
date_header::format(duration.as_secs(), &mut buffer)
|
||||||
|
.map_err(|_| RetryAfterInvalidDateTime)?;
|
||||||
|
let value = http::HeaderValue::from_bytes(&buffer)
|
||||||
|
.expect("date_header should produce a valid header value");
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error when converting a [`RetryAfter`] to a [`http::HeaderValue`].
|
||||||
|
///
|
||||||
|
/// Happens when the `DateTime` is too far in the past (before the Unix epoch) or the
|
||||||
|
/// future (after the year 9999).
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[allow(clippy::exhaustive_structs)]
|
||||||
|
#[error(
|
||||||
|
"Retry-After header serialization failed: the datetime is too far in the past or the future"
|
||||||
|
)]
|
||||||
|
pub struct RetryAfterInvalidDateTime;
|
||||||
|
|
||||||
|
impl From<RetryAfterInvalidDateTime> for IntoHttpError {
|
||||||
|
fn from(_value: RetryAfterInvalidDateTime) -> Self {
|
||||||
|
IntoHttpError::RetryAfterInvalidDatetime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
|
/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
|
||||||
pub trait FromHttpResponseErrorExt {
|
pub trait FromHttpResponseErrorExt {
|
||||||
/// If `self` is a server error in the `errcode` + `error` format expected
|
/// If `self` is a server error in the `errcode` + `error` format expected
|
||||||
@ -548,9 +630,13 @@ impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use assert_matches2::assert_matches;
|
use assert_matches2::assert_matches;
|
||||||
use serde_json::{from_value as from_json_value, json};
|
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::{ErrorKind, StandardErrorBody};
|
use super::{Error, ErrorBody, ErrorKind, RetryAfter, StandardErrorBody};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_forbidden() {
|
fn deserialize_forbidden() {
|
||||||
@ -615,9 +701,7 @@ mod tests {
|
|||||||
#[cfg(feature = "unstable-msc2967")]
|
#[cfg(feature = "unstable-msc2967")]
|
||||||
#[test]
|
#[test]
|
||||||
fn deserialize_insufficient_scope() {
|
fn deserialize_insufficient_scope() {
|
||||||
use ruma_common::api::EndpointError;
|
use super::AuthenticateError;
|
||||||
|
|
||||||
use super::{AuthenticateError, Error, ErrorBody};
|
|
||||||
|
|
||||||
let response = http::Response::builder()
|
let response = http::Response::builder()
|
||||||
.header(
|
.header(
|
||||||
@ -642,4 +726,223 @@ mod tests {
|
|||||||
assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
|
assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
|
||||||
assert_eq!(scope, "something_privileged");
|
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",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ use serde::{
|
|||||||
};
|
};
|
||||||
use serde_json::from_value as from_json_value;
|
use serde_json::from_value as from_json_value;
|
||||||
|
|
||||||
use super::{ErrorKind, Extra};
|
use super::{ErrorKind, Extra, RetryAfter};
|
||||||
use crate::PrivOwnedStr;
|
use crate::PrivOwnedStr;
|
||||||
|
|
||||||
enum Field<'de> {
|
enum Field<'de> {
|
||||||
@ -178,12 +178,13 @@ impl<'de> Visitor<'de> for ErrorKindVisitor {
|
|||||||
ErrCode::NotJson => ErrorKind::NotJson,
|
ErrCode::NotJson => ErrorKind::NotJson,
|
||||||
ErrCode::NotFound => ErrorKind::NotFound,
|
ErrCode::NotFound => ErrorKind::NotFound,
|
||||||
ErrCode::LimitExceeded => ErrorKind::LimitExceeded {
|
ErrCode::LimitExceeded => ErrorKind::LimitExceeded {
|
||||||
retry_after_ms: retry_after_ms
|
retry_after: retry_after_ms
|
||||||
.map(from_json_value::<UInt>)
|
.map(from_json_value::<UInt>)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(de::Error::custom)?
|
.map_err(de::Error::custom)?
|
||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.map(Duration::from_millis),
|
.map(Duration::from_millis)
|
||||||
|
.map(RetryAfter::Delay),
|
||||||
},
|
},
|
||||||
ErrCode::Unknown => ErrorKind::Unknown,
|
ErrCode::Unknown => ErrorKind::Unknown,
|
||||||
ErrCode::Unrecognized => ErrorKind::Unrecognized,
|
ErrCode::Unrecognized => ErrorKind::Unrecognized,
|
||||||
@ -328,7 +329,7 @@ impl Serialize for ErrorKind {
|
|||||||
Self::UnknownToken { soft_logout: true } => {
|
Self::UnknownToken { soft_logout: true } => {
|
||||||
st.serialize_entry("soft_logout", &true)?;
|
st.serialize_entry("soft_logout", &true)?;
|
||||||
}
|
}
|
||||||
Self::LimitExceeded { retry_after_ms: Some(duration) } => {
|
Self::LimitExceeded { retry_after: Some(RetryAfter::Delay(duration)) } => {
|
||||||
st.serialize_entry(
|
st.serialize_entry(
|
||||||
"retry_after_ms",
|
"retry_after_ms",
|
||||||
&UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
|
&UInt::try_from(duration.as_millis()).map_err(ser::Error::custom)?,
|
||||||
|
@ -129,6 +129,13 @@ pub enum IntoHttpError {
|
|||||||
#[error("header serialization failed: {0}")]
|
#[error("header serialization failed: {0}")]
|
||||||
Header(#[from] http::header::InvalidHeaderValue),
|
Header(#[from] http::header::InvalidHeaderValue),
|
||||||
|
|
||||||
|
/// Retry-After header serialization failed because the datetime provided is after the year
|
||||||
|
/// 9999.
|
||||||
|
#[error(
|
||||||
|
"Retry-After header serialization failed: the year of the datetime is bigger than 9999"
|
||||||
|
)]
|
||||||
|
RetryAfterInvalidDatetime,
|
||||||
|
|
||||||
/// HTTP request construction failed.
|
/// HTTP request construction failed.
|
||||||
#[error("HTTP request construction failed: {0}")]
|
#[error("HTTP request construction failed: {0}")]
|
||||||
Http(#[from] http::Error),
|
Http(#[from] http::Error),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user