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