ruma_client_api/
error.rs

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