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