1use std::{
16 any::type_name,
17 borrow::Cow,
18 fmt::Debug,
19 num::NonZeroUsize,
20 sync::{
21 Arc,
22 atomic::{AtomicU64, Ordering},
23 },
24 time::Duration,
25};
26
27use bytes::{Bytes, BytesMut};
28use bytesize::ByteSize;
29use eyeball::SharedObservable;
30use http::Method;
31use matrix_sdk_base::SendOutsideWasm;
32use ruma::api::{
33 OutgoingRequest, OutgoingRequestExt, SupportedVersions,
34 auth_scheme::{self, AuthScheme, SendAccessToken},
35 error::{FromHttpResponseError, IntoHttpError},
36 path_builder,
37};
38use tokio::sync::{Semaphore, SemaphorePermit};
39use tracing::{Instrument, debug, error, field::debug, trace};
40
41use crate::{HttpResult, config::RequestConfig, error::HttpError};
42
43#[cfg(not(target_family = "wasm"))]
44mod native;
45#[cfg(target_family = "wasm")]
46mod wasm;
47
48#[cfg(not(target_family = "wasm"))]
49pub(crate) use native::HttpSettings;
50
51pub(crate) const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
52
53#[derive(Clone, Debug)]
54struct MaybeSemaphore(Arc<Option<Semaphore>>);
55
56#[allow(dead_code)] struct MaybeSemaphorePermit<'a>(Option<SemaphorePermit<'a>>);
58
59impl MaybeSemaphore {
60 fn new(max: Option<NonZeroUsize>) -> Self {
61 let inner = max.map(|i| Semaphore::new(i.into()));
62 MaybeSemaphore(Arc::new(inner))
63 }
64
65 async fn acquire(&self) -> MaybeSemaphorePermit<'_> {
66 match self.0.as_ref() {
67 Some(inner) => {
68 MaybeSemaphorePermit(inner.acquire().await.ok())
71 }
72 None => MaybeSemaphorePermit(None),
73 }
74 }
75}
76
77#[derive(Clone, Debug)]
78pub(crate) struct HttpClient {
79 pub(crate) inner: reqwest::Client,
80 pub(crate) request_config: RequestConfig,
81 concurrent_request_semaphore: MaybeSemaphore,
82 next_request_id: Arc<AtomicU64>,
83}
84
85impl HttpClient {
86 pub(crate) fn new(inner: reqwest::Client, request_config: RequestConfig) -> Self {
87 HttpClient {
88 inner,
89 request_config,
90 concurrent_request_semaphore: MaybeSemaphore::new(
91 request_config.max_concurrent_requests,
92 ),
93 next_request_id: AtomicU64::new(0).into(),
94 }
95 }
96
97 fn get_request_id(&self) -> String {
98 let request_id = self.next_request_id.fetch_add(1, Ordering::SeqCst);
99 format!("REQ-{request_id}")
100 }
101
102 fn serialize_request<R>(
103 &self,
104 request: R,
105 config: RequestConfig,
106 homeserver: String,
107 access_token: Option<&str>,
108 path_builder_input: <R::PathBuilder as path_builder::PathBuilder>::Input<'_>,
109 ) -> Result<http::Request<Bytes>, IntoHttpError>
110 where
111 R: OutgoingRequest + Debug,
112 R::Authentication: SupportedAuthScheme,
113 {
114 trace!(request_type = type_name::<R>(), "Serializing request");
115
116 let send_access_token = match access_token {
117 Some(access_token) => match (config.force_auth, config.skip_auth) {
118 (true, true) | (true, false) => SendAccessToken::Always(access_token),
119 (false, true) => SendAccessToken::None,
120 (false, false) => SendAccessToken::IfRequired(access_token),
121 },
122 None => SendAccessToken::None,
123 };
124 let authentication_input = R::Authentication::authentication_input(send_access_token);
125
126 let request = request
127 .try_into_http_request::<BytesMut>(
128 &homeserver,
129 authentication_input,
130 path_builder_input,
131 )?
132 .map(|body| body.freeze());
133
134 Ok(request)
135 }
136
137 pub fn send<R>(
138 &self,
139 request: R,
140 config: Option<RequestConfig>,
141 homeserver: String,
142 access_token: Option<&str>,
143 path_builder_input: <R::PathBuilder as path_builder::PathBuilder>::Input<'_>,
144 send_progress: SharedObservable<TransmissionProgress>,
145 ) -> impl Future<Output = Result<R::IncomingResponse, HttpError>>
146 where
147 R: OutgoingRequest + Debug,
148 R::Authentication: SupportedAuthScheme,
149 HttpError: From<FromHttpResponseError<R::EndpointError>>,
150 {
151 fn make_span(client: &HttpClient, config: &RequestConfig) -> tracing::Span {
154 tracing::info_span!(
155 "send",
156 uri = tracing::field::Empty,
157 ?config,
158 method = tracing::field::Empty,
159 request_id = client.get_request_id(),
160 request_size = tracing::field::Empty,
161 request_duration = tracing::field::Empty,
162 status = tracing::field::Empty,
163 response_size = tracing::field::Empty,
164 sentry_event_id = tracing::field::Empty
165 )
166 }
167 fn record_request_uri_and_size(request: &http::Request<Bytes>) {
168 let method = request.method();
169
170 let mut uri_parts = request.uri().clone().into_parts();
171
172 if let Some(path_and_query) = &mut uri_parts.path_and_query {
175 *path_and_query =
176 path_and_query.path().try_into().expect("path is valid PathAndQuery");
177 }
178
179 let uri = http::Uri::from_parts(uri_parts).expect("created from valid URI");
180
181 let span = tracing::Span::current();
182 span.record("method", debug(method)).record("uri", uri.to_string());
183
184 if [Method::POST, Method::PUT, Method::PATCH].contains(method) {
187 let request_size = request.body().len().try_into().unwrap_or(u64::MAX);
188 span.record(
189 "request_size",
190 ByteSize(request_size).display().si_short().to_string(),
191 );
192 }
193 }
194 fn log_got_response() {
197 debug!("Got response");
198 }
199 fn log_error(e: &HttpError) {
200 error!("Error while sending request: {e:?}");
201 }
202
203 let config = match config {
204 Some(config) => config,
205 None => self.request_config,
206 };
207
208 async move {
209 let request = self
210 .serialize_request(request, config, homeserver, access_token, path_builder_input)
211 .map_err(HttpError::IntoHttp)?;
212 record_request_uri_and_size(&request);
213
214 let _handle = self.concurrent_request_semaphore.acquire().await;
216
217 match Box::pin(self.send_request::<R>(request, config, send_progress)).await {
221 Ok(response) => {
222 log_got_response();
223 Ok(response)
224 }
225 Err(e) => {
226 log_error(&e);
227 Err(e)
228 }
229 }
230 }
231 .instrument(make_span(self, &config))
232 }
233}
234
235#[derive(Clone, Copy, Debug, Default)]
237pub struct TransmissionProgress {
238 pub current: usize,
240 pub total: usize,
242}
243
244async fn response_to_http_response(
245 mut response: reqwest::Response,
246) -> Result<http::Response<Bytes>, reqwest::Error> {
247 let status = response.status();
248
249 let mut http_builder = http::Response::builder().status(status);
250 let headers = http_builder.headers_mut().expect("Can't get the response builder headers");
251
252 for (k, v) in response.headers_mut().drain() {
253 if let Some(key) = k {
254 headers.insert(key, v);
255 }
256 }
257
258 let body = response.bytes().await?;
259
260 Ok(http_builder.body(body).expect("Can't construct a response using the given body"))
261}
262
263pub trait SupportedAuthScheme: AuthScheme {
268 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_>;
270}
271
272impl SupportedAuthScheme for auth_scheme::NoAccessToken {
273 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
274 access_token
275 }
276}
277
278impl SupportedAuthScheme for auth_scheme::AccessToken {
279 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
280 access_token
281 }
282}
283
284impl SupportedAuthScheme for auth_scheme::AccessTokenOptional {
285 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
286 access_token
287 }
288}
289
290impl SupportedAuthScheme for auth_scheme::AppserviceToken {
291 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
292 access_token
293 }
294}
295
296impl SupportedAuthScheme for auth_scheme::AppserviceTokenOptional {
297 fn authentication_input(access_token: SendAccessToken<'_>) -> Self::Input<'_> {
298 access_token
299 }
300}
301
302impl SupportedAuthScheme for auth_scheme::NoAuthentication {
303 fn authentication_input(_access_token: SendAccessToken<'_>) -> Self::Input<'_> {}
304}
305
306pub trait SupportedPathBuilder: path_builder::PathBuilder {
312 fn get_path_builder_input(
315 client: &crate::Client,
316 skip_auth: bool,
317 ) -> impl Future<Output = HttpResult<Self::Input<'static>>> + SendOutsideWasm;
318}
319
320impl SupportedPathBuilder for path_builder::VersionHistory {
321 async fn get_path_builder_input(
322 client: &crate::Client,
323 skip_auth: bool,
324 ) -> HttpResult<Cow<'static, SupportedVersions>> {
325 if !client.auth_ctx().has_valid_access_token() {
330 if let Ok(Some(versions)) = client.supported_versions_cached().await {
332 return Ok(Cow::Owned(versions));
333 }
334
335 let response = client.fetch_server_versions_inner(true, None).await?;
339
340 Ok(Cow::Owned(response.as_supported_versions()))
341 } else if skip_auth {
342 let cached_versions = client.supported_versions_cached().await;
343
344 let versions = if let Ok(Some(versions)) = cached_versions {
345 versions
346 } else {
347 let request_config = RequestConfig::default().retry_limit(5).skip_auth();
350 let response =
351 client.fetch_server_versions_inner(true, Some(request_config)).await?;
352
353 response.as_supported_versions()
354 };
355
356 Ok(Cow::Owned(versions))
357 } else {
358 client.supported_versions_inner(true).await.map(Cow::Owned)
359 }
360 }
361}
362
363impl SupportedPathBuilder for path_builder::SinglePath {
364 async fn get_path_builder_input(_client: &crate::Client, _skip_auth: bool) -> HttpResult<()> {
365 Ok(())
366 }
367}
368
369#[cfg(all(test, not(target_family = "wasm")))]
370mod tests {
371 use std::{
372 num::NonZeroUsize,
373 sync::{
374 Arc,
375 atomic::{AtomicU8, Ordering},
376 },
377 time::Duration,
378 };
379
380 use matrix_sdk_common::executor::spawn;
381 use matrix_sdk_test::{async_test, test_json};
382 use wiremock::{
383 Mock, Request, ResponseTemplate,
384 matchers::{method, path},
385 };
386
387 use crate::{
388 http_client::RequestConfig,
389 test_utils::{set_client_session, test_client_builder_with_server},
390 };
391
392 #[async_test]
393 async fn test_ensure_concurrent_request_limit_is_observed() {
394 let (client_builder, server) = test_client_builder_with_server().await;
395 let client = client_builder
396 .request_config(RequestConfig::default().max_concurrent_requests(NonZeroUsize::new(5)))
397 .build()
398 .await
399 .unwrap();
400
401 set_client_session(&client).await;
402
403 let counter = Arc::new(AtomicU8::new(0));
404 let inner_counter = counter.clone();
405
406 Mock::given(method("GET"))
407 .and(path("/_matrix/client/versions"))
408 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
409 .mount(&server)
410 .await;
411
412 Mock::given(method("GET"))
413 .and(path("_matrix/client/r0/account/whoami"))
414 .respond_with(move |_req: &Request| {
415 inner_counter.fetch_add(1, Ordering::SeqCst);
416 ResponseTemplate::new(200).set_delay(Duration::from_secs(60))
418 })
419 .mount(&server)
420 .await;
421
422 let bg_task = spawn(async move {
423 futures_util::future::join_all((0..10).map(|_| client.whoami())).await
424 });
425
426 tokio::time::sleep(Duration::from_millis(300)).await;
428
429 assert_eq!(
430 counter.load(Ordering::SeqCst),
431 5,
432 "More requests passed than the limit we configured"
433 );
434 bg_task.abort();
435 }
436
437 #[async_test]
438 async fn test_ensure_no_max_concurrent_request_does_not_limit() {
439 let (client_builder, server) = test_client_builder_with_server().await;
440 let client = client_builder
441 .request_config(RequestConfig::default().max_concurrent_requests(None))
442 .build()
443 .await
444 .unwrap();
445
446 set_client_session(&client).await;
447
448 let counter = Arc::new(AtomicU8::new(0));
449 let inner_counter = counter.clone();
450
451 Mock::given(method("GET"))
452 .and(path("/_matrix/client/versions"))
453 .respond_with(ResponseTemplate::new(200).set_body_json(&*test_json::VERSIONS))
454 .mount(&server)
455 .await;
456
457 Mock::given(method("GET"))
458 .and(path("_matrix/client/r0/account/whoami"))
459 .respond_with(move |_req: &Request| {
460 inner_counter.fetch_add(1, Ordering::SeqCst);
461 ResponseTemplate::new(200).set_delay(Duration::from_secs(60))
462 })
463 .mount(&server)
464 .await;
465
466 let bg_task = spawn(async move {
467 futures_util::future::join_all((0..254).map(|_| client.whoami())).await
468 });
469
470 tokio::time::sleep(Duration::from_secs(1)).await;
472
473 assert_eq!(counter.load(Ordering::SeqCst), 254, "Not all requests passed through");
474 bg_task.abort();
475 }
476}