client-api: Add helper methods to convert SystemTime from/to a HTTP date

This commit is contained in:
Kévin Commaille 2024-05-08 13:46:29 +02:00 committed by Kévin Commaille
parent b4d0ab42a3
commit 10c7e59c57
4 changed files with 87 additions and 56 deletions

View File

@ -6,17 +6,22 @@ use as_variant::as_variant;
use bytes::{BufMut, Bytes};
use ruma_common::{
api::{
error::{FromHttpResponseError, IntoHttpError, MatrixErrorBody},
error::{
FromHttpResponseError, HeaderDeserializationError, HeaderSerializationError,
IntoHttpError, MatrixErrorBody,
},
EndpointError, OutgoingResponse,
},
RoomVersionId,
};
use serde::{Deserialize, Serialize};
use serde_json::{from_slice as from_json_slice, Value as JsonValue};
use thiserror::Error;
use web_time::{Duration, SystemTime, UNIX_EPOCH};
use web_time::{Duration, SystemTime};
use crate::PrivOwnedStr;
use crate::{
http_headers::{http_date_to_system_time, system_time_to_http_date},
PrivOwnedStr,
};
/// Deserialize and Serialize implementations for ErrorKind.
/// Separate module because it's a lot of code.
@ -372,9 +377,8 @@ impl EndpointError for Error {
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)
if let Some(Ok(retry_after_header)) =
headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
{
*retry_after = Some(retry_after_header);
}
@ -567,59 +571,31 @@ pub enum RetryAfter {
DateTime(SystemTime),
}
impl RetryAfter {
fn from_header_value(value: &http::HeaderValue) -> Option<Self> {
let bytes = value.as_bytes();
impl TryFrom<&http::HeaderValue> for RetryAfter {
type Error = HeaderDeserializationError;
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.
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 {
// It should be a date.
let ts = date_header::parse(bytes).ok()?;
Some(Self::DateTime(UNIX_EPOCH.checked_add(Duration::from_secs(ts))?))
Ok(Self::DateTime(http_date_to_system_time(value)?))
}
}
}
impl TryFrom<&RetryAfter> for http::HeaderValue {
type Error = RetryAfterInvalidDateTime;
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) => {
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)
}
RetryAfter::DateTime(time) => system_time_to_http_date(time),
}
}
}
/// 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>`.
pub trait FromHttpResponseErrorExt {
/// If `self` is a server error in the `errcode` + `error` format expected

View File

@ -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)]
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.
///
/// [`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 =
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)
}

View File

@ -5,6 +5,7 @@ Bug fixes:
- Allow to deserialize `Ruleset` with missing fields.
Breaking changes:
- 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
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_number_or_string` to reflect that.
- The http crate had a major version bump to version 1.1
- `IntoHttpError::Header` now contains a `HeaderSerializationError`
Improvements:

View File

@ -2,7 +2,7 @@
//! converting between http requests / responses and ruma's representation of
//! 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 serde_json::{from_slice as from_json_slice, Value as JsonValue};
@ -127,20 +127,19 @@ pub enum IntoHttpError {
/// Header serialization failed.
#[error("header serialization failed: {0}")]
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,
Header(#[from] HeaderSerializationError),
/// HTTP request construction failed.
#[error("HTTP request construction failed: {0}")]
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.
#[derive(Debug, Error)]
#[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)]
#[non_exhaustive]
pub enum HeaderDeserializationError {
/// Failed to convert `http::header::HeaderValue` to `str`.
#[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.
#[error("missing header `{0}`")]
@ -303,3 +310,19 @@ impl fmt::Display 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,
}