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