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