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