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