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