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