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