Skip to content

Commit c1302c4

Browse files
zecakehandybalaam
authored andcommitted
feat(sdk): Allow to refresh the token in Client::fetch_server_versions
We need to handle 2 possible deadlocks for this: 1. We cannot try to refresh an expired access token if this call happens while we are currently trying to refresh the token. The easiest way to handle this is to never try to refresh the token when making this call inside `get_path_builder_input()` so we implement a "failsafe" mode that disables refreshing the access token in case it expired. However it attempts the GET /versions again without the token. 2. We cannot access the cached supported versions if we are in the process of refreshing that cache because the RwLock has a write lock. So if the access token has expired and we try to refresh it, the possible calls to `get_path_builder_input()` must not wait for a read lock to be available. So the solution is to never wait for a read lock, and skip the cache if a read lock is not available. This also gets rid of workarounds in other functions. Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
1 parent 176684a commit c1302c4

6 files changed

Lines changed: 249 additions & 53 deletions

File tree

crates/matrix-sdk-ui/src/room_list_service/mod.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ use eyeball::Subscriber;
6363
use futures_util::{Stream, StreamExt, pin_mut};
6464
use matrix_sdk::{
6565
Client, Error as SlidingSyncError, Room, SlidingSync, SlidingSyncList, SlidingSyncMode,
66-
config::RequestConfig, event_cache::EventCacheError, timeout::timeout,
66+
event_cache::EventCacheError, timeout::timeout,
6767
};
6868
pub use room_list::*;
6969
use ruma::{
@@ -165,14 +165,8 @@ impl RoomListService {
165165
{
166166
cached.features
167167
} else {
168-
// Our `/versions` calls don't support token refresh as of now (11.11.2025), so
169-
// we're going to skip the sending of the authentication headers in case they
170-
// might have expired.
171-
//
172-
// We only care about a feature which is advertised without being authenticaded
173-
// anyways.
174168
client
175-
.fetch_server_versions(Some(RequestConfig::new().skip_auth()))
169+
.fetch_server_versions(None)
176170
.await
177171
.map_err(|e| Error::SlidingSync(e.into()))?
178172
.as_supported_versions()

crates/matrix-sdk/src/authentication/oauth/mod.rs

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -510,26 +510,17 @@ impl OAuth {
510510
.is_some_and(|err| err.status_code == http::StatusCode::NOT_FOUND)
511511
};
512512

513-
let response = self
514-
.client
515-
.send(get_authorization_server_metadata::v1::Request::new())
516-
// Skip auth while sending this request. This request itself might not require auth,
517-
// but as part of the building of the correct URL we might do a `/versions` call which
518-
// optionally accepts it.
519-
//
520-
// If we're fetching the server metadata to refresh our token, then we don't have valid
521-
// auth headers so the `/versions` call will fail and subsequently the refresh of the
522-
// token as well.
523-
.with_request_config(self.client.request_config().skip_auth())
524-
.await
525-
.map_err(|error| {
526-
// If the endpoint returns a 404, i.e. the server doesn't support the endpoint.
527-
if is_endpoint_unsupported(&error) {
528-
OAuthDiscoveryError::NotSupported
529-
} else {
530-
error.into()
531-
}
532-
})?;
513+
let response =
514+
self.client.send(get_authorization_server_metadata::v1::Request::new()).await.map_err(
515+
|error| {
516+
// If the endpoint returns a 404, i.e. the server doesn't support the endpoint.
517+
if is_endpoint_unsupported(&error) {
518+
OAuthDiscoveryError::NotSupported
519+
} else {
520+
error.into()
521+
}
522+
},
523+
)?;
533524

534525
let metadata = response.metadata.deserialize()?;
535526

crates/matrix-sdk/src/client/caches.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ use tokio::sync::RwLock;
2222
/// A collection of in-memory data that the `Client` might want to cache to
2323
/// avoid hitting the homeserver every time users request the data.
2424
pub(crate) struct ClientCaches {
25-
/// Supported versions, either prefilled during building or fetched from the
26-
/// server.
27-
pub(super) supported_versions: RwLock<CachedValue<SupportedVersions>>,
25+
/// The supported versions of the homeserver.
26+
///
27+
/// We only want to cache:
28+
///
29+
/// - The versions prefilled with `ClientBuilder::server_versions()`
30+
/// - The versions fetched from an *authenticated* request to the server.
31+
pub(crate) supported_versions: RwLock<CachedValue<SupportedVersions>>,
2832
/// Well-known information.
2933
pub(super) well_known: RwLock<CachedValue<Option<WellKnownResponse>>>,
3034
pub(crate) server_metadata: tokio::sync::Mutex<TtlCache<String, AuthorizationServerMetadata>>,
@@ -34,7 +38,7 @@ pub(crate) struct ClientCaches {
3438
/// between a value that is set to `None` (because it doesn't exist) and a value
3539
/// that has not been cached yet.
3640
#[derive(Clone)]
37-
pub(super) enum CachedValue<Value> {
41+
pub(crate) enum CachedValue<Value> {
3842
/// A value has been cached.
3943
Cached(Value),
4044
/// Nothing has been cached yet.

crates/matrix-sdk/src/client/mod.rs

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1975,11 +1975,73 @@ impl Client {
19751975
&self,
19761976
request_config: Option<RequestConfig>,
19771977
) -> HttpResult<get_supported_versions::Response> {
1978-
let server_versions = self
1979-
.send_inner(get_supported_versions::Request::new(), request_config, Default::default())
1980-
.await?;
1978+
// Since this was called by the user, try to refresh the access token if
1979+
// necessary.
1980+
self.fetch_server_versions_inner(false, request_config).await
1981+
}
1982+
1983+
/// Fetches server versions from network; no caching.
1984+
///
1985+
/// If the access token is expired and `failsafe` is `false`, this will
1986+
/// attempt to refresh the access token, otherwise this will try to make an
1987+
/// unauthenticated request instead.
1988+
pub(crate) async fn fetch_server_versions_inner(
1989+
&self,
1990+
failsafe: bool,
1991+
request_config: Option<RequestConfig>,
1992+
) -> HttpResult<get_supported_versions::Response> {
1993+
if !failsafe {
1994+
// `Client::send()` handles refreshing access tokens.
1995+
return self
1996+
.send(get_supported_versions::Request::new())
1997+
.with_request_config(request_config)
1998+
.await;
1999+
}
19812000

1982-
Ok(server_versions)
2001+
let homeserver = self.homeserver().to_string();
2002+
2003+
// If we have a fresh access token, try with it first.
2004+
if !request_config.as_ref().is_some_and(|config| config.skip_auth && !config.force_auth)
2005+
&& self.auth_ctx().has_valid_access_token()
2006+
&& let Some(access_token) = self.access_token()
2007+
{
2008+
let result = self
2009+
.inner
2010+
.http_client
2011+
.send(
2012+
get_supported_versions::Request::new(),
2013+
request_config,
2014+
homeserver.clone(),
2015+
Some(&access_token),
2016+
(),
2017+
Default::default(),
2018+
)
2019+
.await;
2020+
2021+
if let Err(Some(ErrorKind::UnknownToken { .. })) =
2022+
result.as_ref().map_err(HttpError::client_api_error_kind)
2023+
{
2024+
// If the access token is actually expired, mark it as expired and fallback to
2025+
// the unauthenticated request below.
2026+
self.auth_ctx().set_access_token_expired(&access_token);
2027+
} else {
2028+
// If the request succeeded or it's an other error, just stop now.
2029+
return result;
2030+
}
2031+
}
2032+
2033+
// Try without authentication.
2034+
self.inner
2035+
.http_client
2036+
.send(
2037+
get_supported_versions::Request::new(),
2038+
request_config,
2039+
homeserver.clone(),
2040+
None,
2041+
(),
2042+
Default::default(),
2043+
)
2044+
.await
19832045
}
19842046

19852047
/// Fetches client well_known from network; no caching.
@@ -2019,7 +2081,13 @@ impl Client {
20192081

20202082
/// Load supported versions from storage, or fetch them from network and
20212083
/// cache them.
2022-
async fn load_or_fetch_supported_versions(&self) -> HttpResult<SupportedVersionsResponse> {
2084+
///
2085+
/// If `failsafe` is true, this will try to minimize side effects to avoid
2086+
/// possible deadlocks.
2087+
async fn load_or_fetch_supported_versions(
2088+
&self,
2089+
failsafe: bool,
2090+
) -> HttpResult<SupportedVersionsResponse> {
20232091
match self.state_store().get_kv_data(StateStoreDataKey::SupportedVersions).await {
20242092
Ok(Some(stored)) => {
20252093
if let Some(supported_versions) =
@@ -2037,7 +2105,7 @@ impl Client {
20372105
}
20382106
}
20392107

2040-
let server_versions = self.fetch_server_versions(None).await?;
2108+
let server_versions = self.fetch_server_versions_inner(failsafe, None).await?;
20412109
let supported_versions = SupportedVersionsResponse {
20422110
versions: server_versions.versions,
20432111
unstable_features: server_versions.unstable_features,
@@ -2099,6 +2167,18 @@ impl Client {
20992167
/// # anyhow::Ok(()) };
21002168
/// ```
21012169
pub async fn supported_versions(&self) -> HttpResult<SupportedVersions> {
2170+
self.supported_versions_inner(false).await
2171+
}
2172+
2173+
/// Get the Matrix versions and features supported by the homeserver by
2174+
/// fetching them from the server or the cache.
2175+
///
2176+
/// If `failsafe` is true, this will try to minimize side effects to avoid
2177+
/// possible deadlocks.
2178+
pub(crate) async fn supported_versions_inner(
2179+
&self,
2180+
failsafe: bool,
2181+
) -> HttpResult<SupportedVersions> {
21022182
let cached_supported_versions = &self.inner.caches.supported_versions;
21032183
if let CachedValue::Cached(val) = &*cached_supported_versions.read().await {
21042184
return Ok(val.clone());
@@ -2109,7 +2189,7 @@ impl Client {
21092189
return Ok(val.clone());
21102190
}
21112191

2112-
let supported = self.load_or_fetch_supported_versions().await?;
2192+
let supported = self.load_or_fetch_supported_versions(failsafe).await?;
21132193

21142194
// Fill both unstable features and server versions at once.
21152195
let mut supported_versions = supported.supported_versions();
@@ -3569,6 +3649,7 @@ pub(crate) mod tests {
35693649

35703650
let versions_mock = server
35713651
.mock_versions()
3652+
.expect_default_access_token()
35723653
.ok_with_unstable_features()
35733654
.named("first versions mock")
35743655
.expect(1)
@@ -3590,6 +3671,8 @@ pub(crate) mod tests {
35903671

35913672
assert!(client.server_versions().await.unwrap().contains(&MatrixVersion::V1_0));
35923673

3674+
// The result was cached.
3675+
assert_matches!(client.supported_versions_cached().await, Ok(Some(_)));
35933676
// This subsequent call hits the in-memory cache.
35943677
assert!(client.server_versions().await.unwrap().contains(&MatrixVersion::V1_0));
35953678

crates/matrix-sdk/src/http_client/mod.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use ruma::api::{
3838
use tokio::sync::{Semaphore, SemaphorePermit};
3939
use tracing::{debug, field::debug, instrument, trace};
4040

41-
use crate::{HttpResult, config::RequestConfig, error::HttpError};
41+
use crate::{HttpResult, client::caches::CachedValue, config::RequestConfig, error::HttpError};
4242

4343
#[cfg(not(target_family = "wasm"))]
4444
mod native;
@@ -263,7 +263,26 @@ impl SupportedPathBuilder for path_builder::VersionHistory {
263263
client: &crate::Client,
264264
skip_auth: bool,
265265
) -> HttpResult<Cow<'static, SupportedVersions>> {
266-
if skip_auth {
266+
// We always enable "failsafe" mode for the GET /versions requests in this
267+
// function. It disables trying to refresh the access token for those requests,
268+
// to avoid possible deadlocks.
269+
270+
if !client.auth_ctx().has_valid_access_token() {
271+
// Get the value in the cache without waiting. If the lock is not available, we
272+
// are in the middle of refreshing the cache so waiting for it would result in a
273+
// deadlock.
274+
if let Ok(CachedValue::Cached(versions)) =
275+
client.inner.caches.supported_versions.try_read().as_deref()
276+
{
277+
return Ok(Cow::Owned(versions.clone()));
278+
}
279+
280+
// The request will skip auth so we might not get all the supported features, so
281+
// just fetch the supported versions and don't cache them.
282+
let response = client.fetch_server_versions_inner(true, None).await?;
283+
284+
Ok(Cow::Owned(response.as_supported_versions()))
285+
} else if skip_auth {
267286
let cached_versions = client.get_cached_supported_versions().await;
268287

269288
let versions = if let Some(versions) = cached_versions {
@@ -272,14 +291,15 @@ impl SupportedPathBuilder for path_builder::VersionHistory {
272291
// If we're skipping auth we might not get all the supported features, so just
273292
// fetch the versions and don't cache them.
274293
let request_config = RequestConfig::default().retry_limit(5).skip_auth();
275-
let response = client.fetch_server_versions(Some(request_config)).await?;
294+
let response =
295+
client.fetch_server_versions_inner(true, Some(request_config)).await?;
276296

277297
response.as_supported_versions()
278298
};
279299

280300
Ok(Cow::Owned(versions))
281301
} else {
282-
client.supported_versions().await.map(Cow::Owned)
302+
client.supported_versions_inner(true).await.map(Cow::Owned)
283303
}
284304
}
285305
}

0 commit comments

Comments
 (0)