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(StandardErrorBody),
848
849    /// A JSON body with an unexpected structure.
850    Json(JsonValue),
851
852    /// A response body that is not valid JSON.
853    NotJson {
854        /// The raw bytes of the response body.
855        bytes: Bytes,
856
857        /// The error from trying to deserialize the bytes as JSON.
858        deserialization_error: Arc<serde_json::Error>,
859    },
860}
861
862/// A JSON body with the fields expected for Client API errors.
863#[derive(Clone, Debug, Deserialize, Serialize)]
864#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
865pub struct StandardErrorBody {
866    /// A value which can be used to handle an error message.
867    #[serde(flatten)]
868    pub kind: ErrorKind,
869
870    /// A human-readable error message, usually a sentence explaining what went wrong.
871    #[serde(rename = "error")]
872    pub message: String,
873}
874
875impl StandardErrorBody {
876    /// Construct a new `StandardErrorBody` with the given kind and message.
877    pub fn new(kind: ErrorKind, message: String) -> Self {
878        Self { kind, message }
879    }
880}
881
882/// A Matrix Error
883#[derive(Debug, Clone)]
884#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
885pub struct Error {
886    /// The http status code.
887    pub status_code: http::StatusCode,
888
889    /// The http response's body.
890    pub body: ErrorBody,
891}
892
893impl Error {
894    /// Constructs a new `Error` with the given status code and body.
895    ///
896    /// This is equivalent to calling `body.into_error(status_code)`.
897    pub fn new(status_code: http::StatusCode, body: ErrorBody) -> Self {
898        Self { status_code, body }
899    }
900
901    /// If `self` is a server error in the `errcode` + `error` format expected
902    /// for client-server API endpoints, returns the error kind (`errcode`).
903    pub fn error_kind(&self) -> Option<&ErrorKind> {
904        as_variant!(&self.body, ErrorBody::Standard(StandardErrorBody { kind, .. }) => kind)
905    }
906}
907
908impl EndpointError for Error {
909    fn from_http_response<T: AsRef<[u8]>>(response: http::Response<T>) -> Self {
910        let status = response.status();
911
912        let body_bytes = &response.body().as_ref();
913        let error_body: ErrorBody = match from_json_slice::<StandardErrorBody>(body_bytes) {
914            Ok(mut standard_body) => {
915                let headers = response.headers();
916
917                match &mut standard_body.kind {
918                    #[cfg(feature = "unstable-msc2967")]
919                    ErrorKind::Forbidden { authenticate } => {
920                        *authenticate = headers
921                            .get(http::header::WWW_AUTHENTICATE)
922                            .and_then(|val| val.to_str().ok())
923                            .and_then(AuthenticateError::from_str);
924                    }
925                    ErrorKind::LimitExceeded { retry_after } => {
926                        // The Retry-After header takes precedence over the retry_after_ms field in
927                        // the body.
928                        if let Some(Ok(retry_after_header)) =
929                            headers.get(http::header::RETRY_AFTER).map(RetryAfter::try_from)
930                        {
931                            *retry_after = Some(retry_after_header);
932                        }
933                    }
934                    _ => {}
935                }
936
937                ErrorBody::Standard(standard_body)
938            }
939            Err(_) => match MatrixErrorBody::from_bytes(body_bytes) {
940                MatrixErrorBody::Json(json) => ErrorBody::Json(json),
941                MatrixErrorBody::NotJson { bytes, deserialization_error, .. } => {
942                    ErrorBody::NotJson { bytes, deserialization_error }
943                }
944            },
945        };
946
947        error_body.into_error(status)
948    }
949}
950
951impl fmt::Display for Error {
952    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
953        let status_code = self.status_code.as_u16();
954        match &self.body {
955            ErrorBody::Standard(StandardErrorBody { kind, message }) => {
956                let errcode = kind.errcode();
957                write!(f, "[{status_code} / {errcode}] {message}")
958            }
959            ErrorBody::Json(json) => write!(f, "[{status_code}] {json}"),
960            ErrorBody::NotJson { .. } => write!(f, "[{status_code}] <non-json bytes>"),
961        }
962    }
963}
964
965impl std::error::Error for Error {}
966
967impl ErrorBody {
968    /// Convert the ErrorBody into an Error by adding the http status code.
969    ///
970    /// This is equivalent to calling `Error::new(status_code, self)`.
971    pub fn into_error(self, status_code: http::StatusCode) -> Error {
972        Error { status_code, body: self }
973    }
974}
975
976impl OutgoingResponse for Error {
977    fn try_into_http_response<T: Default + BufMut>(
978        self,
979    ) -> Result<http::Response<T>, IntoHttpError> {
980        let mut builder = http::Response::builder()
981            .header(http::header::CONTENT_TYPE, ruma_common::http_headers::APPLICATION_JSON)
982            .status(self.status_code);
983
984        #[allow(clippy::collapsible_match)]
985        if let Some(kind) = self.error_kind() {
986            match kind {
987                #[cfg(feature = "unstable-msc2967")]
988                ErrorKind::Forbidden { authenticate: Some(auth_error) } => {
989                    builder = builder.header(http::header::WWW_AUTHENTICATE, auth_error);
990                }
991                ErrorKind::LimitExceeded { retry_after: Some(retry_after) } => {
992                    let header_value = http::HeaderValue::try_from(retry_after)?;
993                    builder = builder.header(http::header::RETRY_AFTER, header_value);
994                }
995                _ => {}
996            }
997        }
998
999        builder
1000            .body(match self.body {
1001                ErrorBody::Standard(standard_body) => {
1002                    ruma_common::serde::json_to_buf(&standard_body)?
1003                }
1004                ErrorBody::Json(json) => ruma_common::serde::json_to_buf(&json)?,
1005                ErrorBody::NotJson { .. } => {
1006                    return Err(IntoHttpError::Json(serde::ser::Error::custom(
1007                        "attempted to serialize ErrorBody::NotJson",
1008                    )));
1009                }
1010            })
1011            .map_err(Into::into)
1012    }
1013}
1014
1015/// Errors in the `WWW-Authenticate` header.
1016///
1017/// To construct this use `::from_str()`. To get its serialized form, use its
1018/// `TryInto<http::HeaderValue>` implementation.
1019#[cfg(feature = "unstable-msc2967")]
1020#[derive(Clone, Debug, PartialEq, Eq)]
1021#[non_exhaustive]
1022pub enum AuthenticateError {
1023    /// insufficient_scope
1024    ///
1025    /// Encountered when authentication is handled by OpenID Connect and the current access token
1026    /// isn't authorized for the proper scope for this request. It should be paired with a
1027    /// `401` status code and a `M_FORBIDDEN` error.
1028    InsufficientScope {
1029        /// The new scope to request an authorization for.
1030        scope: String,
1031    },
1032
1033    #[doc(hidden)]
1034    _Custom { errcode: PrivOwnedStr, attributes: AuthenticateAttrs },
1035}
1036
1037#[cfg(feature = "unstable-msc2967")]
1038#[doc(hidden)]
1039#[derive(Clone, Debug, PartialEq, Eq)]
1040pub struct AuthenticateAttrs(BTreeMap<String, String>);
1041
1042#[cfg(feature = "unstable-msc2967")]
1043impl AuthenticateError {
1044    /// Construct an `AuthenticateError` from a string.
1045    ///
1046    /// Returns `None` if the string doesn't contain an error.
1047    fn from_str(s: &str) -> Option<Self> {
1048        if let Some(val) = s.strip_prefix("Bearer").map(str::trim) {
1049            let mut errcode = None;
1050            let mut attrs = BTreeMap::new();
1051
1052            // Split the attributes separated by commas and optionally spaces, then split the keys
1053            // and the values, with the values optionally surrounded by double quotes.
1054            for (key, value) in val
1055                .split(',')
1056                .filter_map(|attr| attr.trim().split_once('='))
1057                .map(|(key, value)| (key, value.trim_matches('"')))
1058            {
1059                if key == "error" {
1060                    errcode = Some(value);
1061                } else {
1062                    attrs.insert(key.to_owned(), value.to_owned());
1063                }
1064            }
1065
1066            if let Some(errcode) = errcode {
1067                let error = if let Some(scope) =
1068                    attrs.get("scope").filter(|_| errcode == "insufficient_scope")
1069                {
1070                    AuthenticateError::InsufficientScope { scope: scope.to_owned() }
1071                } else {
1072                    AuthenticateError::_Custom {
1073                        errcode: PrivOwnedStr(errcode.into()),
1074                        attributes: AuthenticateAttrs(attrs),
1075                    }
1076                };
1077
1078                return Some(error);
1079            }
1080        }
1081
1082        None
1083    }
1084}
1085
1086#[cfg(feature = "unstable-msc2967")]
1087impl TryFrom<&AuthenticateError> for http::HeaderValue {
1088    type Error = http::header::InvalidHeaderValue;
1089
1090    fn try_from(error: &AuthenticateError) -> Result<Self, Self::Error> {
1091        let s = match error {
1092            AuthenticateError::InsufficientScope { scope } => {
1093                format!("Bearer error=\"insufficient_scope\", scope=\"{scope}\"")
1094            }
1095            AuthenticateError::_Custom { errcode, attributes } => {
1096                let mut s = format!("Bearer error=\"{}\"", errcode.0);
1097
1098                for (key, value) in attributes.0.iter() {
1099                    s.push_str(&format!(", {key}=\"{value}\""));
1100                }
1101
1102                s
1103            }
1104        };
1105
1106        s.try_into()
1107    }
1108}
1109
1110/// How long a client should wait before it tries again.
1111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1112#[allow(clippy::exhaustive_enums)]
1113pub enum RetryAfter {
1114    /// The client should wait for the given duration.
1115    ///
1116    /// This variant should be preferred for backwards compatibility, as it will also populate the
1117    /// `retry_after_ms` field in the body of the response.
1118    Delay(Duration),
1119    /// The client should wait for the given date and time.
1120    DateTime(SystemTime),
1121}
1122
1123impl TryFrom<&http::HeaderValue> for RetryAfter {
1124    type Error = HeaderDeserializationError;
1125
1126    fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
1127        if value.as_bytes().iter().all(|b| b.is_ascii_digit()) {
1128            // It should be a duration.
1129            Ok(Self::Delay(Duration::from_secs(u64::from_str(value.to_str()?)?)))
1130        } else {
1131            // It should be a date.
1132            Ok(Self::DateTime(http_date_to_system_time(value)?))
1133        }
1134    }
1135}
1136
1137impl TryFrom<&RetryAfter> for http::HeaderValue {
1138    type Error = HeaderSerializationError;
1139
1140    fn try_from(value: &RetryAfter) -> Result<Self, Self::Error> {
1141        match value {
1142            RetryAfter::Delay(duration) => Ok(duration.as_secs().into()),
1143            RetryAfter::DateTime(time) => system_time_to_http_date(time),
1144        }
1145    }
1146}
1147
1148/// Extension trait for `FromHttpResponseError<ruma_client_api::Error>`.
1149pub trait FromHttpResponseErrorExt {
1150    /// If `self` is a server error in the `errcode` + `error` format expected
1151    /// for client-server API endpoints, returns the error kind (`errcode`).
1152    fn error_kind(&self) -> Option<&ErrorKind>;
1153}
1154
1155impl FromHttpResponseErrorExt for FromHttpResponseError<Error> {
1156    fn error_kind(&self) -> Option<&ErrorKind> {
1157        as_variant!(self, Self::Server)?.error_kind()
1158    }
1159}
1160
1161#[cfg(test)]
1162mod tests {
1163    use assert_matches2::assert_matches;
1164    use ruma_common::api::{EndpointError, OutgoingResponse};
1165    use serde_json::{
1166        from_slice as from_json_slice, from_value as from_json_value, json, Value as JsonValue,
1167    };
1168    use web_time::{Duration, UNIX_EPOCH};
1169
1170    use super::{Error, ErrorBody, ErrorKind, RetryAfter, StandardErrorBody};
1171
1172    #[test]
1173    fn deserialize_forbidden() {
1174        let deserialized: StandardErrorBody = from_json_value(json!({
1175            "errcode": "M_FORBIDDEN",
1176            "error": "You are not authorized to ban users in this room.",
1177        }))
1178        .unwrap();
1179
1180        assert_eq!(
1181            deserialized.kind,
1182            ErrorKind::Forbidden {
1183                #[cfg(feature = "unstable-msc2967")]
1184                authenticate: None
1185            }
1186        );
1187        assert_eq!(deserialized.message, "You are not authorized to ban users in this room.");
1188    }
1189
1190    #[test]
1191    fn deserialize_wrong_room_key_version() {
1192        let deserialized: StandardErrorBody = from_json_value(json!({
1193            "current_version": "42",
1194            "errcode": "M_WRONG_ROOM_KEYS_VERSION",
1195            "error": "Wrong backup version."
1196        }))
1197        .expect("We should be able to deserialize a wrong room keys version error");
1198
1199        assert_matches!(deserialized.kind, ErrorKind::WrongRoomKeysVersion { current_version });
1200        assert_eq!(current_version.as_deref(), Some("42"));
1201        assert_eq!(deserialized.message, "Wrong backup version.");
1202    }
1203
1204    #[cfg(feature = "unstable-msc2967")]
1205    #[test]
1206    fn custom_authenticate_error_sanity() {
1207        use super::AuthenticateError;
1208
1209        let s = "Bearer error=\"custom_error\", misc=\"some content\"";
1210
1211        let error = AuthenticateError::from_str(s).unwrap();
1212        let error_header = http::HeaderValue::try_from(&error).unwrap();
1213
1214        assert_eq!(error_header.to_str().unwrap(), s);
1215    }
1216
1217    #[cfg(feature = "unstable-msc2967")]
1218    #[test]
1219    fn serialize_insufficient_scope() {
1220        use super::AuthenticateError;
1221
1222        let error =
1223            AuthenticateError::InsufficientScope { scope: "something_privileged".to_owned() };
1224        let error_header = http::HeaderValue::try_from(&error).unwrap();
1225
1226        assert_eq!(
1227            error_header.to_str().unwrap(),
1228            "Bearer error=\"insufficient_scope\", scope=\"something_privileged\""
1229        );
1230    }
1231
1232    #[cfg(feature = "unstable-msc2967")]
1233    #[test]
1234    fn deserialize_insufficient_scope() {
1235        use super::AuthenticateError;
1236
1237        let response = http::Response::builder()
1238            .header(
1239                http::header::WWW_AUTHENTICATE,
1240                "Bearer error=\"insufficient_scope\", scope=\"something_privileged\"",
1241            )
1242            .status(http::StatusCode::UNAUTHORIZED)
1243            .body(
1244                serde_json::to_string(&json!({
1245                    "errcode": "M_FORBIDDEN",
1246                    "error": "Insufficient privilege",
1247                }))
1248                .unwrap(),
1249            )
1250            .unwrap();
1251        let error = Error::from_http_response(response);
1252
1253        assert_eq!(error.status_code, http::StatusCode::UNAUTHORIZED);
1254        assert_matches!(error.body, ErrorBody::Standard(StandardErrorBody { kind, message }));
1255        assert_matches!(kind, ErrorKind::Forbidden { authenticate });
1256        assert_eq!(message, "Insufficient privilege");
1257        assert_matches!(authenticate, Some(AuthenticateError::InsufficientScope { scope }));
1258        assert_eq!(scope, "something_privileged");
1259    }
1260
1261    #[test]
1262    fn deserialize_limit_exceeded_no_retry_after() {
1263        let response = http::Response::builder()
1264            .status(http::StatusCode::TOO_MANY_REQUESTS)
1265            .body(
1266                serde_json::to_string(&json!({
1267                    "errcode": "M_LIMIT_EXCEEDED",
1268                    "error": "Too many requests",
1269                }))
1270                .unwrap(),
1271            )
1272            .unwrap();
1273        let error = Error::from_http_response(response);
1274
1275        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1276        assert_matches!(
1277            error.body,
1278            ErrorBody::Standard(StandardErrorBody {
1279                kind: ErrorKind::LimitExceeded { retry_after: None },
1280                message
1281            })
1282        );
1283        assert_eq!(message, "Too many requests");
1284    }
1285
1286    #[test]
1287    fn deserialize_limit_exceeded_retry_after_body() {
1288        let response = http::Response::builder()
1289            .status(http::StatusCode::TOO_MANY_REQUESTS)
1290            .body(
1291                serde_json::to_string(&json!({
1292                    "errcode": "M_LIMIT_EXCEEDED",
1293                    "error": "Too many requests",
1294                    "retry_after_ms": 2000,
1295                }))
1296                .unwrap(),
1297            )
1298            .unwrap();
1299        let error = Error::from_http_response(response);
1300
1301        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1302        assert_matches!(
1303            error.body,
1304            ErrorBody::Standard(StandardErrorBody {
1305                kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
1306                message
1307            })
1308        );
1309        assert_matches!(retry_after, RetryAfter::Delay(delay));
1310        assert_eq!(delay.as_millis(), 2000);
1311        assert_eq!(message, "Too many requests");
1312    }
1313
1314    #[test]
1315    fn deserialize_limit_exceeded_retry_after_header_delay() {
1316        let response = http::Response::builder()
1317            .status(http::StatusCode::TOO_MANY_REQUESTS)
1318            .header(http::header::RETRY_AFTER, "2")
1319            .body(
1320                serde_json::to_string(&json!({
1321                    "errcode": "M_LIMIT_EXCEEDED",
1322                    "error": "Too many requests",
1323                }))
1324                .unwrap(),
1325            )
1326            .unwrap();
1327        let error = Error::from_http_response(response);
1328
1329        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1330        assert_matches!(
1331            error.body,
1332            ErrorBody::Standard(StandardErrorBody {
1333                kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
1334                message
1335            })
1336        );
1337        assert_matches!(retry_after, RetryAfter::Delay(delay));
1338        assert_eq!(delay.as_millis(), 2000);
1339        assert_eq!(message, "Too many requests");
1340    }
1341
1342    #[test]
1343    fn deserialize_limit_exceeded_retry_after_header_datetime() {
1344        let response = http::Response::builder()
1345            .status(http::StatusCode::TOO_MANY_REQUESTS)
1346            .header(http::header::RETRY_AFTER, "Fri, 15 May 2015 15:34:21 GMT")
1347            .body(
1348                serde_json::to_string(&json!({
1349                    "errcode": "M_LIMIT_EXCEEDED",
1350                    "error": "Too many requests",
1351                }))
1352                .unwrap(),
1353            )
1354            .unwrap();
1355        let error = Error::from_http_response(response);
1356
1357        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1358        assert_matches!(
1359            error.body,
1360            ErrorBody::Standard(StandardErrorBody {
1361                kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
1362                message
1363            })
1364        );
1365        assert_matches!(retry_after, RetryAfter::DateTime(time));
1366        assert_eq!(time.duration_since(UNIX_EPOCH).unwrap().as_secs(), 1_431_704_061);
1367        assert_eq!(message, "Too many requests");
1368    }
1369
1370    #[test]
1371    fn deserialize_limit_exceeded_retry_after_header_over_body() {
1372        let response = http::Response::builder()
1373            .status(http::StatusCode::TOO_MANY_REQUESTS)
1374            .header(http::header::RETRY_AFTER, "2")
1375            .body(
1376                serde_json::to_string(&json!({
1377                    "errcode": "M_LIMIT_EXCEEDED",
1378                    "error": "Too many requests",
1379                    "retry_after_ms": 3000,
1380                }))
1381                .unwrap(),
1382            )
1383            .unwrap();
1384        let error = Error::from_http_response(response);
1385
1386        assert_eq!(error.status_code, http::StatusCode::TOO_MANY_REQUESTS);
1387        assert_matches!(
1388            error.body,
1389            ErrorBody::Standard(StandardErrorBody {
1390                kind: ErrorKind::LimitExceeded { retry_after: Some(retry_after) },
1391                message
1392            })
1393        );
1394        assert_matches!(retry_after, RetryAfter::Delay(delay));
1395        assert_eq!(delay.as_millis(), 2000);
1396        assert_eq!(message, "Too many requests");
1397    }
1398
1399    #[test]
1400    fn serialize_limit_exceeded_retry_after_none() {
1401        let error = Error::new(
1402            http::StatusCode::TOO_MANY_REQUESTS,
1403            ErrorBody::Standard(StandardErrorBody {
1404                kind: ErrorKind::LimitExceeded { retry_after: None },
1405                message: "Too many requests".to_owned(),
1406            }),
1407        );
1408
1409        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1410
1411        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1412        assert_eq!(response.headers().get(http::header::RETRY_AFTER), None);
1413
1414        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1415        assert_eq!(
1416            json_body,
1417            json!({
1418                "errcode": "M_LIMIT_EXCEEDED",
1419                "error": "Too many requests",
1420            })
1421        );
1422    }
1423
1424    #[test]
1425    fn serialize_limit_exceeded_retry_after_delay() {
1426        let error = Error::new(
1427            http::StatusCode::TOO_MANY_REQUESTS,
1428            ErrorBody::Standard(StandardErrorBody {
1429                kind: ErrorKind::LimitExceeded {
1430                    retry_after: Some(RetryAfter::Delay(Duration::from_secs(3))),
1431                },
1432                message: "Too many requests".to_owned(),
1433            }),
1434        );
1435
1436        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1437
1438        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1439        let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
1440        assert_eq!(retry_after_header.to_str().unwrap(), "3");
1441
1442        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1443        assert_eq!(
1444            json_body,
1445            json!({
1446                "errcode": "M_LIMIT_EXCEEDED",
1447                "error": "Too many requests",
1448                "retry_after_ms": 3000,
1449            })
1450        );
1451    }
1452
1453    #[test]
1454    fn serialize_limit_exceeded_retry_after_datetime() {
1455        let error = Error::new(
1456            http::StatusCode::TOO_MANY_REQUESTS,
1457            ErrorBody::Standard(StandardErrorBody {
1458                kind: ErrorKind::LimitExceeded {
1459                    retry_after: Some(RetryAfter::DateTime(
1460                        UNIX_EPOCH + Duration::from_secs(1_431_704_061),
1461                    )),
1462                },
1463                message: "Too many requests".to_owned(),
1464            }),
1465        );
1466
1467        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1468
1469        assert_eq!(response.status(), http::StatusCode::TOO_MANY_REQUESTS);
1470        let retry_after_header = response.headers().get(http::header::RETRY_AFTER).unwrap();
1471        assert_eq!(retry_after_header.to_str().unwrap(), "Fri, 15 May 2015 15:34:21 GMT");
1472
1473        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1474        assert_eq!(
1475            json_body,
1476            json!({
1477                "errcode": "M_LIMIT_EXCEEDED",
1478                "error": "Too many requests",
1479            })
1480        );
1481    }
1482
1483    #[test]
1484    fn serialize_user_locked() {
1485        let error = Error::new(
1486            http::StatusCode::UNAUTHORIZED,
1487            ErrorBody::Standard(StandardErrorBody {
1488                kind: ErrorKind::UserLocked,
1489                message: "This account has been locked".to_owned(),
1490            }),
1491        );
1492
1493        let response = error.try_into_http_response::<Vec<u8>>().unwrap();
1494
1495        assert_eq!(response.status(), http::StatusCode::UNAUTHORIZED);
1496        let json_body: JsonValue = from_json_slice(response.body()).unwrap();
1497        assert_eq!(
1498            json_body,
1499            json!({
1500                "errcode": "M_USER_LOCKED",
1501                "error": "This account has been locked",
1502                "soft_logout": true,
1503            })
1504        );
1505    }
1506}