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