client-api: Add helper methods to convert SystemTime from/to a HTTP date
This commit is contained in:
parent
b4d0ab42a3
commit
10c7e59c57
@ -6,17 +6,22 @@ use as_variant::as_variant;
|
|||||||
use bytes::{BufMut, Bytes};
|
use bytes::{BufMut, Bytes};
|
||||||
use ruma_common::{
|
use ruma_common::{
|
||||||
api::{
|
api::{
|
||||||
error::{FromHttpResponseError, IntoHttpError, MatrixErrorBody},
|
error::{
|
||||||
|
FromHttpResponseError, HeaderDeserializationError, HeaderSerializationError,
|
||||||
|
IntoHttpError, MatrixErrorBody,
|
||||||
|
},
|
||||||
EndpointError, OutgoingResponse,
|
EndpointError, OutgoingResponse,
|
||||||
},
|
},
|
||||||
RoomVersionId,
|
RoomVersionId,
|
||||||
};
|
};
|
||||||
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};
|
||||||
use web_time::{Duration, SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use crate::PrivOwnedStr;
|
use crate::{
|
||||||
|
http_headers::{http_date_to_system_time, system_time_to_http_date},
|
||||||
|
PrivOwnedStr,
|
||||||
|
};
|
||||||
|
|
||||||
/// Deserialize and Serialize implementations for ErrorKind.
|
/// Deserialize and Serialize implementations for ErrorKind.
|
||||||
/// Separate module because it's a lot of code.
|
/// Separate module because it's a lot of code.
|
||||||
@ -372,9 +377,8 @@ impl EndpointError for Error {
|
|||||||
ErrorKind::LimitExceeded { retry_after } => {
|
ErrorKind::LimitExceeded { retry_after } => {
|
||||||
// The Retry-After header takes precedence over the retry_after_ms field in
|
// The Retry-After header takes precedence over the retry_after_ms field in
|
||||||
// the body.
|
// the body.
|
||||||
if let Some(retry_after_header) = headers
|
if let Some(Ok(retry_after_header)) =
|
||||||
.get(http::header::RETRY_AFTER)
|
headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
|
||||||
.and_then(RetryAfter::from_header_value)
|
|
||||||
{
|
{
|
||||||
*retry_after = Some(retry_after_header);
|
*retry_after = Some(retry_after_header);
|
||||||
}
|
}
|
||||||
@ -567,57 +571,29 @@ pub enum RetryAfter {
|
|||||||
DateTime(SystemTime),
|
DateTime(SystemTime),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RetryAfter {
|
impl TryFrom<&http::HeaderValue> for RetryAfter {
|
||||||
fn from_header_value(value: &http::HeaderValue) -> Option<Self> {
|
type Error = HeaderDeserializationError;
|
||||||
let bytes = value.as_bytes();
|
|
||||||
|
|
||||||
if bytes.iter().all(|b| b.is_ascii_digit()) {
|
fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
|
||||||
|
if value.as_bytes().iter().all(|b| b.is_ascii_digit()) {
|
||||||
// It should be a duration.
|
// It should be a duration.
|
||||||
Some(Self::Delay(Duration::from_secs(u64::from_str(value.to_str().ok()?).ok()?)))
|
Ok(Self::Delay(Duration::from_secs(u64::from_str(value.to_str()?)?)))
|
||||||
} else {
|
} else {
|
||||||
// It should be a date.
|
// It should be a date.
|
||||||
let ts = date_header::parse(bytes).ok()?;
|
Ok(Self::DateTime(http_date_to_system_time(value)?))
|
||||||
Some(Self::DateTime(UNIX_EPOCH.checked_add(Duration::from_secs(ts))?))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&RetryAfter> for http::HeaderValue {
|
impl TryFrom<&RetryAfter> for http::HeaderValue {
|
||||||
type Error = RetryAfterInvalidDateTime;
|
type Error = HeaderSerializationError;
|
||||||
|
|
||||||
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
|
fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
|
||||||
match value {
|
match value {
|
||||||
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
|
RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
|
||||||
RetryAfter::DateTime(time) => {
|
RetryAfter::DateTime(time) => system_time_to_http_date(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>`.
|
||||||
|
@ -1,10 +1,40 @@
|
|||||||
//! Custom HTTP headers not defined in the `http` crate.
|
//! Helpers for HTTP headers with the `http` crate.
|
||||||
#![allow(clippy::declare_interior_mutable_const)]
|
#![allow(clippy::declare_interior_mutable_const)]
|
||||||
|
|
||||||
use http::header::HeaderName;
|
use http::{header::HeaderName, HeaderValue};
|
||||||
|
use ruma_common::api::error::{HeaderDeserializationError, HeaderSerializationError};
|
||||||
|
use web_time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
/// The [`Cross-Origin-Resource-Policy`] HTTP response header.
|
/// The [`Cross-Origin-Resource-Policy`] HTTP response header.
|
||||||
///
|
///
|
||||||
/// [`Cross-Origin-Resource-Policy`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy
|
/// [`Cross-Origin-Resource-Policy`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy
|
||||||
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
|
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
|
||||||
HeaderName::from_static("cross-origin-resource-policy");
|
HeaderName::from_static("cross-origin-resource-policy");
|
||||||
|
|
||||||
|
/// Convert as `SystemTime` to a HTTP date header value.
|
||||||
|
pub fn system_time_to_http_date(
|
||||||
|
time: &SystemTime,
|
||||||
|
) -> Result<HeaderValue, HeaderSerializationError> {
|
||||||
|
let mut buffer = [0; 29];
|
||||||
|
|
||||||
|
let duration =
|
||||||
|
time.duration_since(UNIX_EPOCH).map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
|
||||||
|
date_header::format(duration.as_secs(), &mut buffer)
|
||||||
|
.map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
|
||||||
|
|
||||||
|
Ok(http::HeaderValue::from_bytes(&buffer)
|
||||||
|
.expect("date_header should produce a valid header value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert a header value representing a HTTP date to a `SystemTime`.
|
||||||
|
pub fn http_date_to_system_time(
|
||||||
|
value: &HeaderValue,
|
||||||
|
) -> Result<SystemTime, HeaderDeserializationError> {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
|
||||||
|
let ts = date_header::parse(bytes).map_err(|_| HeaderDeserializationError::InvalidHttpDate)?;
|
||||||
|
|
||||||
|
UNIX_EPOCH
|
||||||
|
.checked_add(Duration::from_secs(ts))
|
||||||
|
.ok_or(HeaderDeserializationError::InvalidHttpDate)
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ Bug fixes:
|
|||||||
- Allow to deserialize `Ruleset` with missing fields.
|
- Allow to deserialize `Ruleset` with missing fields.
|
||||||
|
|
||||||
Breaking changes:
|
Breaking changes:
|
||||||
|
|
||||||
- The power levels fields in `PushConditionRoomCtx` are grouped in an optional `power_levels` field.
|
- The power levels fields in `PushConditionRoomCtx` are grouped in an optional `power_levels` field.
|
||||||
If the field is missing, push rules that depend on it will never match. However, this allows to
|
If the field is missing, push rules that depend on it will never match. However, this allows to
|
||||||
match the `.m.rule.invite_for_me` push rule because usually the `invite_state` doesn't include
|
match the `.m.rule.invite_for_me` push rule because usually the `invite_state` doesn't include
|
||||||
@ -14,6 +15,7 @@ Breaking changes:
|
|||||||
- `deserialize_as_f64_or_string` has been extended to also support parsing integers, and renamed to
|
- `deserialize_as_f64_or_string` has been extended to also support parsing integers, and renamed to
|
||||||
`deserialize_as_number_or_string` to reflect that.
|
`deserialize_as_number_or_string` to reflect that.
|
||||||
- The http crate had a major version bump to version 1.1
|
- The http crate had a major version bump to version 1.1
|
||||||
|
- `IntoHttpError::Header` now contains a `HeaderSerializationError`
|
||||||
|
|
||||||
Improvements:
|
Improvements:
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//! converting between http requests / responses and ruma's representation of
|
//! converting between http requests / responses and ruma's representation of
|
||||||
//! matrix API requests / responses.
|
//! matrix API requests / responses.
|
||||||
|
|
||||||
use std::{error::Error as StdError, fmt, sync::Arc};
|
use std::{error::Error as StdError, fmt, num::ParseIntError, sync::Arc};
|
||||||
|
|
||||||
use bytes::{BufMut, Bytes};
|
use bytes::{BufMut, Bytes};
|
||||||
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
|
||||||
@ -127,20 +127,19 @@ pub enum IntoHttpError {
|
|||||||
|
|
||||||
/// Header serialization failed.
|
/// Header serialization failed.
|
||||||
#[error("header serialization failed: {0}")]
|
#[error("header serialization failed: {0}")]
|
||||||
Header(#[from] http::header::InvalidHeaderValue),
|
Header(#[from] HeaderSerializationError),
|
||||||
|
|
||||||
/// 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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<http::header::InvalidHeaderValue> for IntoHttpError {
|
||||||
|
fn from(value: http::header::InvalidHeaderValue) -> Self {
|
||||||
|
Self::Header(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// An error when converting a http request to one of ruma's endpoint-specific request types.
|
/// An error when converting a http request to one of ruma's endpoint-specific request types.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@ -258,13 +257,21 @@ impl From<http::header::ToStrError> for DeserializationError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error with the http headers.
|
/// An error when deserializing the HTTP headers.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum HeaderDeserializationError {
|
pub enum HeaderDeserializationError {
|
||||||
/// Failed to convert `http::header::HeaderValue` to `str`.
|
/// Failed to convert `http::header::HeaderValue` to `str`.
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
ToStrError(http::header::ToStrError),
|
ToStrError(#[from] http::header::ToStrError),
|
||||||
|
|
||||||
|
/// Failed to convert `http::header::HeaderValue` to an integer.
|
||||||
|
#[error("{0}")]
|
||||||
|
ParseIntError(#[from] ParseIntError),
|
||||||
|
|
||||||
|
/// Failed to parse a HTTP date from a `http::header::Value`.
|
||||||
|
#[error("failed to parse HTTP date")]
|
||||||
|
InvalidHttpDate,
|
||||||
|
|
||||||
/// The given required header is missing.
|
/// The given required header is missing.
|
||||||
#[error("missing header `{0}`")]
|
#[error("missing header `{0}`")]
|
||||||
@ -303,3 +310,19 @@ impl fmt::Display for IncorrectArgumentCount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StdError for IncorrectArgumentCount {}
|
impl StdError for IncorrectArgumentCount {}
|
||||||
|
|
||||||
|
/// An error when serializing the HTTP headers.
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum HeaderSerializationError {
|
||||||
|
/// Failed to convert a header value to `http::header::HeaderValue`.
|
||||||
|
#[error(transparent)]
|
||||||
|
ToHeaderValue(#[from] http::header::InvalidHeaderValue),
|
||||||
|
|
||||||
|
/// The `SystemTime` could not be converted to a HTTP date.
|
||||||
|
///
|
||||||
|
/// This only happens if the `SystemTime` provided is too far in the past (before the Unix
|
||||||
|
/// epoch) or the future (after the year 9999).
|
||||||
|
#[error("invalid HTTP date")]
|
||||||
|
InvalidHttpDate,
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user