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(response: http::Response<&[u8]>) -> Self {
111        let status = response.status();
112
113        let body_bytes = response.body();
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<std::convert::Infallible> for IntoHttpError {
248    fn from(value: std::convert::Infallible) -> Self {
249        match value {}
250    }
251}
252
253impl From<http::header::InvalidHeaderValue> for IntoHttpError {
254    fn from(value: http::header::InvalidHeaderValue) -> Self {
255        Self::Header(value.into())
256    }
257}
258
259/// An error when converting a http request to one of ruma's endpoint-specific request types.
260#[derive(Debug, Error)]
261#[non_exhaustive]
262pub enum FromHttpRequestError {
263    /// Deserialization failed
264    #[error("deserialization failed: {0}")]
265    Deserialization(DeserializationError),
266
267    /// HTTP method mismatch
268    #[error("http method mismatch: expected {expected}, received: {received}")]
269    MethodMismatch {
270        /// expected http method
271        expected: http::method::Method,
272        /// received http method
273        received: http::method::Method,
274    },
275}
276
277impl<T> From<T> for FromHttpRequestError
278where
279    T: Into<DeserializationError>,
280{
281    fn from(err: T) -> Self {
282        Self::Deserialization(err.into())
283    }
284}
285
286/// An error when converting a http response to one of Ruma's endpoint-specific response types.
287#[derive(Debug)]
288#[non_exhaustive]
289pub enum FromHttpResponseError<E> {
290    /// Deserialization failed
291    Deserialization(DeserializationError),
292
293    /// The server returned a non-success status
294    Server(E),
295}
296
297impl<E> FromHttpResponseError<E> {
298    /// Map `FromHttpResponseError<E>` to `FromHttpResponseError<F>` by applying a function to a
299    /// contained `Server` value, leaving a `Deserialization` value untouched.
300    pub fn map<F>(self, f: impl FnOnce(E) -> F) -> FromHttpResponseError<F> {
301        match self {
302            Self::Deserialization(d) => FromHttpResponseError::Deserialization(d),
303            Self::Server(s) => FromHttpResponseError::Server(f(s)),
304        }
305    }
306}
307
308impl<E, F> FromHttpResponseError<Result<E, F>> {
309    /// Transpose `FromHttpResponseError<Result<E, F>>` to `Result<FromHttpResponseError<E>, F>`.
310    pub fn transpose(self) -> Result<FromHttpResponseError<E>, F> {
311        match self {
312            Self::Deserialization(d) => Ok(FromHttpResponseError::Deserialization(d)),
313            Self::Server(s) => s.map(FromHttpResponseError::Server),
314        }
315    }
316}
317
318impl<E: fmt::Display> fmt::Display for FromHttpResponseError<E> {
319    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
320        match self {
321            Self::Deserialization(err) => write!(f, "deserialization failed: {err}"),
322            Self::Server(err) => write!(f, "the server returned an error: {err}"),
323        }
324    }
325}
326
327impl<E, T> From<T> for FromHttpResponseError<E>
328where
329    T: Into<DeserializationError>,
330{
331    fn from(err: T) -> Self {
332        Self::Deserialization(err.into())
333    }
334}
335
336impl<E: StdError> StdError for FromHttpResponseError<E> {}
337
338/// Extension trait for `FromHttpResponseError<Error>`.
339pub trait FromHttpResponseErrorExt {
340    /// If `self` is a server error in the `errcode` + `error` format expected
341    /// for Matrix API endpoints, returns the error kind (`errcode`).
342    fn error_kind(&self) -> Option<&ErrorKind>;
343}
344
345impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
346    fn error_kind(&self) -> Option<&ErrorKind> {
347        as_variant!(self, Self::Server)?.error_kind()
348    }
349}
350
351/// An error when converting a http request / response to one of ruma's endpoint-specific request /
352/// response types.
353#[derive(Debug, Error)]
354#[non_exhaustive]
355pub enum DeserializationError {
356    /// Encountered invalid UTF-8.
357    #[error(transparent)]
358    Utf8(#[from] std::str::Utf8Error),
359
360    /// JSON deserialization failed.
361    #[error(transparent)]
362    Json(#[from] serde_json::Error),
363
364    /// Query parameter deserialization failed.
365    #[error(transparent)]
366    Query(#[from] serde_html_form::de::Error),
367
368    /// Got an invalid identifier.
369    #[error(transparent)]
370    Ident(#[from] crate::IdParseError),
371
372    /// Header value deserialization failed.
373    #[error(transparent)]
374    Header(#[from] HeaderDeserializationError),
375
376    /// Deserialization of `multipart/mixed` response failed.
377    #[error(transparent)]
378    MultipartMixed(#[from] MultipartMixedDeserializationError),
379}
380
381impl From<std::convert::Infallible> for DeserializationError {
382    fn from(err: std::convert::Infallible) -> Self {
383        match err {}
384    }
385}
386
387impl From<http::header::ToStrError> for DeserializationError {
388    fn from(err: http::header::ToStrError) -> Self {
389        Self::Header(HeaderDeserializationError::ToStrError(err))
390    }
391}
392
393/// An error when deserializing the HTTP headers.
394#[derive(Debug, Error)]
395#[non_exhaustive]
396pub enum HeaderDeserializationError {
397    /// Failed to convert `http::header::HeaderValue` to `str`.
398    #[error("{0}")]
399    ToStrError(#[from] http::header::ToStrError),
400
401    /// Failed to convert `http::header::HeaderValue` to an integer.
402    #[error("{0}")]
403    ParseIntError(#[from] ParseIntError),
404
405    /// Failed to parse a HTTP date from a `http::header::Value`.
406    #[error("failed to parse HTTP date")]
407    InvalidHttpDate,
408
409    /// The given required header is missing.
410    #[error("missing header `{0}`")]
411    MissingHeader(String),
412
413    /// The given header failed to parse.
414    #[error("invalid header: {0}")]
415    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
416
417    /// A header was received with a unexpected value.
418    #[error(
419        "The {header} header was received with an unexpected value, \
420         expected {expected}, received {unexpected}"
421    )]
422    InvalidHeaderValue {
423        /// The name of the header containing the invalid value.
424        header: String,
425        /// The value the header should have been set to.
426        expected: String,
427        /// The value we instead received and rejected.
428        unexpected: String,
429    },
430
431    /// The `Content-Type` header for a `multipart/mixed` response is missing the `boundary`
432    /// attribute.
433    #[error(
434        "The `Content-Type` header for a `multipart/mixed` response is missing the `boundary` attribute"
435    )]
436    MissingMultipartBoundary,
437}
438
439/// An error when deserializing a `multipart/mixed` response.
440#[derive(Debug, Error)]
441#[non_exhaustive]
442pub enum MultipartMixedDeserializationError {
443    /// There were not the number of body parts that were expected.
444    #[error(
445        "multipart/mixed response does not have enough body parts, \
446         expected {expected}, found {found}"
447    )]
448    MissingBodyParts {
449        /// The number of body parts expected in the response.
450        expected: usize,
451        /// The number of body parts found in the received response.
452        found: usize,
453    },
454
455    /// The separator between the headers and the content of a body part is missing.
456    #[error("multipart/mixed body part is missing separator between headers and content")]
457    MissingBodyPartInnerSeparator,
458
459    /// The separator between a header's name and value is missing.
460    #[error("multipart/mixed body part header is missing separator between name and value")]
461    MissingHeaderSeparator,
462
463    /// A header failed to parse.
464    #[error("invalid multipart/mixed header: {0}")]
465    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
466}
467
468/// An error that happens when Ruma cannot understand a Matrix version.
469#[derive(Debug)]
470#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
471pub struct UnknownVersionError;
472
473impl fmt::Display for UnknownVersionError {
474    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475        write!(f, "version string was unknown")
476    }
477}
478
479impl StdError for UnknownVersionError {}
480
481/// An error that happens when an incorrect amount of arguments have been passed to [`PathBuilder`]
482/// parts formatting.
483///
484/// [`PathBuilder`]: super::path_builder::PathBuilder
485#[derive(Debug)]
486#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
487pub struct IncorrectArgumentCount {
488    /// The expected amount of arguments.
489    pub expected: usize,
490
491    /// The amount of arguments received.
492    pub got: usize,
493}
494
495impl fmt::Display for IncorrectArgumentCount {
496    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
497        write!(f, "incorrect path argument count, expected {}, got {}", self.expected, self.got)
498    }
499}
500
501impl StdError for IncorrectArgumentCount {}
502
503/// An error when serializing the HTTP headers.
504#[derive(Debug, Error)]
505#[non_exhaustive]
506pub enum HeaderSerializationError {
507    /// Failed to convert a header value to `http::header::HeaderValue`.
508    #[error(transparent)]
509    ToHeaderValue(#[from] http::header::InvalidHeaderValue),
510
511    /// The `SystemTime` could not be converted to a HTTP date.
512    ///
513    /// This only happens if the `SystemTime` provided is too far in the past (before the Unix
514    /// epoch) or the future (after the year 9999).
515    #[error("invalid HTTP date")]
516    InvalidHttpDate,
517}