matrix_sdk/client/
futures.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![deny(unreachable_pub)]
16
17use std::{fmt::Debug, future::IntoFuture};
18
19use eyeball::SharedObservable;
20#[cfg(not(target_arch = "wasm32"))]
21use eyeball::Subscriber;
22#[cfg(feature = "experimental-oidc")]
23use mas_oidc_client::{
24    error::{
25        Error as OidcClientError, ErrorBody as OidcErrorBody, HttpError as OidcHttpError,
26        TokenRefreshError, TokenRequestError,
27    },
28    types::errors::ClientErrorCode,
29};
30use matrix_sdk_common::boxed_into_future;
31use ruma::api::{client::error::ErrorKind, error::FromHttpResponseError, OutgoingRequest};
32#[cfg(feature = "experimental-oidc")]
33use tracing::error;
34use tracing::trace;
35
36use super::super::Client;
37#[cfg(feature = "experimental-oidc")]
38use crate::authentication::oidc::OidcError;
39use crate::{
40    config::RequestConfig,
41    error::{HttpError, HttpResult},
42    RefreshTokenError, TransmissionProgress,
43};
44
45/// `IntoFuture` returned by [`Client::send`].
46#[allow(missing_debug_implementations)]
47pub struct SendRequest<R> {
48    pub(crate) client: Client,
49    pub(crate) homeserver_override: Option<String>,
50    pub(crate) request: R,
51    pub(crate) config: Option<RequestConfig>,
52    pub(crate) send_progress: SharedObservable<TransmissionProgress>,
53}
54
55impl<R> SendRequest<R> {
56    /// Replace the default `SharedObservable` used for tracking upload
57    /// progress.
58    ///
59    /// Note that any subscribers obtained from
60    /// [`subscribe_to_send_progress`][Self::subscribe_to_send_progress]
61    /// will be invalidated by this.
62    pub fn with_send_progress_observable(
63        mut self,
64        send_progress: SharedObservable<TransmissionProgress>,
65    ) -> Self {
66        self.send_progress = send_progress;
67        self
68    }
69
70    /// Replace this request's target (homeserver) with a custom one.
71    ///
72    /// This is useful at the moment because the current sliding sync
73    /// implementation uses a proxy server.
74    pub fn with_homeserver_override(mut self, homeserver_override: Option<String>) -> Self {
75        self.homeserver_override = homeserver_override;
76        self
77    }
78
79    /// Use the given [`RequestConfig`] for this send request, instead of the
80    /// one provided by default.
81    pub fn with_request_config(mut self, request_config: impl Into<Option<RequestConfig>>) -> Self {
82        self.config = request_config.into();
83        self
84    }
85
86    /// Get a subscriber to observe the progress of sending the request
87    /// body.
88    #[cfg(not(target_arch = "wasm32"))]
89    pub fn subscribe_to_send_progress(&self) -> Subscriber<TransmissionProgress> {
90        self.send_progress.subscribe()
91    }
92}
93
94impl<R> IntoFuture for SendRequest<R>
95where
96    R: OutgoingRequest + Clone + Debug + Send + Sync + 'static,
97    R::IncomingResponse: Send + Sync,
98    HttpError: From<FromHttpResponseError<R::EndpointError>>,
99{
100    type Output = HttpResult<R::IncomingResponse>;
101    boxed_into_future!();
102
103    fn into_future(self) -> Self::IntoFuture {
104        let Self { client, request, config, send_progress, homeserver_override } = self;
105
106        Box::pin(async move {
107            let res = Box::pin(client.send_inner(
108                request.clone(),
109                config,
110                homeserver_override.clone(),
111                send_progress.clone(),
112            ))
113            .await;
114
115            // An `M_UNKNOWN_TOKEN` error can potentially be fixed with a token refresh.
116            if let Err(Some(ErrorKind::UnknownToken { soft_logout })) =
117                res.as_ref().map_err(HttpError::client_api_error_kind)
118            {
119                trace!("Token refresh: Unknown token error received.");
120
121                // If automatic token refresh isn't supported, there is nothing more to do.
122                if !client.inner.auth_ctx.handle_refresh_tokens {
123                    trace!("Token refresh: Automatic refresh disabled.");
124                    client.broadcast_unknown_token(soft_logout);
125                    return res;
126                }
127
128                // Try to refresh the token and retry the request.
129                if let Err(refresh_error) = client.refresh_access_token().await {
130                    match &refresh_error {
131                        RefreshTokenError::RefreshTokenRequired => {
132                            trace!("Token refresh: The session doesn't have a refresh token.");
133                            // Refreshing access tokens is not supported by this `Session`, ignore.
134                            client.broadcast_unknown_token(soft_logout);
135                        }
136
137                        #[cfg(feature = "experimental-oidc")]
138                        RefreshTokenError::Oidc(oidc_error) => {
139                            match **oidc_error {
140                                OidcError::Oidc(OidcClientError::TokenRefresh(
141                                    TokenRefreshError::Token(TokenRequestError::Http(
142                                        OidcHttpError {
143                                            body:
144                                                Some(OidcErrorBody {
145                                                    error: ClientErrorCode::InvalidGrant,
146                                                    ..
147                                                }),
148                                            ..
149                                        },
150                                    )),
151                                )) => {
152                                    error!("Token refresh: OIDC refresh_token rejected with invalid grant");
153                                    // The refresh was denied, signal to sign out the user.
154                                    client.broadcast_unknown_token(soft_logout);
155                                }
156                                _ => {
157                                    trace!("Token refresh: OIDC refresh encountered a problem.");
158                                    // The refresh failed for other reasons, no
159                                    // need to sign out.
160                                }
161                            };
162                            return Err(HttpError::RefreshToken(refresh_error));
163                        }
164
165                        _ => {
166                            trace!("Token refresh: Token refresh failed.");
167                            // This isn't necessarily correct, but matches the behaviour when
168                            // implementing OIDC.
169                            client.broadcast_unknown_token(soft_logout);
170                            return Err(HttpError::RefreshToken(refresh_error));
171                        }
172                    }
173                } else {
174                    trace!("Token refresh: Refresh succeeded, retrying request.");
175                    return Box::pin(client.send_inner(
176                        request,
177                        config,
178                        homeserver_override,
179                        send_progress,
180                    ))
181                    .await;
182                }
183            }
184
185            res
186        })
187    }
188}