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