Skip to main content

ruma_client_api/
error.rs

1//! Errors that can be sent from the homeserver.
2
3use std::{fmt, str::FromStr, sync::Arc};
4
5use as_variant::as_variant;
6use bytes::{BufMut, Bytes};
7use ruma_common::{
8    RoomVersionId,
9    api::{
10        EndpointError, OutgoingResponse,
11        error::{
12            FromHttpResponseError, HeaderDeserializationError, HeaderSerializationError,
13            IntoHttpError, MatrixErrorBody,
14        },
15    },
16    serde::{JsonObject, StringEnum},
17};
18use serde::{Deserialize, Serialize};
19use serde_json::{Value as JsonValue, from_slice as from_json_slice};
20use web_time::{Duration, SystemTime};
21
22use crate::{
23    PrivOwnedStr,
24    http_headers::{http_date_to_system_time, system_time_to_http_date},
25};
26
27/// Deserialize and Serialize implementations for ErrorKind.
28/// Separate module because it's a lot of code.
29mod kind_serde;
30
31/// An enum for the error kind.
32///
33/// Items may contain additional information.
34#[derive(Clone, Debug, PartialEq, Eq)]
35#[non_exhaustive]
36// Please keep the variants sorted alphabetically.
37pub enum ErrorKind {
38    /// `M_APPSERVICE_LOGIN_UNSUPPORTED`
39    ///
40    /// An application service used the [`m.login.application_service`] type an endpoint from the
41    /// [legacy authentication API] in a way that is not supported by the homeserver, because the
42    /// server only supports the [OAuth 2.0 API].
43    ///
44    /// [`m.login.application_service`]: https://spec.matrix.org/latest/application-service-api/#server-admin-style-permissions
45    /// [legacy authentication API]: https://spec.matrix.org/latest/client-server-api/#legacy-api
46    /// [OAuth 2.0 API]: https://spec.matrix.org/latest/client-server-api/#oauth-20-api
47    AppserviceLoginUnsupported,
48
49    /// `M_BAD_ALIAS`
50    ///
51    /// One or more [room aliases] within the `m.room.canonical_alias` event do not point to the
52    /// room ID for which the state event is to be sent to.
53    ///
54    /// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
55    BadAlias,
56
57    /// `M_BAD_JSON`
58    ///
59    /// The request contained valid JSON, but it was malformed in some way, e.g. missing required
60    /// keys, invalid values for keys.
61    BadJson,
62
63    /// `M_BAD_STATE`
64    ///
65    /// The state change requested cannot be performed, such as attempting to unban a user who is
66    /// not banned.
67    BadState,
68
69    /// `M_BAD_STATUS`
70    ///
71    /// The application service returned a bad status.
72    BadStatus(BadStatusErrorData),
73
74    /// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
75    ///
76    /// The user is unable to reject an invite to join the [server notices] room.
77    ///
78    /// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
79    CannotLeaveServerNoticeRoom,
80
81    /// `M_CANNOT_OVERWRITE_MEDIA`
82    ///
83    /// The [`create_content_async`] endpoint was called with a media ID that already has content.
84    ///
85    /// [`create_content_async`]: crate::media::create_content_async
86    CannotOverwriteMedia,
87
88    /// `M_CAPTCHA_INVALID`
89    ///
90    /// The Captcha provided did not match what was expected.
91    CaptchaInvalid,
92
93    /// `M_CAPTCHA_NEEDED`
94    ///
95    /// A Captcha is required to complete the request.
96    CaptchaNeeded,
97
98    /// `M_CONFLICTING_UNSUBSCRIPTION`
99    ///
100    /// Part of [MSC4306]: an automatic thread subscription has been skipped by the server, because
101    /// the user unsubsubscribed after the indicated subscribed-to event.
102    ///
103    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
104    #[cfg(feature = "unstable-msc4306")]
105    ConflictingUnsubscription,
106
107    /// `M_CONNECTION_FAILED`
108    ///
109    /// The connection to the application service failed.
110    ConnectionFailed,
111
112    /// `M_CONNECTION_TIMEOUT`
113    ///
114    /// The connection to the application service timed out.
115    ConnectionTimeout,
116
117    /// `M_DUPLICATE_ANNOTATION`
118    ///
119    /// The request is an attempt to send a [duplicate annotation].
120    ///
121    /// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
122    DuplicateAnnotation,
123
124    /// `M_EXCLUSIVE`
125    ///
126    /// The resource being requested is reserved by an application service, or the application
127    /// service making the request has not created the resource.
128    Exclusive,
129
130    /// `M_FORBIDDEN`
131    ///
132    /// Forbidden access, e.g. joining a room without permission, failed login.
133    Forbidden,
134
135    /// `M_GUEST_ACCESS_FORBIDDEN`
136    ///
137    /// The room or resource does not permit [guests] to access it.
138    ///
139    /// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
140    GuestAccessForbidden,
141
142    /// `M_INCOMPATIBLE_ROOM_VERSION`
143    ///
144    /// The client attempted to join a room that has a version the server does not support.
145    IncompatibleRoomVersion(IncompatibleRoomVersionErrorData),
146
147    /// `M_INVALID_PARAM`
148    ///
149    /// A parameter that was specified has the wrong value. For example, the server expected an
150    /// integer and instead received a string.
151    InvalidParam,
152
153    /// `M_INVALID_ROOM_STATE`
154    ///
155    /// The initial state implied by the parameters to the [`create_room`] request is invalid, e.g.
156    /// the user's `power_level` is set below that necessary to set the room name.
157    ///
158    /// [`create_room`]: crate::room::create_room
159    InvalidRoomState,
160
161    /// `M_INVALID_USERNAME`
162    ///
163    /// The desired user name is not valid.
164    InvalidUsername,
165
166    /// `M_INVITE_BLOCKED`
167    ///
168    /// The invite was interdicted by moderation tools or configured access controls without having
169    /// been witnessed by the invitee.
170    InviteBlocked,
171
172    /// `M_LIMIT_EXCEEDED`
173    ///
174    /// The request has been refused due to [rate limiting]: too many requests have been sent in a
175    /// short period of time.
176    ///
177    /// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
178    LimitExceeded(LimitExceededErrorData),
179
180    /// `M_MISSING_PARAM`
181    ///
182    /// A required parameter was missing from the request.
183    MissingParam,
184
185    /// `M_MISSING_TOKEN`
186    ///
187    /// No [access token] was specified for the request, but one is required.
188    ///
189    /// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
190    MissingToken,
191
192    /// `M_NOT_FOUND`
193    ///
194    /// No resource was found for this request.
195    NotFound,
196
197    /// `M_NOT_IN_THREAD`
198    ///
199    /// Part of [MSC4306]: an automatic thread subscription was set to an event ID that isn't part
200    /// of the subscribed-to thread.
201    ///
202    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
203    #[cfg(feature = "unstable-msc4306")]
204    NotInThread,
205
206    /// `M_NOT_JSON`
207    ///
208    /// The request did not contain valid JSON.
209    NotJson,
210
211    /// `M_NOT_YET_UPLOADED`
212    ///
213    /// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used and the content is
214    /// not yet available.
215    ///
216    /// [`create_mxc_uri`]: crate::media::create_mxc_uri
217    NotYetUploaded,
218
219    /// `M_RESOURCE_LIMIT_EXCEEDED`
220    ///
221    /// The request cannot be completed because the homeserver has reached a resource limit imposed
222    /// on it. For example, a homeserver held in a shared hosting environment may reach a resource
223    /// limit if it starts using too much memory or disk space.
224    ResourceLimitExceeded(ResourceLimitExceededErrorData),
225
226    /// `M_ROOM_IN_USE`
227    ///
228    /// The [room alias] specified in the [`create_room`] request is already taken.
229    ///
230    /// [`create_room`]: crate::room::create_room
231    /// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
232    RoomInUse,
233
234    /// `M_SERVER_NOT_TRUSTED`
235    ///
236    /// The client's request used a third-party server, e.g. identity server, that this server does
237    /// not trust.
238    ServerNotTrusted,
239
240    /// `M_THREEPID_AUTH_FAILED`
241    ///
242    /// Authentication could not be performed on the [third-party identifier].
243    ///
244    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
245    ThreepidAuthFailed,
246
247    /// `M_THREEPID_DENIED`
248    ///
249    /// The server does not permit this [third-party identifier]. This may happen if the server
250    /// only permits, for example, email addresses from a particular domain.
251    ///
252    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
253    ThreepidDenied,
254
255    /// `M_THREEPID_IN_USE`
256    ///
257    /// The [third-party identifier] is already in use by another user.
258    ///
259    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
260    ThreepidInUse,
261
262    /// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
263    ///
264    /// The homeserver does not support adding a [third-party identifier] of the given medium.
265    ///
266    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
267    ThreepidMediumNotSupported,
268
269    /// `M_THREEPID_NOT_FOUND`
270    ///
271    /// No account matching the given [third-party identifier] could be found.
272    ///
273    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
274    ThreepidNotFound,
275
276    /// `M_TOKEN_INCORRECT`
277    ///
278    /// The token that the user entered to validate the session is incorrect.
279    TokenIncorrect,
280
281    /// `M_TOO_LARGE`
282    ///
283    /// The request or entity was too large.
284    TooLarge,
285
286    /// `M_UNABLE_TO_AUTHORISE_JOIN`
287    ///
288    /// The room is [restricted] and none of the conditions can be validated by the homeserver.
289    /// This can happen if the homeserver does not know about any of the rooms listed as
290    /// conditions, for example.
291    ///
292    /// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
293    UnableToAuthorizeJoin,
294
295    /// `M_UNABLE_TO_GRANT_JOIN`
296    ///
297    /// A different server should be attempted for the join. This is typically because the resident
298    /// server can see that the joining user satisfies one or more conditions, such as in the case
299    /// of [restricted rooms], but the resident server would be unable to meet the authorization
300    /// rules.
301    ///
302    /// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
303    UnableToGrantJoin,
304
305    /// `M_UNACTIONABLE`
306    ///
307    /// The server does not want to handle the [federated report].
308    ///
309    /// [federated report]: https://github.com/matrix-org/matrix-spec-proposals/pull/3843
310    #[cfg(feature = "unstable-msc3843")]
311    Unactionable,
312
313    /// `M_UNAUTHORIZED`
314    ///
315    /// The request was not correctly authorized. Usually due to login failures.
316    Unauthorized,
317
318    /// `M_UNKNOWN`
319    ///
320    /// An unknown error has occurred.
321    Unknown,
322
323    /// `M_UNKNOWN_POS`
324    ///
325    /// The sliding sync ([MSC4186]) connection was expired by the server.
326    ///
327    /// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
328    #[cfg(feature = "unstable-msc4186")]
329    UnknownPos,
330
331    /// `M_UNKNOWN_TOKEN`
332    ///
333    /// The [access or refresh token] specified was not recognized.
334    ///
335    /// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
336    UnknownToken(UnknownTokenErrorData),
337
338    /// `M_UNRECOGNIZED`
339    ///
340    /// The server did not understand the request.
341    ///
342    /// This is expected to be returned with a 404 HTTP status code if the endpoint is not
343    /// implemented or a 405 HTTP status code if the endpoint is implemented, but the incorrect
344    /// HTTP method is used.
345    Unrecognized,
346
347    /// `M_UNSUPPORTED_ROOM_VERSION`
348    ///
349    /// The request to [`create_room`] used a room version that the server does not support.
350    ///
351    /// [`create_room`]: crate::room::create_room
352    UnsupportedRoomVersion,
353
354    /// `M_URL_NOT_SET`
355    ///
356    /// The application service doesn't have a URL configured.
357    UrlNotSet,
358
359    /// `M_USER_DEACTIVATED`
360    ///
361    /// The user ID associated with the request has been deactivated.
362    UserDeactivated,
363
364    /// `M_USER_IN_USE`
365    ///
366    /// The desired user ID is already taken.
367    UserInUse,
368
369    /// `M_USER_LOCKED`
370    ///
371    /// The account has been [locked] and cannot be used at this time.
372    ///
373    /// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
374    UserLocked,
375
376    /// `M_USER_SUSPENDED`
377    ///
378    /// The account has been [suspended] and can only be used for limited actions at this time.
379    ///
380    /// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
381    UserSuspended,
382
383    /// `M_WEAK_PASSWORD`
384    ///
385    /// The password was [rejected] by the server for being too weak.
386    ///
387    /// [rejected]: https://spec.matrix.org/latest/client-server-api/#password-management
388    WeakPassword,
389
390    /// `M_WRONG_ROOM_KEYS_VERSION`
391    ///
392    /// The version of the [room keys backup] provided in the request does not match the current
393    /// backup version.
394    ///
395    /// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
396    WrongRoomKeysVersion(WrongRoomKeysVersionErrorData),
397
398    #[doc(hidden)]
399    _Custom(CustomErrorKind),
400}
401
402impl ErrorKind {
403    /// Get the [`ErrorCode`] for this `ErrorKind`.
404    pub fn errcode(&self) -> ErrorCode {
405        match self {
406            ErrorKind::AppserviceLoginUnsupported => ErrorCode::AppserviceLoginUnsupported,
407            ErrorKind::BadAlias => ErrorCode::BadAlias,
408            ErrorKind::BadJson => ErrorCode::BadJson,
409            ErrorKind::BadState => ErrorCode::BadState,
410            ErrorKind::BadStatus(_) => ErrorCode::BadStatus,
411            ErrorKind::CannotLeaveServerNoticeRoom => ErrorCode::CannotLeaveServerNoticeRoom,
412            ErrorKind::CannotOverwriteMedia => ErrorCode::CannotOverwriteMedia,
413            ErrorKind::CaptchaInvalid => ErrorCode::CaptchaInvalid,
414            ErrorKind::CaptchaNeeded => ErrorCode::CaptchaNeeded,
415            #[cfg(feature = "unstable-msc4306")]
416            ErrorKind::ConflictingUnsubscription => ErrorCode::ConflictingUnsubscription,
417            ErrorKind::ConnectionFailed => ErrorCode::ConnectionFailed,
418            ErrorKind::ConnectionTimeout => ErrorCode::ConnectionTimeout,
419            ErrorKind::DuplicateAnnotation => ErrorCode::DuplicateAnnotation,
420            ErrorKind::Exclusive => ErrorCode::Exclusive,
421            ErrorKind::Forbidden => ErrorCode::Forbidden,
422            ErrorKind::GuestAccessForbidden => ErrorCode::GuestAccessForbidden,
423            ErrorKind::IncompatibleRoomVersion(_) => ErrorCode::IncompatibleRoomVersion,
424            ErrorKind::InvalidParam => ErrorCode::InvalidParam,
425            ErrorKind::InvalidRoomState => ErrorCode::InvalidRoomState,
426            ErrorKind::InvalidUsername => ErrorCode::InvalidUsername,
427            ErrorKind::InviteBlocked => ErrorCode::InviteBlocked,
428            ErrorKind::LimitExceeded(_) => ErrorCode::LimitExceeded,
429            ErrorKind::MissingParam => ErrorCode::MissingParam,
430            ErrorKind::MissingToken => ErrorCode::MissingToken,
431            ErrorKind::NotFound => ErrorCode::NotFound,
432            #[cfg(feature = "unstable-msc4306")]
433            ErrorKind::NotInThread => ErrorCode::NotInThread,
434            ErrorKind::NotJson => ErrorCode::NotJson,
435            ErrorKind::NotYetUploaded => ErrorCode::NotYetUploaded,
436            ErrorKind::ResourceLimitExceeded(_) => ErrorCode::ResourceLimitExceeded,
437            ErrorKind::RoomInUse => ErrorCode::RoomInUse,
438            ErrorKind::ServerNotTrusted => ErrorCode::ServerNotTrusted,
439            ErrorKind::ThreepidAuthFailed => ErrorCode::ThreepidAuthFailed,
440            ErrorKind::ThreepidDenied => ErrorCode::ThreepidDenied,
441            ErrorKind::ThreepidInUse => ErrorCode::ThreepidInUse,
442            ErrorKind::ThreepidMediumNotSupported => ErrorCode::ThreepidMediumNotSupported,
443            ErrorKind::ThreepidNotFound => ErrorCode::ThreepidNotFound,
444            ErrorKind::TokenIncorrect => ErrorCode::TokenIncorrect,
445            ErrorKind::TooLarge => ErrorCode::TooLarge,
446            ErrorKind::UnableToAuthorizeJoin => ErrorCode::UnableToAuthorizeJoin,
447            ErrorKind::UnableToGrantJoin => ErrorCode::UnableToGrantJoin,
448            #[cfg(feature = "unstable-msc3843")]
449            ErrorKind::Unactionable => ErrorCode::Unactionable,
450            ErrorKind::Unauthorized => ErrorCode::Unauthorized,
451            ErrorKind::Unknown => ErrorCode::Unknown,
452            #[cfg(feature = "unstable-msc4186")]
453            ErrorKind::UnknownPos => ErrorCode::UnknownPos,
454            ErrorKind::UnknownToken(_) => ErrorCode::UnknownToken,
455            ErrorKind::Unrecognized => ErrorCode::Unrecognized,
456            ErrorKind::UnsupportedRoomVersion => ErrorCode::UnsupportedRoomVersion,
457            ErrorKind::UrlNotSet => ErrorCode::UrlNotSet,
458            ErrorKind::UserDeactivated => ErrorCode::UserDeactivated,
459            ErrorKind::UserInUse => ErrorCode::UserInUse,
460            ErrorKind::UserLocked => ErrorCode::UserLocked,
461            ErrorKind::UserSuspended => ErrorCode::UserSuspended,
462            ErrorKind::WeakPassword => ErrorCode::WeakPassword,
463            ErrorKind::WrongRoomKeysVersion(_) => ErrorCode::WrongRoomKeysVersion,
464            ErrorKind::_Custom(CustomErrorKind { errcode, .. }) => errcode.as_str().into(),
465        }
466    }
467
468    /// Get the JSON data for this `ErrorKind`, if it uses a custom error code.
469    pub fn custom_json_data(&self) -> Option<&JsonObject> {
470        as_variant!(self, Self::_Custom(error_kind) => &error_kind.data)
471    }
472}
473
474/// Data for the `M_BAD_STATUS` [`ErrorKind`].
475#[derive(Clone, Debug, Default, PartialEq, Eq)]
476#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
477pub struct BadStatusErrorData {
478    /// The HTTP status code of the response.
479    pub status: Option<http::StatusCode>,
480
481    /// The body of the response.
482    pub body: Option<String>,
483}
484
485impl BadStatusErrorData {
486    /// Construct a new empty `BadStatusErrorData`.
487    pub fn new() -> Self {
488        Self::default()
489    }
490}
491
492/// Data for the `M_INCOMPATIBLE_ROOM_VERSION` [`ErrorKind`].
493#[derive(Clone, Debug, PartialEq, Eq)]
494#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
495pub struct IncompatibleRoomVersionErrorData {
496    /// The room's version.
497    pub room_version: RoomVersionId,
498}
499
500impl IncompatibleRoomVersionErrorData {
501    /// Construct a new `IncompatibleRoomVersionErrorData` with the given room version.
502    pub fn new(room_version: RoomVersionId) -> Self {
503        Self { room_version }
504    }
505}
506
507/// Data for the `M_LIMIT_EXCEEDED` [`ErrorKind`].
508#[derive(Clone, Debug, Default, PartialEq, Eq)]
509#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
510pub struct LimitExceededErrorData {
511    /// How long a client should wait before they can try again.
512    pub retry_after: Option<RetryAfter>,
513}
514
515impl LimitExceededErrorData {
516    /// Construct a new empty `LimitExceededErrorData`.
517    pub fn new() -> Self {
518        Self::default()
519    }
520}
521
522/// Data for the `M_RESOURCE_LIMIT_EXCEEDED` [`ErrorKind`].
523#[derive(Clone, Debug, PartialEq, Eq)]
524#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
525pub struct ResourceLimitExceededErrorData {
526    /// A URI giving a contact method for the server administrator.
527    pub admin_contact: String,
528}
529
530impl ResourceLimitExceededErrorData {
531    /// Construct a new `ResourceLimitExceededErrorData` with the given admin contact URI.
532    pub fn new(admin_contact: String) -> Self {
533        Self { admin_contact }
534    }
535}
536
537/// Data for the `M_UNKNOWN_TOKEN` [`ErrorKind`].
538#[derive(Clone, Debug, Default, PartialEq, Eq)]
539#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
540pub struct UnknownTokenErrorData {
541    /// If this is `true`, the client is in a "[soft logout]" state, i.e. the server requires
542    /// re-authentication but the session is not invalidated. The client can acquire a new
543    /// access token by specifying the device ID it is already using to the login API.
544    ///
545    /// [soft logout]: https://spec.matrix.org/latest/client-server-api/#soft-logout
546    pub soft_logout: bool,
547}
548
549impl UnknownTokenErrorData {
550    /// Construct a new `UnknownTokenErrorData` with `soft_logout` set to `false`.
551    pub fn new() -> Self {
552        Self::default()
553    }
554}
555
556/// Data for the `M_WRONG_ROOM_KEYS_VERSION` [`ErrorKind`].
557#[derive(Clone, Debug, PartialEq, Eq)]
558#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
559pub struct WrongRoomKeysVersionErrorData {
560    /// The currently active backup version.
561    pub current_version: String,
562}
563
564impl WrongRoomKeysVersionErrorData {
565    /// Construct a new `WrongRoomKeysVersionErrorData` with the given current active backup
566    /// version.
567    pub fn new(current_version: String) -> Self {
568        Self { current_version }
569    }
570}
571
572/// A custom error kind.
573#[doc(hidden)]
574#[derive(Clone, Debug, PartialEq, Eq)]
575pub struct CustomErrorKind {
576    /// The error code.
577    errcode: String,
578
579    /// The data for the error.
580    data: JsonObject,
581}
582
583/// The possible [error codes] defined in the Matrix spec.
584///
585/// [error codes]: https://spec.matrix.org/latest/client-server-api/#standard-error-response
586#[derive(Clone, StringEnum)]
587#[non_exhaustive]
588#[ruma_enum(rename_all(prefix = "M_", rule = "SCREAMING_SNAKE_CASE"))]
589// Please keep the variants sorted alphabetically.
590pub enum ErrorCode {
591    /// `M_APPSERVICE_LOGIN_UNSUPPORTED`
592    ///
593    /// An application service used the [`m.login.application_service`] type an endpoint from the
594    /// [legacy authentication API] in a way that is not supported by the homeserver, because the
595    /// server only supports the [OAuth 2.0 API].
596    ///
597    /// [`m.login.application_service`]: https://spec.matrix.org/latest/application-service-api/#server-admin-style-permissions
598    /// [legacy authentication API]: https://spec.matrix.org/latest/client-server-api/#legacy-api
599    /// [OAuth 2.0 API]: https://spec.matrix.org/latest/client-server-api/#oauth-20-api
600    AppserviceLoginUnsupported,
601
602    /// `M_BAD_ALIAS`
603    ///
604    /// One or more [room aliases] within the `m.room.canonical_alias` event do not point to the
605    /// room ID for which the state event is to be sent to.
606    ///
607    /// [room aliases]: https://spec.matrix.org/latest/client-server-api/#room-aliases
608    BadAlias,
609
610    /// `M_BAD_JSON`
611    ///
612    /// The request contained valid JSON, but it was malformed in some way, e.g. missing required
613    /// keys, invalid values for keys.
614    BadJson,
615
616    /// `M_BAD_STATE`
617    ///
618    /// The state change requested cannot be performed, such as attempting to unban a user who is
619    /// not banned.
620    BadState,
621
622    /// `M_BAD_STATUS`
623    ///
624    /// The application service returned a bad status.
625    BadStatus,
626
627    /// `M_CANNOT_LEAVE_SERVER_NOTICE_ROOM`
628    ///
629    /// The user is unable to reject an invite to join the [server notices] room.
630    ///
631    /// [server notices]: https://spec.matrix.org/latest/client-server-api/#server-notices
632    CannotLeaveServerNoticeRoom,
633
634    /// `M_CANNOT_OVERWRITE_MEDIA`
635    ///
636    /// The [`create_content_async`] endpoint was called with a media ID that already has content.
637    ///
638    /// [`create_content_async`]: crate::media::create_content_async
639    CannotOverwriteMedia,
640
641    /// `M_CAPTCHA_INVALID`
642    ///
643    /// The Captcha provided did not match what was expected.
644    CaptchaInvalid,
645
646    /// `M_CAPTCHA_NEEDED`
647    ///
648    /// A Captcha is required to complete the request.
649    CaptchaNeeded,
650
651    /// `M_CONFLICTING_UNSUBSCRIPTION`
652    ///
653    /// Part of [MSC4306]: an automatic thread subscription has been skipped by the server, because
654    /// the user unsubsubscribed after the indicated subscribed-to event.
655    ///
656    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
657    #[cfg(feature = "unstable-msc4306")]
658    #[ruma_enum(rename = "IO.ELEMENT.MSC4306.M_CONFLICTING_UNSUBSCRIPTION")]
659    ConflictingUnsubscription,
660
661    /// `M_CONNECTION_FAILED`
662    ///
663    /// The connection to the application service failed.
664    ConnectionFailed,
665
666    /// `M_CONNECTION_TIMEOUT`
667    ///
668    /// The connection to the application service timed out.
669    ConnectionTimeout,
670
671    /// `M_DUPLICATE_ANNOTATION`
672    ///
673    /// The request is an attempt to send a [duplicate annotation].
674    ///
675    /// [duplicate annotation]: https://spec.matrix.org/latest/client-server-api/#avoiding-duplicate-annotations
676    DuplicateAnnotation,
677
678    /// `M_EXCLUSIVE`
679    ///
680    /// The resource being requested is reserved by an application service, or the application
681    /// service making the request has not created the resource.
682    Exclusive,
683
684    /// `M_FORBIDDEN`
685    ///
686    /// Forbidden access, e.g. joining a room without permission, failed login.
687    Forbidden,
688
689    /// `M_GUEST_ACCESS_FORBIDDEN`
690    ///
691    /// The room or resource does not permit [guests] to access it.
692    ///
693    /// [guests]: https://spec.matrix.org/latest/client-server-api/#guest-access
694    GuestAccessForbidden,
695
696    /// `M_INCOMPATIBLE_ROOM_VERSION`
697    ///
698    /// The client attempted to join a room that has a version the server does not support.
699    IncompatibleRoomVersion,
700
701    /// `M_INVALID_PARAM`
702    ///
703    /// A parameter that was specified has the wrong value. For example, the server expected an
704    /// integer and instead received a string.
705    InvalidParam,
706
707    /// `M_INVALID_ROOM_STATE`
708    ///
709    /// The initial state implied by the parameters to the [`create_room`] request is invalid, e.g.
710    /// the user's `power_level` is set below that necessary to set the room name.
711    ///
712    /// [`create_room`]: crate::room::create_room
713    InvalidRoomState,
714
715    /// `M_INVALID_USERNAME`
716    ///
717    /// The desired user name is not valid.
718    InvalidUsername,
719
720    /// `M_INVITE_BLOCKED`
721    ///
722    /// The invite was interdicted by moderation tools or configured access controls without having
723    /// been witnessed by the invitee.
724    ///
725    /// Unstable prefix intentionally shared with MSC4155 for compatibility.
726    #[ruma_enum(alias = "ORG.MATRIX.MSC4155.INVITE_BLOCKED")]
727    InviteBlocked,
728
729    /// `M_LIMIT_EXCEEDED`
730    ///
731    /// The request has been refused due to [rate limiting]: too many requests have been sent in a
732    /// short period of time.
733    ///
734    /// [rate limiting]: https://spec.matrix.org/latest/client-server-api/#rate-limiting
735    LimitExceeded,
736
737    /// `M_MISSING_PARAM`
738    ///
739    /// A required parameter was missing from the request.
740    MissingParam,
741
742    /// `M_MISSING_TOKEN`
743    ///
744    /// No [access token] was specified for the request, but one is required.
745    ///
746    /// [access token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
747    MissingToken,
748
749    /// `M_NOT_FOUND`
750    ///
751    /// No resource was found for this request.
752    NotFound,
753
754    /// `M_NOT_IN_THREAD`
755    ///
756    /// Part of [MSC4306]: an automatic thread subscription was set to an event ID that isn't part
757    /// of the subscribed-to thread.
758    ///
759    /// [MSC4306]: https://github.com/matrix-org/matrix-spec-proposals/pull/4306
760    #[cfg(feature = "unstable-msc4306")]
761    #[ruma_enum(rename = "IO.ELEMENT.MSC4306.M_NOT_IN_THREAD")]
762    NotInThread,
763
764    /// `M_NOT_JSON`
765    ///
766    /// The request did not contain valid JSON.
767    NotJson,
768
769    /// `M_NOT_YET_UPLOADED`
770    ///
771    /// An `mxc:` URI generated with the [`create_mxc_uri`] endpoint was used and the content is
772    /// not yet available.
773    ///
774    /// [`create_mxc_uri`]: crate::media::create_mxc_uri
775    NotYetUploaded,
776
777    /// `M_RESOURCE_LIMIT_EXCEEDED`
778    ///
779    /// The request cannot be completed because the homeserver has reached a resource limit imposed
780    /// on it. For example, a homeserver held in a shared hosting environment may reach a resource
781    /// limit if it starts using too much memory or disk space.
782    ResourceLimitExceeded,
783
784    /// `M_ROOM_IN_USE`
785    ///
786    /// The [room alias] specified in the [`create_room`] request is already taken.
787    ///
788    /// [`create_room`]: crate::room::create_room
789    /// [room alias]: https://spec.matrix.org/latest/client-server-api/#room-aliases
790    RoomInUse,
791
792    /// `M_SERVER_NOT_TRUSTED`
793    ///
794    /// The client's request used a third-party server, e.g. identity server, that this server does
795    /// not trust.
796    ServerNotTrusted,
797
798    /// `M_THREEPID_AUTH_FAILED`
799    ///
800    /// Authentication could not be performed on the [third-party identifier].
801    ///
802    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
803    ThreepidAuthFailed,
804
805    /// `M_THREEPID_DENIED`
806    ///
807    /// The server does not permit this [third-party identifier]. This may happen if the server
808    /// only permits, for example, email addresses from a particular domain.
809    ///
810    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
811    ThreepidDenied,
812
813    /// `M_THREEPID_IN_USE`
814    ///
815    /// The [third-party identifier] is already in use by another user.
816    ///
817    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
818    ThreepidInUse,
819
820    /// `M_THREEPID_MEDIUM_NOT_SUPPORTED`
821    ///
822    /// The homeserver does not support adding a [third-party identifier] of the given medium.
823    ///
824    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
825    ThreepidMediumNotSupported,
826
827    /// `M_THREEPID_NOT_FOUND`
828    ///
829    /// No account matching the given [third-party identifier] could be found.
830    ///
831    /// [third-party identifier]: https://spec.matrix.org/latest/client-server-api/#adding-account-administrative-contact-information
832    ThreepidNotFound,
833
834    /// `M_TOKEN_INCORRECT`
835    ///
836    /// The token that the user entered to validate the session is incorrect.
837    TokenIncorrect,
838
839    /// `M_TOO_LARGE`
840    ///
841    /// The request or entity was too large.
842    TooLarge,
843
844    /// `M_UNABLE_TO_AUTHORISE_JOIN`
845    ///
846    /// The room is [restricted] and none of the conditions can be validated by the homeserver.
847    /// This can happen if the homeserver does not know about any of the rooms listed as
848    /// conditions, for example.
849    ///
850    /// [restricted]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
851    #[ruma_enum(rename = "M_UNABLE_TO_AUTHORISE_JOIN")]
852    UnableToAuthorizeJoin,
853
854    /// `M_UNABLE_TO_GRANT_JOIN`
855    ///
856    /// A different server should be attempted for the join. This is typically because the resident
857    /// server can see that the joining user satisfies one or more conditions, such as in the case
858    /// of [restricted rooms], but the resident server would be unable to meet the authorization
859    /// rules.
860    ///
861    /// [restricted rooms]: https://spec.matrix.org/latest/client-server-api/#restricted-rooms
862    UnableToGrantJoin,
863
864    /// `M_UNACTIONABLE`
865    ///
866    /// The server does not want to handle the [federated report].
867    ///
868    /// [federated report]: https://github.com/matrix-org/matrix-spec-proposals/pull/3843
869    #[cfg(feature = "unstable-msc3843")]
870    Unactionable,
871
872    /// `M_UNAUTHORIZED`
873    ///
874    /// The request was not correctly authorized. Usually due to login failures.
875    Unauthorized,
876
877    /// `M_UNKNOWN`
878    ///
879    /// An unknown error has occurred.
880    Unknown,
881
882    /// `M_UNKNOWN_POS`
883    ///
884    /// The sliding sync ([MSC4186]) connection was expired by the server.
885    ///
886    /// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
887    #[cfg(feature = "unstable-msc4186")]
888    UnknownPos,
889
890    /// `M_UNKNOWN_TOKEN`
891    ///
892    /// The [access or refresh token] specified was not recognized.
893    ///
894    /// [access or refresh token]: https://spec.matrix.org/latest/client-server-api/#client-authentication
895    UnknownToken,
896
897    /// `M_UNRECOGNIZED`
898    ///
899    /// The server did not understand the request.
900    ///
901    /// This is expected to be returned with a 404 HTTP status code if the endpoint is not
902    /// implemented or a 405 HTTP status code if the endpoint is implemented, but the incorrect
903    /// HTTP method is used.
904    Unrecognized,
905
906    /// `M_UNSUPPORTED_ROOM_VERSION`
907    UnsupportedRoomVersion,
908
909    /// `M_URL_NOT_SET`
910    ///
911    /// The application service doesn't have a URL configured.
912    UrlNotSet,
913
914    /// `M_USER_DEACTIVATED`
915    ///
916    /// The user ID associated with the request has been deactivated.
917    UserDeactivated,
918
919    /// `M_USER_IN_USE`
920    ///
921    /// The desired user ID is already taken.
922    UserInUse,
923
924    /// `M_USER_LOCKED`
925    ///
926    /// The account has been [locked] and cannot be used at this time.
927    ///
928    /// [locked]: https://spec.matrix.org/latest/client-server-api/#account-locking
929    UserLocked,
930
931    /// `M_USER_SUSPENDED`
932    ///
933    /// The account has been [suspended] and can only be used for limited actions at this time.
934    ///
935    /// [suspended]: https://spec.matrix.org/latest/client-server-api/#account-suspension
936    UserSuspended,
937
938    /// `M_WEAK_PASSWORD`
939    ///
940    /// The password was [rejected] by the server for being too weak.
941    ///
942    /// [rejected]: https://spec.matrix.org/latest/client-server-api/#password-management
943    WeakPassword,
944
945    /// `M_WRONG_ROOM_KEYS_VERSION`
946    ///
947    /// The version of the [room keys backup] provided in the request does not match the current
948    /// backup version.
949    ///
950    /// [room keys backup]: https://spec.matrix.org/latest/client-server-api/#server-side-key-backups
951    WrongRoomKeysVersion,
952
953    #[doc(hidden)]
954    _Custom(PrivOwnedStr),
955}
956
957/// The body of a Matrix Client API error.
958#[derive(Debug, Clone)]
959#[allow(clippy::exhaustive_enums)]
960pub enum ErrorBody {
961    /// A JSON body with the fields expected for Client API errors.
962    Standard(StandardErrorBody),
963
964    /// A JSON body with an unexpected structure.
965    Json(JsonValue),
966
967    /// A response body that is not valid JSON.
968    NotJson {
969        /// The raw bytes of the response body.
970        bytes: Bytes,
971
972        /// The error from trying to deserialize the bytes as JSON.
973        deserialization_error: Arc<serde_json::Error>,
974    },
975}
976
977/// A JSON body with the fields expected for Client API errors.
978#[derive(Clone, Debug, Deserialize, Serialize)]
979#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
980pub struct StandardErrorBody {
981    /// A value which can be used to handle an error message.
982    #[serde(flatten)]
983    pub kind: ErrorKind,
984
985    /// A human-readable error message, usually a sentence explaining what went wrong.
986    #[serde(rename = "error")]
987    pub message: String,
988}
989
990impl StandardErrorBody {
991    /// Construct a new `StandardErrorBody` with the given kind and message.
992    pub fn new(kind: ErrorKind, message: String) -> Self {
993        Self { kind, message }
994    }
995}
996
997/// A Matrix Error
998#[derive(Debug, Clone)]
999#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1000pub struct Error {
1001    /// The http status code.
1002    pub status_code: http::StatusCode,
1003
1004    /// The http response's body.
1005    pub body: ErrorBody,
1006}
1007
1008impl Error {
1009    /// Constructs a new `Error` with the given status code and body.
1010    ///
1011    /// This is equivalent to calling `body.into_error(status_code)`.
1012    pub fn new(status_code: http::StatusCode, body: ErrorBody) -> Self {
1013        Self { status_code, body }
1014    }
1015
1016    /// If `self` is a server error in the `errcode` + `error` format expected
1017    /// for client-server API endpoints, returns the error kind (`errcode`).
1018    pub fn error_kind(&self) -> Option<&ErrorKind> {
1019        as_variant!(&self.body, ErrorBody::Standard(StandardErrorBody { kind, .. }) => kind)
1020    }
1021}
1022
1023impl EndpointError for Error {
1024    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self {
1025        let status = response.status();
1026
1027        let body_bytes = &response.body().as_ref();
1028        let error_body: ErrorBody = match from_json_slice::<StandardErrorBody>(body_bytes) {
1029            Ok(mut standard_body) => {
1030                let headers = response.headers();
1031
1032                if let ErrorKind::LimitExceeded(LimitExceededErrorData { retry_after }) =
1033                    &mut standard_body.kind
1034                {
1035                    // The Retry-After header takes precedence over the retry_after_ms field in
1036                    // the body.
1037                    if let Some(Ok(retry_after_header)) =
1038                        headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
1039                    {
1040                        *retry_after = Some(retry_after_header);
1041                    }
1042                }
1043
1044                ErrorBody::Standard(standard_body)
1045            }
1046            Err(_) => match MatrixErrorBody::from_bytes(body_bytes) {
1047                MatrixErrorBody::Json(json) => ErrorBody::Json(json),
1048                MatrixErrorBody::NotJson { bytes, deserialization_error, .. } => {
1049                    ErrorBody::NotJson { bytes, deserialization_error }
1050                }
1051            },
1052        };
1053
1054        error_body.into_error(status)
1055    }
1056}
1057
1058impl fmt::Display for Error {
1059    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1060        let status_code = self.status_code.as_u16();
1061        match &self.body {
1062            ErrorBody::Standard(StandardErrorBody { kind, message }) => {
1063                let errcode = kind.errcode();
1064                write!(f, "[{status_code} / {errcode}] {message}")
1065            }
1066            ErrorBody::Json(json) => write!(f, "[{status_code}] {json}"),
1067            ErrorBody::NotJson { .. } => write!(f, "[{status_code}] <non-json bytes>"),
1068        }
1069    }
1070}
1071
1072impl std::error::Error for Error {}
1073
1074impl ErrorBody {
1075    /// Convert the ErrorBody into an Error by adding the http status code.
1076    ///
1077    /// This is equivalent to calling `Error::new(status_code, self)`.
1078    pub fn into_error(self, status_code: http::StatusCode) -> Error {
1079        Error { status_code, body: self }
1080    }
1081}
1082
1083impl OutgoingResponse for Error {
1084    fn try_into_http_response<T: Default + BufMut>(
1085        self,
1086    ) -> Result<http::Response<T>, IntoHttpError> {
1087        let mut builder = http::Response::builder()
1088            .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
1089            .status(self.status_code);
1090
1091        // Add data in headers.
1092        if let Some(ErrorKind::LimitExceeded(LimitExceededErrorData {
1093            retry_after: Some(retry_after),
1094        })) = self.error_kind()
1095        {
1096            let header_value = http::HeaderValue::try_from(retry_after)?;
1097            builder = builder.header(http::header::RETRY_AFTER, header_value);
1098        }
1099
1100        builder
1101            .body(match self.body {
1102                ErrorBody::Standard(standard_body) => {
1103                    ruma_common::serde::json_to_buf(&standard_body)?
1104                }
1105                ErrorBody::Json(json) => ruma_common::serde::json_to_buf(&json)?,
1106                ErrorBody::NotJson { .. } => {
1107                    return Err(IntoHttpError::Json(serde::ser::Error::custom(
1108                        "attempted to serialize ErrorBody::NotJson",
1109                    )));
1110                }
1111            })
1112            .map_err(Into::into)
1113    }
1114}
1115
1116/// How long a client should wait before it tries again.
1117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1118#[allow(clippy::exhaustive_enums)]
1119pub enum RetryAfter {
1120    /// The client should wait for the given duration.
1121    ///
1122    /// This variant should be preferred for backwards compatibility, as it will also populate the
1123    /// `retry_after_ms` field in the body of the response.
1124    Delay(Duration),
1125    /// The client should wait for the given date and time.
1126    DateTime(SystemTime),
1127}
1128
1129impl TryFrom<&http::HeaderValue> for RetryAfter {
1130    type Error = HeaderDeserializationError;
1131
1132    fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
1133        if value.as_bytes().iter().all(|b| b.is_ascii_digit()) {
1134            // It should be a duration.
1135            Ok(Self::Delay(Duration::from_secs(u64::from_str(value.to_str()?)?)))
1136        } else {
1137            // It should be a date.
1138            Ok(Self::DateTime(http_date_to_system_time(value)?))
1139        }
1140    }
1141}
1142
1143impl TryFrom<&RetryAfter> for http::HeaderValue {
1144    type Error = HeaderSerializationError;
1145
1146    fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
1147        match value {
1148            RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
1149            RetryAfter::DateTime(time) => system_time_to_http_date(time),
1150        }
1151    }
1152}
1153
1154/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
1155pub trait FromHttpResponseErrorExt {
1156    /// If `self` is a server error in the `errcode` + `error` format expected
1157    /// for client-server API endpoints, returns the error kind (`errcode`).
1158    fn error_kind(&self) -> Option<&ErrorKind>;
1159}
1160
1161impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
1162    fn error_kind(&self) -> Option<&ErrorKind> {
1163        as_variant!(self, Self::Server)?.error_kind()
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use assert_matches2::assert_matches;
1170    use ruma_common::api::{EndpointError, OutgoingResponse};
1171    use serde_json::{
1172        Value as JsonValue, from_slice as from_json_slice, from_value as from_json_value, json,
1173    };
1174    use web_time::{Duration, UNIX_EPOCH};
1175
1176    use super::{
1177        Error, ErrorBody, ErrorKind, LimitExceededErrorData, RetryAfter, StandardErrorBody,
1178        WrongRoomKeysVersionErrorData,
1179    };
1180
1181    #[test]
1182    fn deserialize_forbidden() {
1183        let deserialized: StandardErrorBody = from_json_value(json!({
1184            "errcode": "M_FORBIDDEN",
1185            "error": "You are not authorized to ban users in this room.",
1186        }))
1187        .unwrap();
1188
1189        assert_eq!(deserialized.kind, ErrorKind::Forbidden);
1190        assert_eq!(deserialized.message, "You are not authorized to ban users in this room.");
1191    }
1192
1193    #[test]
1194    fn deserialize_wrong_room_key_version() {
1195        let deserialized: StandardErrorBody = from_json_value(json!({
1196            "current_version": "42",
1197            "errcode": "M_WRONG_ROOM_KEYS_VERSION",
1198            "error": "Wrong backup version."
1199        }))
1200        .expect("We should be able to deserialize a wrong room keys version error");
1201
1202        assert_matches!(
1203            deserialized.kind,
1204            ErrorKind::WrongRoomKeysVersion(WrongRoomKeysVersionErrorData { current_version })
1205        );
1206        assert_eq!(current_version, "42");
1207        assert_eq!(deserialized.message, "Wrong backup version.");
1208    }
1209
1210    #[test]
1211    fn deserialize_limit_exceeded_no_retry_after() {
1212        let response = http::Response::builder()
1213            .status(http::StatusCode::TOO_MANY_REQUESTS)
1214            .body(
1215                serde_json::to_string(&json!({
1216                    "errcode": "M_LIMIT_EXCEEDED",
1217                    "error": "Too many requests",
1218                }))
1219                .unwrap(),
1220            )
1221            .unwrap();
1222        let error = Error::from_http_response(response);
1223
1224        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1225        assert_matches!(
1226            error.body,
1227            ErrorBody::Standard(StandardErrorBody {
1228                kind: ErrorKind::LimitExceeded(LimitExceededErrorData { retry_after: None }),
1229                message
1230            })
1231        );
1232        assert_eq!(message, "Too many requests");
1233    }
1234
1235    #[test]
1236    fn deserialize_limit_exceeded_retry_after_body() {
1237        let response = http::Response::builder()
1238            .status(http::StatusCode::TOO_MANY_REQUESTS)
1239            .body(
1240                serde_json::to_string(&json!({
1241                    "errcode": "M_LIMIT_EXCEEDED",
1242                    "error": "Too many requests",
1243                    "retry_after_ms": 2000,
1244                }))
1245                .unwrap(),
1246            )
1247            .unwrap();
1248        let error = Error::from_http_response(response);
1249
1250        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1251        assert_matches!(
1252            error.body,
1253            ErrorBody::Standard(StandardErrorBody {
1254                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1255                    retry_after: Some(retry_after)
1256                }),
1257                message
1258            })
1259        );
1260        assert_matches!(retry_after, RetryAfter::Delay(delay));
1261        assert_eq!(delay.as_millis(), 2000);
1262        assert_eq!(message, "Too many requests");
1263    }
1264
1265    #[test]
1266    fn deserialize_limit_exceeded_retry_after_header_delay() {
1267        let response = http::Response::builder()
1268            .status(http::StatusCode::TOO_MANY_REQUESTS)
1269            .header(http::header::RETRY_AFTER, "2")
1270            .body(
1271                serde_json::to_string(&json!({
1272                    "errcode": "M_LIMIT_EXCEEDED",
1273                    "error": "Too many requests",
1274                }))
1275                .unwrap(),
1276            )
1277            .unwrap();
1278        let error = Error::from_http_response(response);
1279
1280        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1281        assert_matches!(
1282            error.body,
1283            ErrorBody::Standard(StandardErrorBody {
1284                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1285                    retry_after: Some(retry_after)
1286                }),
1287                message
1288            })
1289        );
1290        assert_matches!(retry_after, RetryAfter::Delay(delay));
1291        assert_eq!(delay.as_millis(), 2000);
1292        assert_eq!(message, "Too many requests");
1293    }
1294
1295    #[test]
1296    fn deserialize_limit_exceeded_retry_after_header_datetime() {
1297        let response = http::Response::builder()
1298            .status(http::StatusCode::TOO_MANY_REQUESTS)
1299            .header(http::header::RETRY_AFTER, "Fri, 15 May 2015 15:34:21 GMT")
1300            .body(
1301                serde_json::to_string(&json!({
1302                    "errcode": "M_LIMIT_EXCEEDED",
1303                    "error": "Too many requests",
1304                }))
1305                .unwrap(),
1306            )
1307            .unwrap();
1308        let error = Error::from_http_response(response);
1309
1310        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1311        assert_matches!(
1312            error.body,
1313            ErrorBody::Standard(StandardErrorBody {
1314                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1315                    retry_after: Some(retry_after)
1316                }),
1317                message
1318            })
1319        );
1320        assert_matches!(retry_after, RetryAfter::DateTime(time));
1321        assert_eq!(time.duration_since(UNIX_EPOCH).unwrap().as_secs(), 1_431_704_061);
1322        assert_eq!(message, "Too many requests");
1323    }
1324
1325    #[test]
1326    fn deserialize_limit_exceeded_retry_after_header_over_body() {
1327        let response = http::Response::builder()
1328            .status(http::StatusCode::TOO_MANY_REQUESTS)
1329            .header(http::header::RETRY_AFTER, "2")
1330            .body(
1331                serde_json::to_string(&json!({
1332                    "errcode": "M_LIMIT_EXCEEDED",
1333                    "error": "Too many requests",
1334                    "retry_after_ms": 3000,
1335                }))
1336                .unwrap(),
1337            )
1338            .unwrap();
1339        let error = Error::from_http_response(response);
1340
1341        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1342        assert_matches!(
1343            error.body,
1344            ErrorBody::Standard(StandardErrorBody {
1345                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1346                    retry_after: Some(retry_after)
1347                }),
1348                message
1349            })
1350        );
1351        assert_matches!(retry_after, RetryAfter::Delay(delay));
1352        assert_eq!(delay.as_millis(), 2000);
1353        assert_eq!(message, "Too many requests");
1354    }
1355
1356    #[test]
1357    fn serialize_limit_exceeded_retry_after_none() {
1358        let error = Error::new(
1359            http::StatusCode::TOO_MANY_REQUESTS,
1360            ErrorBody::Standard(StandardErrorBody {
1361                kind: ErrorKind::LimitExceeded(LimitExceededErrorData { retry_after: None }),
1362                message: "Too many requests".to_owned(),
1363            }),
1364        );
1365
1366        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1367
1368        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1369        assert_eq!(response.headers().get(http::header::RETRY_AFTER), None);
1370
1371        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1372        assert_eq!(
1373            json_body,
1374            json!({
1375                "errcode": "M_LIMIT_EXCEEDED",
1376                "error": "Too many requests",
1377            })
1378        );
1379    }
1380
1381    #[test]
1382    fn serialize_limit_exceeded_retry_after_delay() {
1383        let error = Error::new(
1384            http::StatusCode::TOO_MANY_REQUESTS,
1385            ErrorBody::Standard(StandardErrorBody {
1386                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1387                    retry_after: Some(RetryAfter::Delay(Duration::from_secs(3))),
1388                }),
1389                message: "Too many requests".to_owned(),
1390            }),
1391        );
1392
1393        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1394
1395        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1396        let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
1397        assert_eq!(retry_after_header.to_str().unwrap(), "3");
1398
1399        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1400        assert_eq!(
1401            json_body,
1402            json!({
1403                "errcode": "M_LIMIT_EXCEEDED",
1404                "error": "Too many requests",
1405                "retry_after_ms": 3000,
1406            })
1407        );
1408    }
1409
1410    #[test]
1411    fn serialize_limit_exceeded_retry_after_datetime() {
1412        let error = Error::new(
1413            http::StatusCode::TOO_MANY_REQUESTS,
1414            ErrorBody::Standard(StandardErrorBody {
1415                kind: ErrorKind::LimitExceeded(LimitExceededErrorData {
1416                    retry_after: Some(RetryAfter::DateTime(
1417                        UNIX_EPOCH + Duration::from_secs(1_431_704_061),
1418                    )),
1419                }),
1420                message: "Too many requests".to_owned(),
1421            }),
1422        );
1423
1424        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1425
1426        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1427        let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
1428        assert_eq!(retry_after_header.to_str().unwrap(), "Fri, 15 May 2015 15:34:21 GMT");
1429
1430        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1431        assert_eq!(
1432            json_body,
1433            json!({
1434                "errcode": "M_LIMIT_EXCEEDED",
1435                "error": "Too many requests",
1436            })
1437        );
1438    }
1439
1440    #[test]
1441    fn serialize_user_locked() {
1442        let error = Error::new(
1443            http::StatusCode::UNAUTHORIZED,
1444            ErrorBody::Standard(StandardErrorBody {
1445                kind: ErrorKind::UserLocked,
1446                message: "This account has been locked".to_owned(),
1447            }),
1448        );
1449
1450        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1451
1452        assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
1453        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1454        assert_eq!(
1455            json_body,
1456            json!({
1457                "errcode": "M_USER_LOCKED",
1458                "error": "This account has been locked",
1459                "soft_logout": true,
1460            })
1461        );
1462    }
1463
1464    #[test]
1465    fn deserialize_custom_error_kind() {
1466        let deserialized: StandardErrorBody = from_json_value(json!({
1467            "errcode": "LOCAL_DEV_ERROR",
1468            "error": "You are using the homeserver in local dev mode.",
1469            "foo": "bar",
1470        }))
1471        .unwrap();
1472
1473        assert_eq!(deserialized.kind.errcode().as_str(), "LOCAL_DEV_ERROR");
1474        assert_matches!(deserialized.kind.custom_json_data(), Some(json_data));
1475        assert_matches!(json_data.get("foo"), Some(JsonValue::String(foo)));
1476        assert_eq!(foo, "bar");
1477        assert_eq!(deserialized.message, "You are using the homeserver in local dev mode.");
1478    }
1479}