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 bytes::{BufMut, Bytes};
8use serde_json::{from_slice as from_json_slice, Value as JsonValue};
9use thiserror::Error;
10
11use super::{EndpointError, MatrixVersion, OutgoingResponse};
12
13/// A general-purpose Matrix error type consisting of an HTTP status code and a JSON body.
14///
15/// Note that individual `ruma-*-api` crates may provide more specific error types.
16#[allow(clippy::exhaustive_structs)]
17#[derive(Clone, Debug)]
18pub struct MatrixError {
19    /// The http response's status code.
20    pub status_code: http::StatusCode,
21
22    /// The http response's body.
23    pub body: MatrixErrorBody,
24}
25
26impl fmt::Display for MatrixError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        let status_code = self.status_code.as_u16();
29        match &self.body {
30            MatrixErrorBody::Json(json) => write!(f, "[{status_code}] {json}"),
31            MatrixErrorBody::NotJson { .. } => write!(f, "[{status_code}] <non-json bytes>"),
32        }
33    }
34}
35
36impl StdError for MatrixError {}
37
38impl OutgoingResponse for MatrixError {
39    fn try_into_http_response<T: Default + BufMut>(
40        self,
41    ) -> Result<http::Response<T>, IntoHttpError> {
42        http::Response::builder()
43            .header(http::header::CONTENT_TYPE, "application/json")
44            .status(self.status_code)
45            .body(match self.body {
46                MatrixErrorBody::Json(json) => crate::serde::json_to_buf(&json)?,
47                MatrixErrorBody::NotJson { .. } => {
48                    return Err(IntoHttpError::Json(serde::ser::Error::custom(
49                        "attempted to serialize MatrixErrorBody::NotJson",
50                    )));
51                }
52            })
53            .map_err(Into::into)
54    }
55}
56
57impl EndpointError for MatrixError {
58    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self {
59        let status_code = response.status();
60        let body = MatrixErrorBody::from_bytes(response.body().as_ref());
61        Self { status_code, body }
62    }
63}
64
65/// The body of an error response.
66#[derive(Clone, Debug)]
67#[allow(clippy::exhaustive_enums)]
68pub enum MatrixErrorBody {
69    /// A JSON body, as intended.
70    Json(JsonValue),
71
72    /// A response body that is not valid JSON.
73    NotJson {
74        /// The raw bytes of the response body.
75        bytes: Bytes,
76
77        /// The error from trying to deserialize the bytes as JSON.
78        deserialization_error: Arc<serde_json::Error>,
79    },
80}
81
82impl MatrixErrorBody {
83    /// Create a `MatrixErrorBody` from the given HTTP body bytes.
84    pub fn from_bytes(body_bytes: &[u8]) -> Self {
85        match from_json_slice(body_bytes) {
86            Ok(json) => MatrixErrorBody::Json(json),
87            Err(e) => MatrixErrorBody::NotJson {
88                bytes: Bytes::copy_from_slice(body_bytes),
89                deserialization_error: Arc::new(e),
90            },
91        }
92    }
93}
94
95/// An error when converting one of ruma's endpoint-specific request or response
96/// types to the corresponding http type.
97#[derive(Debug, Error)]
98#[non_exhaustive]
99pub enum IntoHttpError {
100    /// Tried to create an authentication request without an access token.
101    #[error("no access token given, but this endpoint requires one")]
102    NeedsAuthentication,
103
104    /// Tried to create a request with an old enough version, for which no unstable endpoint
105    /// exists.
106    ///
107    /// This is also a fallback error for if the version is too new for this endpoint.
108    #[error(
109        "endpoint was not supported by server-reported versions, \
110         but no unstable path to fall back to was defined"
111    )]
112    NoUnstablePath,
113
114    /// Tried to create a request with [`MatrixVersion`]s for all of which this endpoint was
115    /// removed.
116    #[error("could not create any path variant for endpoint, as it was removed in version {0}")]
117    EndpointRemoved(MatrixVersion),
118
119    /// JSON serialization failed.
120    #[error("JSON serialization failed: {0}")]
121    Json(#[from] serde_json::Error),
122
123    /// Query parameter serialization failed.
124    #[error("query parameter serialization failed: {0}")]
125    Query(#[from] serde_html_form::ser::Error),
126
127    /// Header serialization failed.
128    #[error("header serialization failed: {0}")]
129    Header(#[from] HeaderSerializationError),
130
131    /// HTTP request construction failed.
132    #[error("HTTP request construction failed: {0}")]
133    Http(#[from] http::Error),
134}
135
136impl From<http::header::InvalidHeaderValue> for IntoHttpError {
137    fn from(value: http::header::InvalidHeaderValue) -> Self {
138        Self::Header(value.into())
139    }
140}
141
142/// An error when converting a http request to one of ruma's endpoint-specific request types.
143#[derive(Debug, Error)]
144#[non_exhaustive]
145pub enum FromHttpRequestError {
146    /// Deserialization failed
147    #[error("deserialization failed: {0}")]
148    Deserialization(DeserializationError),
149
150    /// HTTP method mismatch
151    #[error("http method mismatch: expected {expected}, received: {received}")]
152    MethodMismatch {
153        /// expected http method
154        expected: http::method::Method,
155        /// received http method
156        received: http::method::Method,
157    },
158}
159
160impl<T> From<T> for FromHttpRequestError
161where
162    T: Into<DeserializationError>,
163{
164    fn from(err: T) -> Self {
165        Self::Deserialization(err.into())
166    }
167}
168
169/// An error when converting a http response to one of Ruma's endpoint-specific response types.
170#[derive(Debug)]
171#[non_exhaustive]
172pub enum FromHttpResponseError<E> {
173    /// Deserialization failed
174    Deserialization(DeserializationError),
175
176    /// The server returned a non-success status
177    Server(E),
178}
179
180impl<E> FromHttpResponseError<E> {
181    /// Map `FromHttpResponseError<E>` to `FromHttpResponseError<F>` by applying a function to a
182    /// contained `Server` value, leaving a `Deserialization` value untouched.
183    pub fn map<F>(self, f: impl FnOnce(E) -> F) -> FromHttpResponseError<F> {
184        match self {
185            Self::Deserialization(d) => FromHttpResponseError::Deserialization(d),
186            Self::Server(s) => FromHttpResponseError::Server(f(s)),
187        }
188    }
189}
190
191impl<E, F> FromHttpResponseError<Result<E, F>> {
192    /// Transpose `FromHttpResponseError<Result<E, F>>` to `Result<FromHttpResponseError<E>, F>`.
193    pub fn transpose(self) -> Result<FromHttpResponseError<E>, F> {
194        match self {
195            Self::Deserialization(d) => Ok(FromHttpResponseError::Deserialization(d)),
196            Self::Server(s) => s.map(FromHttpResponseError::Server),
197        }
198    }
199}
200
201impl<E: fmt::Display> fmt::Display for FromHttpResponseError<E> {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            Self::Deserialization(err) => write!(f, "deserialization failed: {err}"),
205            Self::Server(err) => write!(f, "the server returned an error: {err}"),
206        }
207    }
208}
209
210impl<E, T> From<T> for FromHttpResponseError<E>
211where
212    T: Into<DeserializationError>,
213{
214    fn from(err: T) -> Self {
215        Self::Deserialization(err.into())
216    }
217}
218
219impl<E: StdError> StdError for FromHttpResponseError<E> {}
220
221/// An error when converting a http request / response to one of ruma's endpoint-specific request /
222/// response types.
223#[derive(Debug, Error)]
224#[non_exhaustive]
225pub enum DeserializationError {
226    /// Encountered invalid UTF-8.
227    #[error(transparent)]
228    Utf8(#[from] std::str::Utf8Error),
229
230    /// JSON deserialization failed.
231    #[error(transparent)]
232    Json(#[from] serde_json::Error),
233
234    /// Query parameter deserialization failed.
235    #[error(transparent)]
236    Query(#[from] serde_html_form::de::Error),
237
238    /// Got an invalid identifier.
239    #[error(transparent)]
240    Ident(#[from] crate::IdParseError),
241
242    /// Header value deserialization failed.
243    #[error(transparent)]
244    Header(#[from] HeaderDeserializationError),
245
246    /// Deserialization of `multipart/mixed` response failed.
247    #[error(transparent)]
248    MultipartMixed(#[from] MultipartMixedDeserializationError),
249}
250
251impl From<std::convert::Infallible> for DeserializationError {
252    fn from(err: std::convert::Infallible) -> Self {
253        match err {}
254    }
255}
256
257impl From<http::header::ToStrError> for DeserializationError {
258    fn from(err: http::header::ToStrError) -> Self {
259        Self::Header(HeaderDeserializationError::ToStrError(err))
260    }
261}
262
263/// An error when deserializing the HTTP headers.
264#[derive(Debug, Error)]
265#[non_exhaustive]
266pub enum HeaderDeserializationError {
267    /// Failed to convert `http::header::HeaderValue` to `str`.
268    #[error("{0}")]
269    ToStrError(#[from] http::header::ToStrError),
270
271    /// Failed to convert `http::header::HeaderValue` to an integer.
272    #[error("{0}")]
273    ParseIntError(#[from] ParseIntError),
274
275    /// Failed to parse a HTTP date from a `http::header::Value`.
276    #[error("failed to parse HTTP date")]
277    InvalidHttpDate,
278
279    /// The given required header is missing.
280    #[error("missing header `{0}`")]
281    MissingHeader(String),
282
283    /// The given header failed to parse.
284    #[error("invalid header: {0}")]
285    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
286
287    /// A header was received with a unexpected value.
288    #[error(
289        "The {header} header was received with an unexpected value, \
290         expected {expected}, received {unexpected}"
291    )]
292    InvalidHeaderValue {
293        /// The name of the header containing the invalid value.
294        header: String,
295        /// The value the header should have been set to.
296        expected: String,
297        /// The value we instead received and rejected.
298        unexpected: String,
299    },
300
301    /// The `Content-Type` header for a `multipart/mixed` response is missing the `boundary`
302    /// attribute.
303    #[error(
304        "The `Content-Type` header for a `multipart/mixed` response is missing the `boundary` attribute"
305    )]
306    MissingMultipartBoundary,
307}
308
309/// An error when deserializing a `multipart/mixed` response.
310#[derive(Debug, Error)]
311#[non_exhaustive]
312pub enum MultipartMixedDeserializationError {
313    /// There were not the number of body parts that were expected.
314    #[error(
315        "multipart/mixed response does not have enough body parts, \
316         expected {expected}, found {found}"
317    )]
318    MissingBodyParts {
319        /// The number of body parts expected in the response.
320        expected: usize,
321        /// The number of body parts found in the received response.
322        found: usize,
323    },
324
325    /// The separator between the headers and the content of a body part is missing.
326    #[error("multipart/mixed body part is missing separator between headers and content")]
327    MissingBodyPartInnerSeparator,
328
329    /// The separator between a header's name and value is missing.
330    #[error("multipart/mixed body part header is missing separator between name and value")]
331    MissingHeaderSeparator,
332
333    /// A header failed to parse.
334    #[error("invalid multipart/mixed header: {0}")]
335    InvalidHeader(Box<dyn std::error::Error + Send + Sync + 'static>),
336}
337
338/// An error that happens when Ruma cannot understand a Matrix version.
339#[derive(Debug)]
340#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
341pub struct UnknownVersionError;
342
343impl fmt::Display for UnknownVersionError {
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        write!(f, "version string was unknown")
346    }
347}
348
349impl StdError for UnknownVersionError {}
350
351/// An error that happens when an incorrect amount of arguments have been passed to PathData parts
352/// formatting.
353#[derive(Debug)]
354#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
355pub struct IncorrectArgumentCount {
356    /// The expected amount of arguments.
357    pub expected: usize,
358
359    /// The amount of arguments received.
360    pub got: usize,
361}
362
363impl fmt::Display for IncorrectArgumentCount {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        write!(f, "incorrect path argument count, expected {}, got {}", self.expected, self.got)
366    }
367}
368
369impl StdError for IncorrectArgumentCount {}
370
371/// An error when serializing the HTTP headers.
372#[derive(Debug, Error)]
373#[non_exhaustive]
374pub enum HeaderSerializationError {
375    /// Failed to convert a header value to `http::header::HeaderValue`.
376    #[error(transparent)]
377    ToHeaderValue(#[from] http::header::InvalidHeaderValue),
378
379    /// The `SystemTime` could not be converted to a HTTP date.
380    ///
381    /// This only happens if the `SystemTime` provided is too far in the past (before the Unix
382    /// epoch) or the future (after the year 9999).
383    #[error("invalid HTTP date")]
384    InvalidHttpDate,
385}