Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/desktop/src-tauri/src/captions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod posthog;
mod presets;
mod recording;
mod recording_settings;
mod shared_client;
mod target_select_overlay;
mod thumbnails;
mod tray;
Expand Down
45 changes: 45 additions & 0 deletions apps/desktop/src-tauri/src/shared_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use reqwest::Client;
use std::sync::OnceLock;

/// Global shared HTTP client instance with retry policies
static SHARED_CLIENT: OnceLock<Client> = 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()
.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")
})
}
Comment on lines +11 to +37
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the reqwest::retry::for_all() API and consider error handling.

The retry configuration looks reasonable, but I'd like to verify the API usage:

  1. The reqwest::retry::for_all() call on line 15 should be confirmed to exist in the version of reqwest being used. Standard reqwest doesn't include a built-in retry module by default—this typically requires the reqwest-retry crate or similar middleware.

  2. The .expect() on line 35 will panic if client creation fails during initialization. While this may be acceptable for a critical shared resource, consider whether a more graceful startup failure would be appropriate.

Run the following script to verify the reqwest retry API:

Also, please verify the retry API documentation:


🏁 Script executed:

#!/bin/bash
# Check Cargo.toml for reqwest and retry-related dependencies
rg -n "reqwest" apps/desktop/src-tauri/Cargo.toml -A 3

Length of output: 361


🌐 Web query:

reqwest retry for_all method documentation

💡 Result:

Do you mean the built-in reqwest retry API (reqwest::retry) or the reqwest-retry middleware (crate reqwest-retry)? Which crate and version are you using?

For reference:

Reply with which one (and the version) and I’ll find the exact for_all documentation or show how to use it.


Excellent! I found critical information. The retry module was added to reqwest via PR #2763, which includes a new ClientBuilder::retry() method and a reqwest::retry module to build a retry policy, with a for_host() function and classify_fn() method. However, I need to verify when this was released and whether it's in reqwest 0.12.7.Perfect! I found it. As of version 0.12.23, reqwest includes automatic retry functionality - which means reqwest 0.12.7 does NOT have the built-in retry module. The PR #2763 added it, but it was only released in 0.12.23 or later.

Let me verify the exact API by checking if for_all() is the correct method name:

Looking at the search results, I found a critical issue. The code uses reqwest::retry::for_all(), but the built-in retry functionality was only added to reqwest starting in version 0.12.23, and the version specified in Cargo.toml is 0.12.7. Additionally, the API examples show for_host(), not for_all(). Let me check the actual imports in the source file to be certain:Based on my research, I now have sufficient evidence to write the rewritten review comment. The critical issue is that reqwest's automatic retry functionality was only added in version 0.12.23, but the codebase uses reqwest 0.12.7. The code calls reqwest::retry::for_all(), which does not exist in that version and would not compile.

Code will not compile with reqwest 0.12.7—retry module is unavailable.

The code uses reqwest::retry::for_all() on line 15, but automatic retry functionality was added to reqwest only in version 0.12.23. The Cargo.toml specifies reqwest 0.12.7, which does not include the reqwest::retry module.

Upgrade reqwest to at least 0.12.23 to enable the built-in retry API. Additionally, verify that the for_all() method is the correct API (the PR documentation references for_host() for host-specific retry policies).

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/shared_client.rs around lines 11 to 37, the code
calls reqwest::retry::for_all() but the project’s Cargo.toml pins reqwest to
0.12.7 which does not include the retry module (added in >=0.12.23) and may use
a different API (examples show for_host()). Fix by updating Cargo.toml to
reqwest >=0.12.23 (or latest 0.12.x that includes retry), run cargo update, and
then verify/adjust the retry call to the correct API (e.g., use for_host() or
the actual factory function provided by that reqwest version) so the code
compiles.


/// 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> {
Ok(get_shared_client())
}
Comment on lines +39 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Misleading API: unused parameter and infallible Result type.

The function signature has two issues that make the API confusing:

  1. The _host parameter is completely unused (indicated by the underscore prefix). If the host information isn't needed, this parameter should be removed.

  2. The function returns Result<&'static Client, reqwest::Error> but can never return an Err variant—it always returns Ok(get_shared_client()). This is misleading to callers who might write error-handling code that will never execute.

Consider one of these approaches:

