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