From 108ba60996901596cde096779cfec66ba83f7b15 Mon Sep 17 00:00:00 2001 From: Viraj Bhartiya Date: Thu, 23 Oct 2025 17:06:11 +0530 Subject: [PATCH 1/4] refactor: replace direct client instantiation with shared client in multiple modules --- apps/desktop/src-tauri/src/captions.rs | 4 +- apps/desktop/src-tauri/src/lib.rs | 1 + apps/desktop/src-tauri/src/shared_client.rs | 52 +++++++++++++++++++++ apps/desktop/src-tauri/src/upload.rs | 25 ++-------- apps/desktop/src-tauri/src/web_api.rs | 15 +++--- packages/ui-solid/src/auto-imports.d.ts | 8 ++-- 6 files changed, 71 insertions(+), 34 deletions(-) create mode 100644 apps/desktop/src-tauri/src/shared_client.rs diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs index 88e4ec457c..e191ee07cd 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-tauri/src/captions.rs @@ -19,6 +19,8 @@ use tokio::sync::Mutex; use tracing::instrument; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; +use crate::shared_client::get_shared_client; + // Re-export caption types from cap_project pub use cap_project::{CaptionSegment, CaptionSettings}; @@ -1067,7 +1069,7 @@ pub async fn download_whisper_model( }; // Create the client and download the model - let client = Client::new(); + let client = get_shared_client(); let response = client .get(model_url) .send() diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 2a0eb78225..a881ba5f50 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -21,6 +21,7 @@ mod posthog; mod presets; mod recording; mod recording_settings; +mod shared_client; mod target_select_overlay; mod thumbnails; mod tray; diff --git a/apps/desktop/src-tauri/src/shared_client.rs b/apps/desktop/src-tauri/src/shared_client.rs new file mode 100644 index 0000000000..52bc3d3300 --- /dev/null +++ b/apps/desktop/src-tauri/src/shared_client.rs @@ -0,0 +1,52 @@ +use reqwest::Client; +use std::sync::OnceLock; + +/// Global shared HTTP client instance +static SHARED_CLIENT: OnceLock = OnceLock::new(); + +/// Get the shared HTTP client instance +/// +/// This client is configured with retry policies and is shared across the entire application. +/// This allows for global tracking of requests to each domain for DOS protection. +pub fn get_shared_client() -> &'static Client { + SHARED_CLIENT.get_or_init(|| { + Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .expect("Failed to create shared HTTP client") + }) +} + +/// Get a retryable client builder for specific hosts +/// +/// This function creates a client builder with retry policies configured for the given host. +/// The retry policies are designed to handle server errors and network issues while providing +/// DOS protection through global request tracking. +pub fn get_retryable_client_builder(host: String) -> reqwest::ClientBuilder { + reqwest::Client::builder() + .retry( + reqwest::retry::for_host(host) + .classify_fn(|req_rep| { + match req_rep.status() { + // Server errors and rate limiting + Some(s) if s.is_server_error() || s == reqwest::StatusCode::TOO_MANY_REQUESTS => { + req_rep.retryable() + } + // Network errors + None => req_rep.retryable(), + _ => req_rep.success(), + } + }) + .max_retries_per_request(5) + .max_extra_load(5.0), + ) + .timeout(std::time::Duration::from_secs(30)) +} + +/// Get a retryable client for specific hosts +/// +/// This function creates a client with retry policies configured for the given host. +/// It's a convenience function that builds the client immediately. +pub fn get_retryable_client(host: String) -> Result { + get_retryable_client_builder(host).build() +} diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index d27d1246bc..affb61db84 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -4,6 +4,7 @@ use crate::{ UploadProgress, VideoUploadInfo, api::{self, PresignedS3PutRequest, PresignedS3PutRequestMethod, S3VideoMeta, UploadedPart}, posthog::{PostHogEvent, async_capture_event}, + shared_client::{get_retryable_client, get_retryable_client_builder}, web_api::{AuthedApiError, ManagerExt}, }; use async_stream::{stream, try_stream}; @@ -596,24 +597,6 @@ pub fn from_pending_file_to_chunks( .instrument(Span::current()) } -fn retryable_client(host: String) -> reqwest::ClientBuilder { - reqwest::Client::builder().retry( - reqwest::retry::for_host(host) - .classify_fn(|req_rep| { - match req_rep.status() { - // Server errors - Some(s) if s.is_server_error() || s == StatusCode::TOO_MANY_REQUESTS => { - req_rep.retryable() - } - // Network errors - None => req_rep.retryable(), - _ => req_rep.success(), - } - }) - .max_retries_per_request(5) - .max_extra_load(5.0), - ) -} /// Takes an incoming stream of bytes and individually uploads them to S3. /// @@ -730,8 +713,7 @@ fn multipart_uploader( format!("uploader/part/{part_number}/invalid_url: {err:?}") })?; let mut req = - retryable_client(url.host().unwrap_or("").to_string()) - .build() + get_retryable_client(url.host().unwrap_or("").to_string()) .map_err(|err| { format!("uploader/part/{part_number}/client: {err:?}") })? @@ -813,8 +795,7 @@ pub async fn singlepart_uploader( let url = Uri::from_str(&presigned_url) .map_err(|err| format!("singlepart_uploader/invalid_url: {err:?}"))?; - let resp = retryable_client(url.host().unwrap_or("").to_string()) - .build() + let resp = get_retryable_client(url.host().unwrap_or("").to_string()) .map_err(|err| format!("singlepart_uploader/client: {err:?}"))? .put(&presigned_url) .header("Content-Length", total_size) diff --git a/apps/desktop/src-tauri/src/web_api.rs b/apps/desktop/src-tauri/src/web_api.rs index bdc6b405b4..c7099f4ca0 100644 --- a/apps/desktop/src-tauri/src/web_api.rs +++ b/apps/desktop/src-tauri/src/web_api.rs @@ -6,6 +6,7 @@ use tracing::{error, warn}; use crate::{ ArcLock, auth::{AuthSecret, AuthStore}, + shared_client::get_shared_client, }; #[derive(Error, Debug)] @@ -59,10 +60,10 @@ fn apply_env_headers(req: reqwest::RequestBuilder) -> reqwest::RequestBuilder { async fn do_authed_request( auth: &AuthStore, - build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, + build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, url: String, ) -> Result { - let client = reqwest::Client::new(); + let client = get_shared_client(); let req = build(client, url).header( "Authorization", @@ -82,13 +83,13 @@ pub trait ManagerExt: Manager { async fn authed_api_request( &self, path: impl Into, - build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, + build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, ) -> Result; async fn api_request( &self, path: impl Into, - build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, + build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, ) -> Result; async fn make_app_url(&self, pathname: impl AsRef) -> String; @@ -100,7 +101,7 @@ impl + Emitter, R: Runtime> ManagerExt for T { async fn authed_api_request( &self, path: impl Into, - build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, + build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, ) -> Result { let Some(auth) = AuthStore::get(self.app_handle()).map_err(AuthedApiError::AuthStore)? else { @@ -122,10 +123,10 @@ impl + Emitter, R: Runtime> ManagerExt for T { async fn api_request( &self, path: impl Into, - build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder, + build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder, ) -> Result { let url = self.make_app_url(path.into()).await; - let client = reqwest::Client::new(); + let client = get_shared_client(); apply_env_headers(build(client, url)).send().await } diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index db21f744e1..eca65fb570 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -22,7 +22,7 @@ declare global { const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] - const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"] + const IconCapEditor: typeof import('~icons/cap/editor.jsx')['default'] const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] const IconCapFile: typeof import('~icons/cap/file.jsx')['default'] const IconCapFilmCut: typeof import('~icons/cap/film-cut.jsx')['default'] @@ -57,7 +57,7 @@ declare global { const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] - const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"] + const IconCapUpload: typeof import('~icons/cap/upload.jsx')['default'] const IconCapX: typeof import('~icons/cap/x.jsx')['default'] const IconCapZoomIn: typeof import('~icons/cap/zoom-in.jsx')['default'] const IconCapZoomOut: typeof import('~icons/cap/zoom-out.jsx')['default'] @@ -69,7 +69,7 @@ declare global { const IconLucideClock: typeof import('~icons/lucide/clock.jsx')['default'] const IconLucideDatabase: typeof import('~icons/lucide/database.jsx')['default'] const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] - const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"] + const IconLucideEye: typeof import('~icons/lucide/eye.jsx')['default'] const IconLucideEyeOff: typeof import('~icons/lucide/eye-off.jsx')['default'] const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] const IconLucideGift: typeof import('~icons/lucide/gift.jsx')['default'] @@ -88,7 +88,7 @@ declare global { const IconLucideUnplug: typeof import('~icons/lucide/unplug.jsx')['default'] const IconLucideVideo: typeof import('~icons/lucide/video.jsx')['default'] const IconLucideVolume2: typeof import('~icons/lucide/volume2.jsx')['default'] - const IconLucideVolumeX: typeof import("~icons/lucide/volume-x.jsx")["default"] + const IconLucideVolumeX: typeof import('~icons/lucide/volume-x.jsx')['default'] const IconMaterialSymbolsScreenshotFrame2Rounded: typeof import('~icons/material-symbols/screenshot-frame2-rounded.jsx')['default'] const IconMdiLoading: typeof import("~icons/mdi/loading.jsx")["default"] const IconMdiMonitor: typeof import('~icons/mdi/monitor.jsx')['default'] From d2b4c4af22b74e5e2ec18408a0e0fc21c8911d73 Mon Sep 17 00:00:00 2001 From: Viraj Bhartiya Date: Thu, 23 Oct 2025 18:12:56 +0530 Subject: [PATCH 2/4] refactor: enhance shared HTTP client with retry policies and update upload functions to utilize it --- apps/desktop/src-tauri/src/shared_client.rs | 52 +++++++++------------ apps/desktop/src-tauri/src/upload.rs | 21 ++++++--- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src-tauri/src/shared_client.rs b/apps/desktop/src-tauri/src/shared_client.rs index 52bc3d3300..46a942a5d9 100644 --- a/apps/desktop/src-tauri/src/shared_client.rs +++ b/apps/desktop/src-tauri/src/shared_client.rs @@ -1,7 +1,7 @@ use reqwest::Client; use std::sync::OnceLock; -/// Global shared HTTP client instance +/// Global shared HTTP client instance with retry policies static SHARED_CLIENT: OnceLock = OnceLock::new(); /// Get the shared HTTP client instance @@ -11,42 +11,32 @@ static SHARED_CLIENT: OnceLock = OnceLock::new(); pub fn get_shared_client() -> &'static Client { SHARED_CLIENT.get_or_init(|| { Client::builder() + .retry( + reqwest::retry::for_all_hosts() + .classify_fn(|req_rep| { + match req_rep.status() { + // Server errors and rate limiting + Some(s) if s.is_server_error() || s == reqwest::StatusCode::TOO_MANY_REQUESTS => { + req_rep.retryable() + } + // Network errors + None => req_rep.retryable(), + _ => req_rep.success(), + } + }) + .max_retries_per_request(5) + .max_extra_load(5.0), + ) .timeout(std::time::Duration::from_secs(30)) .build() .expect("Failed to create shared HTTP client") }) } -/// Get a retryable client builder for specific hosts -/// -/// This function creates a client builder with retry policies configured for the given host. -/// The retry policies are designed to handle server errors and network issues while providing -/// DOS protection through global request tracking. -pub fn get_retryable_client_builder(host: String) -> reqwest::ClientBuilder { - reqwest::Client::builder() - .retry( - reqwest::retry::for_host(host) - .classify_fn(|req_rep| { - match req_rep.status() { - // Server errors and rate limiting - Some(s) if s.is_server_error() || s == reqwest::StatusCode::TOO_MANY_REQUESTS => { - req_rep.retryable() - } - // Network errors - None => req_rep.retryable(), - _ => req_rep.success(), - } - }) - .max_retries_per_request(5) - .max_extra_load(5.0), - ) - .timeout(std::time::Duration::from_secs(30)) -} - /// Get a retryable client for specific hosts /// -/// This function creates a client with retry policies configured for the given host. -/// It's a convenience function that builds the client immediately. -pub fn get_retryable_client(host: String) -> Result { - get_retryable_client_builder(host).build() +/// This function returns the shared client which has global retry tracking. +/// All requests use the same client instance for consistent DOS protection. +pub fn get_retryable_client(_host: String) -> Result<&'static Client, reqwest::Error> { + Ok(get_shared_client()) } diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index affb61db84..6ba58fa959 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -712,11 +712,14 @@ fn multipart_uploader( let url = Uri::from_str(&presigned_url).map_err(|err| { format!("uploader/part/{part_number}/invalid_url: {err:?}") })?; - let mut req = - get_retryable_client(url.host().unwrap_or("").to_string()) - .map_err(|err| { - format!("uploader/part/{part_number}/client: {err:?}") - })? + let host = url.host() + .ok_or_else(|| format!("uploader/part/{part_number}/missing_host"))? + .to_string(); + let client = get_retryable_client(host) + .map_err(|err| { + format!("uploader/part/{part_number}/client: {err:?}") + })?; + let mut req = client .put(&presigned_url) .header("Content-Length", chunk.len()) .timeout(Duration::from_secs(5 * 60)) @@ -795,8 +798,12 @@ pub async fn singlepart_uploader( let url = Uri::from_str(&presigned_url) .map_err(|err| format!("singlepart_uploader/invalid_url: {err:?}"))?; - let resp = get_retryable_client(url.host().unwrap_or("").to_string()) - .map_err(|err| format!("singlepart_uploader/client: {err:?}"))? + let host = url.host() + .ok_or_else(|| "singlepart_uploader/missing_host".to_string())? + .to_string(); + let client = get_retryable_client(host) + .map_err(|err| format!("singlepart_uploader/client: {err:?}"))?; + let resp = client .put(&presigned_url) .header("Content-Length", total_size) .body(reqwest::Body::wrap_stream(stream)) From a1e3dea344ad141878e4b44a07e053415844158a Mon Sep 17 00:00:00 2001 From: Viraj Bhartiya Date: Thu, 23 Oct 2025 18:18:24 +0530 Subject: [PATCH 3/4] refactor: simplify shared client usage in upload module by removing redundant builder function --- apps/desktop/src-tauri/src/upload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 6ba58fa959..dc2ac42138 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -4,7 +4,7 @@ use crate::{ UploadProgress, VideoUploadInfo, api::{self, PresignedS3PutRequest, PresignedS3PutRequestMethod, S3VideoMeta, UploadedPart}, posthog::{PostHogEvent, async_capture_event}, - shared_client::{get_retryable_client, get_retryable_client_builder}, + shared_client::get_retryable_client, web_api::{AuthedApiError, ManagerExt}, }; use async_stream::{stream, try_stream}; From d3ea053c13bee4c605406338eaa07784250c7d0e Mon Sep 17 00:00:00 2001 From: Viraj Bhartiya Date: Tue, 28 Oct 2025 11:34:11 +0530 Subject: [PATCH 4/4] refactor: clean up shared client implementation and improve readability in upload module --- apps/desktop/src-tauri/src/shared_client.rs | 11 +++++---- apps/desktop/src-tauri/src/upload.rs | 26 ++++++++++----------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src-tauri/src/shared_client.rs b/apps/desktop/src-tauri/src/shared_client.rs index 46a942a5d9..ea5bfa5a93 100644 --- a/apps/desktop/src-tauri/src/shared_client.rs +++ b/apps/desktop/src-tauri/src/shared_client.rs @@ -5,18 +5,21 @@ use std::sync::OnceLock; static SHARED_CLIENT: OnceLock = OnceLock::new(); /// Get the shared HTTP client instance -/// +/// /// This client is configured with retry policies and is shared across the entire application. /// This allows for global tracking of requests to each domain for DOS protection. pub fn get_shared_client() -> &'static Client { SHARED_CLIENT.get_or_init(|| { Client::builder() .retry( - reqwest::retry::for_all_hosts() + reqwest::retry::for_all() .classify_fn(|req_rep| { match req_rep.status() { // Server errors and rate limiting - Some(s) if s.is_server_error() || s == reqwest::StatusCode::TOO_MANY_REQUESTS => { + Some(s) + if s.is_server_error() + || s == reqwest::StatusCode::TOO_MANY_REQUESTS => + { req_rep.retryable() } // Network errors @@ -34,7 +37,7 @@ pub fn get_shared_client() -> &'static Client { } /// Get a retryable client for specific hosts -/// +/// /// This function returns the shared client which has global retry tracking. /// All requests use the same client instance for consistent DOS protection. pub fn get_retryable_client(_host: String) -> Result<&'static Client, reqwest::Error> { diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index dc2ac42138..19c09fadf6 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -597,7 +597,6 @@ pub fn from_pending_file_to_chunks( .instrument(Span::current()) } - /// Takes an incoming stream of bytes and individually uploads them to S3. /// /// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits. @@ -712,18 +711,18 @@ fn multipart_uploader( let url = Uri::from_str(&presigned_url).map_err(|err| { format!("uploader/part/{part_number}/invalid_url: {err:?}") })?; - let host = url.host() + let host = url + .host() .ok_or_else(|| format!("uploader/part/{part_number}/missing_host"))? .to_string(); - let client = get_retryable_client(host) - .map_err(|err| { - format!("uploader/part/{part_number}/client: {err:?}") - })?; + let client = get_retryable_client(host).map_err(|err| { + format!("uploader/part/{part_number}/client: {err:?}") + })?; let mut req = client - .put(&presigned_url) - .header("Content-Length", chunk.len()) - .timeout(Duration::from_secs(5 * 60)) - .body(chunk); + .put(&presigned_url) + .header("Content-Length", chunk.len()) + .timeout(Duration::from_secs(5 * 60)) + .body(chunk); if let Some(md5_sum) = &md5_sum { req = req.header("Content-MD5", md5_sum); @@ -798,11 +797,12 @@ pub async fn singlepart_uploader( let url = Uri::from_str(&presigned_url) .map_err(|err| format!("singlepart_uploader/invalid_url: {err:?}"))?; - let host = url.host() + let host = url + .host() .ok_or_else(|| "singlepart_uploader/missing_host".to_string())? .to_string(); - let client = get_retryable_client(host) - .map_err(|err| format!("singlepart_uploader/client: {err:?}"))?; + let client = + get_retryable_client(host).map_err(|err| format!("singlepart_uploader/client: {err:?}"))?; let resp = client .put(&presigned_url) .header("Content-Length", total_size)