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