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