Option 1 (Recommended): Simplify the API

If the host parameter truly isn't needed, remove it and change the return type:

-pub fn get_retryable_client(_host: String) -> Result<&'static Client, reqwest::Error> {
-    Ok(get_shared_client())
+pub fn get_retryable_client() -> &'static Client {
+    get_shared_client()
 }

Then update callers in upload.rs to not pass the host parameter.

Option 2: Remove the wrapper entirely

If get_retryable_client is just an alias for get_shared_client, consider removing it and having callers use get_shared_client() directly.

Option 3: Document the intent

If the signature is intentionally kept for future extensibility or API stability, add a comment explaining why the parameter is unused and why a Result is returned.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/desktop/src-tauri/src/shared_client.rs around lines 39–45, the function
get_retryable_client currently accepts an unused _host: String and returns a
Result that is never Err; remove the unused parameter and simplify the signature
to return &'static Client (no Result), i.e. pub fn get_retryable_client() ->
&'static Client { get_shared_client() }, then update all callers (e.g., in
upload.rs) to stop passing a host and to handle the direct client return;
alternatively if you prefer removing the wrapper, replace calls to
get_retryable_client() with get_shared_client() instead.

52 changes: 20 additions & 32 deletions apps/desktop/src-tauri/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
UploadProgress, VideoUploadInfo,
api::{self, PresignedS3PutRequest, PresignedS3PutRequestMethod, S3VideoMeta, UploadedPart},
posthog::{PostHogEvent, async_capture_event},
shared_client::get_retryable_client,
web_api::{AuthedApiError, ManagerExt},
};
use async_stream::{stream, try_stream};
Expand Down Expand Up @@ -596,25 +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.
///
/// Note: It's on the caller to ensure the chunks are sized correctly within S3 limits.
Expand Down Expand Up @@ -729,16 +711,18 @@ fn multipart_uploader(
let url = Uri::from_str(&presigned_url).map_err(|err| {
format!("uploader/part/{part_number}/invalid_url: {err:?}")
})?;
let mut req =
retryable_client(url.host().unwrap_or("<unknown>").to_string())
.build()
.map_err(|err| {
format!("uploader/part/{part_number}/client: {err:?}")
})?
.put(&presigned_url)
.header("Content-Length", chunk.len())
.timeout(Duration::from_secs(5 * 60))
.body(chunk);
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))
.body(chunk);

if let Some(md5_sum) = &md5_sum {
req = req.header("Content-MD5", md5_sum);
Expand Down Expand Up @@ -813,9 +797,13 @@ 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("<unknown>").to_string())
.build()
.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))
Expand Down
15 changes: 8 additions & 7 deletions apps/desktop/src-tauri/src/web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use tracing::{error, warn};
use crate::{
ArcLock,
auth::{AuthSecret, AuthStore},
shared_client::get_shared_client,
};

#[derive(Error, Debug)]
Expand Down Expand Up @@ -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<reqwest::Response, reqwest::Error> {
let client = reqwest::Client::new();
let client = get_shared_client();

let req = build(client, url).header(
"Authorization",
Expand All @@ -82,13 +83,13 @@ pub trait ManagerExt<R: Runtime>: Manager<R> {
async fn authed_api_request(
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, AuthedApiError>;

async fn api_request(
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, reqwest::Error>;

async fn make_app_url(&self, pathname: impl AsRef<str>) -> String;
Expand All @@ -100,7 +101,7 @@ impl<T: Manager<R> + Emitter<R>, R: Runtime> ManagerExt<R> for T {
async fn authed_api_request(
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, AuthedApiError> {
let Some(auth) = AuthStore::get(self.app_handle()).map_err(AuthedApiError::AuthStore)?
else {
Expand All @@ -122,10 +123,10 @@ impl<T: Manager<R> + Emitter<R>, R: Runtime> ManagerExt<R> for T {
async fn api_request(
&self,
path: impl Into<String>,
build: impl FnOnce(reqwest::Client, String) -> reqwest::RequestBuilder,
build: impl FnOnce(&reqwest::Client, String) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, reqwest::Error> {
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
}
Expand Down
8 changes: 4 additions & 4 deletions packages/ui-solid/src/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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']
Expand All @@ -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']
Expand All @@ -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']
Expand Down