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