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