Skip to main content

ruma_common/api/
error.rs

1//! This module contains types for all kinds of errors that can occur when
2//! converting between http requests / responses and ruma's representation of
3//! matrix API requests / responses.
4
5use std::{error::Error as StdError, fmt, num::ParseIntError, sync::Arc};
6
7use as_variant::as_variant;
8use bytes::{BufMut, Bytes};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value as JsonValue, from_slice as from_json_slice};
11use thiserror::Error;
12
13mod kind;
14mod kind_serde;
15#[cfg(test)]
16mod tests;
17
18pub use self::kind::*;
19use super::{EndpointError, MatrixVersion, OutgoingResponse};
20
21/// An error returned from a Matrix API endpoint.
22#[derive(Clone, Debug)]
23#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
24pub struct Error {
25    /// The http response's status code.
26    pub status_code: http::StatusCode,
27
28    /// The http response's body.
29    pub body: ErrorBody,
30}
31
32impl Error {
33    /// Constructs a new `Error` with the given status code and body.
34    ///
35    /// This is equivalent to calling `body.into_error(status_code)`.
36    pub fn new(status_code: http::StatusCode, body: ErrorBody) -> Self {
37        Self { status_code, body }
38    }
39
40    /// If this is an error with a [`StandardErrorBody`], returns the [`ErrorKind`].
41    pub fn error_kind(&self) -> Option<&ErrorKind> {
42        as_variant!(&self.body, ErrorBody::Standard(StandardErrorBody { kind, .. }) => kind)
43    }
44
45    /// Whether this error matches the expected format for an endpoint that is not implemented by
46    /// the homeserver.
47    ///
48    /// Return `true` if this contains an [`ErrorKind::Unrecognized`] with a
49    /// [`http::StatusCode::NOT_FOUND`].
50    ///
51    /// [unsupported endpoint]:
52    pub fn is_endpoint_not_implemented(&self) -> bool {
53        self.status_code == http::StatusCode::NOT_FOUND
54            && self
55                .error_kind()
56                .is_some_and(|error_kind| matches!(error_kind, ErrorKind::Unrecognized))
57    }
58}
59
60impl fmt::Display for Error {
61    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62        let status_code = self.status_code.as_u16();
63        match &self.body {
64            ErrorBody::Standard(StandardErrorBody { kind, message }) => {
65                let errcode = kind.errcode();
66                write!(f, "[{status_code} / {errcode}] {message}")
67            }
68            ErrorBody::Json(json) => write!(f, "[{status_code}] {json}"),
69            ErrorBody::NotJson { .. } => write!(f, "[{status_code}] <non-json bytes>"),
70        }
71    }
72}
73
74impl StdError for Error {}
75
76impl OutgoingResponse for Error {
77    fn try_into_http_response<T: Default + BufMut>(
78        self,
79    ) -> Result<http::Response<T>, IntoHttpError> {
80        let mut builder = http::Response::builder()
81            .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
82            .status(self.status_code);
83
84        // Add data in headers.
85        if let Some(ErrorKind::LimitExceeded(LimitExceededErrorData {
86            retry_after: Some(retry_after),
87        })) = self.error_kind()
88        {
89            let header_value = http::HeaderValue::try_from(retry_after)?;
90            builder = builder.header(http::header::RETRY_AFTER, header_value);
91        }
92
93        builder
94            .body(match self.body {
95                ErrorBody::Standard(standard_body) => {
96                    ruma_common::serde::json_to_buf(&standard_body)?
97                }
98                ErrorBody::Json(json) => ruma_common::serde::json_to_buf(&json)?,
99                ErrorBody::NotJson { .. } => {
100                    return Err(IntoHttpError::Json(serde::ser::Error::custom(
101                        "attempted to serialize ErrorBody::NotJson",
102                    )));
103                }
104            })
105            .map_err(Into::into)
106    }
107}
108
109impl EndpointError for Error {
110    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self {
111        let status = response.status();
112
113        let body_bytes = &response.body().as_ref();
114        let error_body: ErrorBody = match from_json_slice::<StandardErrorBody>(body_bytes) {
115            Ok(mut standard_body) => {
116                let headers = response.headers();
117
118                if let ErrorKind::LimitExceeded(LimitExceededErrorData { retry_after }) =
119                    &mut standard_body.kind
120                {
121                    // The Retry-After header takes precedence over the retry_after_ms field in
122                    // the body.
123                    if let Some(Ok(retry_after_header)) =
124                        headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
125                    {
126                        *retry_after = Some(retry_after_header);
127                    }
128                }
129
130                ErrorBody::Standard(standard_body)
131            }
132            Err(_) => match from_json_slice(body_bytes) {
133                Ok(json) => ErrorBody::Json(json),
134                Err(error) => ErrorBody::NotJson {
135                    bytes: Bytes::copy_from_slice(body_bytes),
136                    deserialization_error: Arc::new(error),
137                },
138            },
139        };
140
141        error_body.into_error(status)
142    }
143}
144
145/// The body of a Matrix API endpoint error.
146#[derive(Debug, Clone)]
147#[allow(clippy::exhaustive_enums)]
148pub enum ErrorBody {
149    /// A JSON body with the fields expected for Matrix endpoints errors.
150    Standard(StandardErrorBody),
151
152    /// A JSON body with an unexpected structure.
153    Json(JsonValue),
154
155    /// A response body that is not valid JSON.
156    NotJson {
157        /// The raw bytes of the response body.
158        bytes: Bytes,
159
160        /// The error from trying to deserialize the bytes as JSON.
161        deserialization_error: Arc<serde_json::Error>,
162    },
163}
164
165impl ErrorBody {
166    /// Convert the ErrorBody into an Error by adding the http status code.
167    ///
168    /// This is equivalent to calling `Error::new(status_code, self)`.
169    pub fn into_error(self, status_code: http::StatusCode) -> Error {
170        Error { status_code, body: self }
171    }
172}
173
174/// A JSON body with the fields expected for Matrix API endpoints errors.
175#[derive(Clone, Debug, Deserialize, Serialize)]
176#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
177pub struct StandardErrorBody {
178    /// A value which can be used to handle an error message.
179    #[serde(flatten)]
180    pub kind: ErrorKind,
181
182    /// A human-readable error message, usually a sentence explaining what went wrong.
183    #[serde(rename = "error")]
184    pub message: String,
185}
186
187impl StandardErrorBody {
188    /// Construct a new `StandardErrorBody` with the given kind and message.
189    pub fn new(kind: ErrorKind, message: String) -> Self {
190        Self { kind, message }
191    }
192}
193
194/// An error when converting one of ruma's endpoint-specific request or response
195/// types to the corresponding http type.
196#[derive(Debug, Error)]
197#[non_exhaustive]
198pub enum IntoHttpError {
199    /// Failed to add the authentication scheme to the request.
200    #[error("failed to add authentication scheme: {0}")]
201    Authentication(Box<dyn std::error::Error + Send + Sync + 'static>),
202
203    /// Tried to create a request with an old enough version, for which no unstable endpoint
204    /// exists.
205    ///
206    /// This is also a fallback error for if the version is too new for this endpoint.
207    #[error(
208        "endpoint was not supported by server-reported versions, \
209         but no unstable path to fall back to was defined"
210    )]
211    NoUnstablePath,
212
213    /// Tried to create a request with [`MatrixVersion`]s for all of which this endpoint was
214    /// removed.
215    #[error(
216        "could not create any path variant for endpoint, as it was removed in version {}",
217        .0.as_str().expect("no endpoint was removed in Matrix 1.0")
218    )]
219    EndpointRemoved(MatrixVersion),
220
221    /// JSON serialization failed.
222    #[error("JSON serialization failed: {0}")]
223    Json(#[from] serde_json::Error),
224
225    /// Query parameter serialization failed.
226    #[error("query parameter serialization failed: {0}")]
227    Query(#[from] serde_html_form::ser::Error),
228
229    /// Header serialization failed.
230    #[error("header serialization failed: {0}")]
231    Header(#[from] HeaderSerializationError),
232
233    /// HTTP request construction failed.
234    #[error("HTTP request construction failed: {0}")]
235    Http(#[from] http::Error),
236}
237
238impl IntoHttpError {
239    /// Construct an [`Authentication`](Self::Authentication) error from the given underlying error.
240    pub fn authentication(
241        error: impl Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
242    ) -> Self {
243        Self::Authentication(error.into())
244    }
245}
246
247impl From<http::header::InvalidHeaderValue> for IntoHttpError {
248    fn from(value: http::header::InvalidHeaderValue) -> Self {
249        Self::Header(value.into())
250    }
251}
252
253/// An error when converting a http request to one of ruma's endpoint-specific request types.
254#[derive(Debug, Error)]
255#[non_exhaustive]
256pub enum FromHttpRequestError {
257    /// Deserialization failed
258    #[error("deserialization failed: {0}")]
259    Deserialization(DeserializationError),
260
261    /// HTTP method mismatch
262    #[error("http method mismatch: expected {expected}, received: {received}")]
263    MethodMismatch {
264        /// expected http method
265        expected: http::method::Method,
266        /// received http method
267        received: http::method::Method,
268    },
269}
270
271impl<T> From<T> for FromHttpRequestError
272where
273    T: Into<DeserializationError>,
274{
275    fn from(err: T) -> Self {
276        Self::Deserialization(err.into())
277    }
278}
279
280/// An error when converting a http response to one of Ruma's endpoint-specific response types.
281#[derive(Debug)]
282#[non_exhaustive]
283pub enum FromHttpResponseError<E> {
284    /// Deserialization failed
285    Deserialization(DeserializationError),
286
287    /// The server returned a non-success status
288    Server(E),
289}
290
291impl<E> FromHttpResponseError<E> {
292    /// Map `FromHttpResponseError<E>` to `FromHttpResponseError<F>` by applying a function to a
293    /// contained `Server` value, leaving a `Deserialization` value untouched.
294    pub fn map<F>(self, f: impl FnOnce(E) -> F) -> FromHttpResponseError<F> {
295        match self {
296            Self::Deserialization(d) => FromHttpResponseError::Deserialization(d),
297            Self::Server(s) => FromHttpResponseError::Server(f(s)),
298        }
299    }
300}
301
302impl<E, F> FromHttpResponseError<Result<E, F>> {
303    /// Transpose `FromHttpResponseError<Result<E, F>>` to `Result<FromHttpResponseError<E>, F>`.
304    pub fn transpose(self) -> Result<FromHttpResponseError<E>, F> {
305        match self {
306            Self::Deserialization(d) => Ok(FromHttpResponseError::Deserialization(d)),
307            Self::Server(s) => s.map(FromHttpResponseError::Server),
308        }
309    }
310}
311
312impl<E: fmt::Display> fmt::Display for FromHttpResponseError<E> {
313    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
314        match self {
315            Self::Deserialization(err) => write!(f, "deserialization failed: {err}"),
316            Self::Server(err) => write!(f, "the server returned an error: {err}"),
317        }
318    }
319}
320
321impl<E, T> From<T> for FromHttpResponseError<E>
322where
323    T: Into<DeserializationError>,
324{
325    fn from(err: T) -> Self {
326        Self::Deserialization(err.into())
327    }
328}
329
330impl<E: StdError> StdError for FromHttpResponseError<E> {}
331
332/// Extension trait for `FromHttpResponseError<Error>`.
333pub trait FromHttpResponseErrorExt {
334    /// If `self` is a server error in the `errcode` + `error` format expected
335    /// for Matrix API endpoints, returns the error kind (`errcode`).
336    fn error_kind(&self) -> Option<&ErrorKind>;
337}
338
339impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
340    fn error_kind(&self) -> Option<&ErrorKind> {
341        as_variant!(self, Self::Server)?.error_kind()
342    }
343}
344
345/// An error when converting a http request / response to one of ruma's endpoint-specific request /
346/// response types.
347#[derive(Debug, Error)]
348#[non_exhaustive]
349pub enum DeserializationError {
350    /// Encountered invalid UTF-8.
351    #[error(transparent)]
352    Utf8(#[from] std::str::Utf8Error),
353
354    /// JSON deserialization failed.
355    #[error(transparent)]
356    Json(#[from] serde_json::Error),
357
358    /// Query parameter deserialization failed.
359    #[error(transparent)]
360    Query(#[from] serde_html_form::de::Error),
361
362    /// Got an invalid identifier.
363    #[error(transparent)]
364    Ident(#[from] crate::IdParseError),
365
366    /// Header value deserialization failed.
367    #[error(transparent)]
368    Header(#[from] HeaderDeserializationError),
369
370    /// Deserialization of `multipart/mixed` response failed.
371    #[error(transparent)]
372    MultipartMixed(#[from] MultipartMixedDeserializationError),
373}
374
375impl From<std::convert::Infallible> for DeserializationError {
376    fn from(err: std::convert::Infallible) -> Self {
377        match err {}
378    }
379}
380
381impl From<http::header::ToStrError> for DeserializationError {
382    fn from(err: http::header::ToStrError) -> Self {
383        Self::Header(HeaderDeserializationError::ToStrError(err))
384    }
385}
386
387/// An error when deserializing the HTTP headers.
388#[derive(Debug, Error)]
389#[non_exhaustive]
390pub enum HeaderDeserializationError {
391    /// Failed to convert `http::header::HeaderValue` to `str`.
392    #[error("{0}")]
393    ToStrError(#[from] http::header::ToStrError),
394
395    /// Failed to convert `http::header::HeaderValue` to an integer.
396    #[error("{0}")]
397    ParseIntError(#[from] ParseIntError),
398
399    /// Failed to parse a HTTP date from a `http::header::Value`.
400    #[error("failed to parse HTTP date")]
401    InvalidHttpDate,
402
403    /// The given required header is missing.
404    #[error("missing header `{0}`")]
405    MissingHeader(String),
406
407    /// The given header failed to parse.
408    #[error("invalid header: {0}")]
409    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
410
411    /// A header was received with a unexpected value.
412    #[error(
413        "The {header} header was received with an unexpected value, \
414         expected {expected}, received {unexpected}"
415    )]
416    InvalidHeaderValue {
417        /// The name of the header containing the invalid value.
418        header: String,
419        /// The value the header should have been set to.
420        expected: String,
421        /// The value we instead received and rejected.
422        unexpected: String,
423    },
424
425    /// The `Content-Type` header for a `multipart/mixed` response is missing the `boundary`
426    /// attribute.
427    #[error(
428        "The `Content-Type` header for a `multipart/mixed` response is missing the `boundary` attribute"
429    )]
430    MissingMultipartBoundary,
431}
432
433/// An error when deserializing a `multipart/mixed` response.
434#[derive(Debug, Error)]
435#[non_exhaustive]
436pub enum MultipartMixedDeserializationError {
437    /// There were not the number of body parts that were expected.
438    #[error(
439        "multipart/mixed response does not have enough body parts, \
440         expected {expected}, found {found}"
441    )]
442    MissingBodyParts {
443        /// The number of body parts expected in the response.
444        expected: usize,
445        /// The number of body parts found in the received response.
446        found: usize,
447    },
448
449    /// The separator between the headers and the content of a body part is missing.
450    #[error("multipart/mixed body part is missing separator between headers and content")]
451    MissingBodyPartInnerSeparator,
452
453    /// The separator between a header's name and value is missing.
454    #[error("multipart/mixed body part header is missing separator between name and value")]
455    MissingHeaderSeparator,
456
457    /// A header failed to parse.
458    #[error("invalid multipart/mixed header: {0}")]
459    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
460}
461
462/// An error that happens when Ruma cannot understand a Matrix version.
463#[derive(Debug)]
464#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
465pub struct UnknownVersionError;
466
467impl fmt::Display for UnknownVersionError {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
469        write!(f, "version string was unknown")
470    }
471}
472
473impl StdError for UnknownVersionError {}
474
475/// An error that happens when an incorrect amount of arguments have been passed to [`PathBuilder`]
476/// parts formatting.
477///
478/// [`PathBuilder`]: super::path_builder::PathBuilder
479#[derive(Debug)]
480#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
481pub struct IncorrectArgumentCount {
482    /// The expected amount of arguments.
483    pub expected: usize,
484
485    /// The amount of arguments received.
486    pub got: usize,
487}
488
489impl fmt::Display for IncorrectArgumentCount {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        write!(f, "incorrect path argument count, expected {}, got {}", self.expected, self.got)
492    }
493}
494
495impl StdError for IncorrectArgumentCount {}
496
497/// An error when serializing the HTTP headers.
498#[derive(Debug, Error)]
499#[non_exhaustive]
500pub enum HeaderSerializationError {
501    /// Failed to convert a header value to `http::header::HeaderValue`.
502    #[error(transparent)]
503    ToHeaderValue(#[from] http::header::InvalidHeaderValue),
504
505    /// The `SystemTime` could not be converted to a HTTP date.
506    ///
507    /// This only happens if the `SystemTime` provided is too far in the past (before the Unix
508    /// epoch) or the future (after the year 9999).
509    #[error("invalid HTTP date")]
510    InvalidHttpDate,
511}