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