From a5730c9017083e66c72db333bec76e80aa1d8673 Mon Sep 17 00:00:00 2001 From: deteam Date: Fri, 10 Apr 2026 16:09:23 +0200 Subject: [PATCH 1/5] fix: async loading of resources for tauri protocol --- crates/tauri/src/protocol/tauri.rs | 133 +++++++++++++++++++---------- 1 file changed, 87 insertions(+), 46 deletions(-) diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index a346754f09d5..13c907dc0384 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -42,34 +42,48 @@ pub fn get( }; let window_origin = window_origin.to_string(); + let web_resource_request_handler = web_resource_request_handler.map(Arc::new); #[cfg(all(dev, mobile))] - let response_cache = Arc::new(Mutex::new(HashMap::new())); + let response_cache = Arc::new(Mutex::new(HashMap::::new())); Box::new(move |_, request, responder| { - match get_response( - request, - &manager, - &window_origin, - web_resource_request_handler.as_deref(), - #[cfg(all(dev, mobile))] - (&url, &response_cache), - ) { - Ok(response) => responder.respond(response), - Err(e) => responder.respond( - HttpResponse::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) - .header("Access-Control-Allow-Origin", &window_origin) - .body(e.to_string().into_bytes()) - .unwrap(), - ), - } + let manager = manager.clone(); + let window_origin = window_origin.clone(); + let web_resource_request_handler = web_resource_request_handler.clone(); + + #[cfg(all(dev, mobile))] + let url = url.clone(); + #[cfg(all(dev, mobile))] + let response_cache = response_cache.clone(); + + crate::async_runtime::spawn(async move { + match get_response( + request, + &manager, + &window_origin, + web_resource_request_handler.as_deref().map(|h| &**h), + #[cfg(all(dev, mobile))] + (&url, &response_cache), + ) + .await + { + Ok(response) => responder.respond(response), + Err(e) => responder.respond( + HttpResponse::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) + .header("Access-Control-Allow-Origin", &window_origin) + .body(e.to_string().into_bytes()) + .unwrap(), + ), + } + }); }) } -fn get_response( - #[allow(unused_mut)] mut request: Request>, +async fn get_response( + request: Request>, #[allow(unused_variables)] manager: &AppManager, window_origin: &str, web_resource_request_handler: Option<&WebResourceRequestHandler>, @@ -119,10 +133,12 @@ fn get_response( let _ = rustls::crypto::ring::default_provider().install_default(); } + #[allow(unused_mut)] let mut client = reqwest::ClientBuilder::new(); if url.starts_with("https://") { // we can't load env vars at runtime, gotta embed them in the lib + #[allow(unused_variables)] if let Some(cert_pem) = option_env!("TAURI_DEV_ROOT_CERTIFICATE") { #[cfg(any( feature = "native-tls", @@ -157,39 +173,61 @@ fn get_response( .build() .unwrap() .request(request.method().clone(), &url); - proxy_builder = proxy_builder.body(std::mem::take(request.body_mut())); for (name, value) in request.headers() { proxy_builder = proxy_builder.header(name, value); } proxy_builder = proxy_builder.body(request.body().clone()); - match crate::async_runtime::safe_block_on(proxy_builder.send()) { - Ok(r) => { - let mut response_cache_ = response_cache.lock().unwrap(); - let mut response = None; - if r.status() == http::StatusCode::NOT_MODIFIED { - response = response_cache_.get(&url); - } - let response = if let Some(r) = response { - r + + match async { + let r = proxy_builder.send().await?; + let status = r.status(); + let headers = r.headers().clone(); + + Ok::<_, reqwest::Error>(if status == http::StatusCode::NOT_MODIFIED { + if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.to_vec().into()) + .unwrap() } else { - let status = r.status(); - let headers = r.headers().clone(); - let body = crate::async_runtime::safe_block_on(r.bytes())?; - let response = CachedResponse { - status, - headers, - body, - }; - response_cache_.insert(url.clone(), response); - response_cache_.get(&url).unwrap() + for (name, value) in &headers { + builder = builder.header(name, value); + } + + builder.status(status).body(Vec::new().into()).unwrap() + } + } else { + let body = r.bytes().await?; + let response = CachedResponse { + status, + headers, + body, }; + + { + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + } + for (name, value) in &response.headers { builder = builder.header(name, value); } + builder .status(response.status) - .body(response.body.to_vec().into())? - } + .body(response.body.to_vec().into()) + .unwrap() + }) + } + .await + { + Ok(response) => response, Err(e) => { let error_message = format!( "Failed to request {}: {}{}", @@ -211,15 +249,18 @@ fn get_response( #[cfg(not(all(dev, mobile)))] let mut response = { - let use_https_scheme = request.uri().scheme() == Some(&http::uri::Scheme::HTTPS); - let asset = manager.get_asset(path, use_https_scheme)?; + let asset = manager.get_asset( + path, + request.uri().scheme() == Some(&http::uri::Scheme::HTTPS), + )?; builder = builder.header(CONTENT_TYPE, &asset.mime_type); if let Some(csp) = &asset.csp_header { builder = builder.header("Content-Security-Policy", csp); } builder.body(asset.bytes.into())? }; - if let Some(handler) = &web_resource_request_handler { + + if let Some(handler) = web_resource_request_handler { handler(request, &mut response); } From 3243ef1ef07634a22b5732f155b65b6ab991201d Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 1 Jun 2026 13:10:15 +0800 Subject: [PATCH 2/5] Move things under one Arc --- crates/tauri/src/protocol/tauri.rs | 68 ++++++++++++++++++------------ 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index 6804bddaa87a..29b718cb9d88 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -25,12 +25,11 @@ struct CachedResponse { } pub fn get( - #[allow(unused_variables)] manager: Arc>, + manager: Arc>, window_origin: &str, web_resource_request_handler: Option>, ) -> UriSchemeProtocolHandler { let window_origin = window_origin.to_string(); - let web_resource_request_handler = web_resource_request_handler.map(Arc::new); #[cfg(all(dev, mobile))] let (url, client, response_cache) = { @@ -86,31 +85,28 @@ pub fn get( (url, client, response_cache) }; - Box::new(move |_, request, responder| { - let manager = manager.clone(); - let window_origin = window_origin.clone(); - let web_resource_request_handler = web_resource_request_handler.clone(); - + let context = Arc::new(Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, #[cfg(all(dev, mobile))] - let (url, client, response_cache) = (url.clone(), client.clone(), response_cache.clone()); + url, + #[cfg(all(dev, mobile))] + response_cache, + }); + Box::new(move |_, request, responder| { + let context = context.clone(); crate::async_runtime::spawn(async move { - match get_response( - request, - &manager, - &window_origin, - web_resource_request_handler.as_deref().map(|h| &**h), - #[cfg(all(dev, mobile))] - (&url, &client, &response_cache), - ) - .await - { + match get_response(&context, request).await { Ok(response) => responder.respond(response), Err(e) => responder.respond( HttpResponse::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header(CONTENT_TYPE, mime::TEXT_PLAIN.essence_str()) - .header("Access-Control-Allow-Origin", &window_origin) + .header("Access-Control-Allow-Origin", &context.window_origin) .body(e.to_string().into_bytes()) .unwrap(), ), @@ -119,17 +115,35 @@ pub fn get( }) } +struct Context { + manager: Arc>, + window_origin: String, + web_resource_request_handler: Option>, + + #[cfg(all(dev, mobile))] + url: String, + #[cfg(all(dev, mobile))] + client: reqwest::Client, + #[cfg(all(dev, mobile))] + response_cache: Arc>>, +} + async fn get_response( + context: &Context, request: Request>, - #[allow(unused_variables)] manager: &AppManager, - window_origin: &str, - web_resource_request_handler: Option<&WebResourceRequestHandler>, - #[cfg(all(dev, mobile))] (url, client, response_cache): ( - &str, - &reqwest::Client, - &Arc>>, - ), ) -> Result>, Box> { + let Context { + manager, + web_resource_request_handler, + window_origin, + #[cfg(all(dev, mobile))] + client, + #[cfg(all(dev, mobile))] + url, + #[cfg(all(dev, mobile))] + response_cache, + } = context; + // use the entire URI as we are going to proxy the request let path = if PROXY_DEV_SERVER { request.uri().to_string() From 3f5ae779664b99b79f7527b9d3b097c68d5c45d0 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 1 Jun 2026 13:26:03 +0800 Subject: [PATCH 3/5] Move out to `proxy_dev_request` --- crates/tauri/src/protocol/tauri.rs | 175 +++++++++++++++-------------- 1 file changed, 91 insertions(+), 84 deletions(-) diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index 29b718cb9d88..0da8b195b946 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -165,95 +165,14 @@ async fn get_response( // where `$P` is not `localhost/*` .unwrap_or_default(); + #[allow(unused_mut)] let mut builder = HttpResponse::builder() .add_configured_headers(manager.config.app.security.headers.as_ref()) .header("Access-Control-Allow-Origin", window_origin); #[cfg(all(dev, mobile))] - let mut response = { - let decoded_path = percent_encoding::percent_decode(path.as_bytes()) - .decode_utf8_lossy() - .to_string(); - let url = format!( - "{}/{}", - url.trim_end_matches('/'), - decoded_path.trim_start_matches('/') - ); - - let mut proxy_builder = client.request(request.method().clone(), &url); - for (name, value) in request.headers() { - proxy_builder = proxy_builder.header(name, value); - } - proxy_builder = proxy_builder.body(request.body().clone()); - - match async { - let r = proxy_builder.send().await?; - let status = r.status(); - let headers = r.headers().clone(); - - Ok::<_, reqwest::Error>(if status == http::StatusCode::NOT_MODIFIED { - if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { - for (name, value) in &response.headers { - builder = builder.header(name, value); - } - - builder - .status(response.status) - .body(response.body.to_vec().into()) - .unwrap() - } else { - for (name, value) in &headers { - builder = builder.header(name, value); - } - - builder.status(status).body(Vec::new().into()).unwrap() - } - } else { - let body = r.bytes().await?; - let response = CachedResponse { - status, - headers, - body, - }; - - { - response_cache - .lock() - .unwrap() - .insert(url.clone(), response.clone()); - } - - for (name, value) in &response.headers { - builder = builder.header(name, value); - } - - builder - .status(response.status) - .body(response.body.to_vec().into()) - .unwrap() - }) - } - .await - { - Ok(response) => response, - Err(e) => { - let error_message = format!( - "Failed to request {}: {}{}", - url.as_str(), - e, - if let Some(s) = e.status() { - format!("status code: {}", s.as_u16()) - } else if cfg!(target_os = "ios") { - ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() - } else { - "".to_string() - } - ); - log::error!("{error_message}"); - return Err(error_message.into()); - } - } - }; + let mut response = + proxy_dev_request(client, url, response_cache, path, builder, &request).await?; #[cfg(not(all(dev, mobile)))] let mut response = { @@ -274,3 +193,91 @@ async fn get_response( Ok(response) } + +#[cfg(all(dev, mobile))] +async fn proxy_dev_request( + client: &reqwest::Client, + url: &String, + response_cache: &Arc>>, + path: String, + mut builder: http::response::Builder, + request: &Request>, +) -> Result>, Box> { + let decoded_path = percent_encoding::percent_decode(path.as_bytes()) + .decode_utf8_lossy() + .to_string(); + let url = format!( + "{}/{}", + url.trim_end_matches('/'), + decoded_path.trim_start_matches('/') + ); + + let mut proxy_builder = client.request(request.method().clone(), &url); + for (name, value) in request.headers() { + proxy_builder = proxy_builder.header(name, value); + } + proxy_builder = proxy_builder.body(request.body().clone()); + + let r = proxy_builder.send().await.map_err(|e|{ + let error_message = format!( + "Failed to request {url}: {e}{}", + if let Some(s) = e.status() { + format!("status code: {}", s.as_u16()) + } else if cfg!(target_os = "ios") { + ", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string() + } else { + "".to_string() + } + ); + log::error!("{error_message}"); + error_message + })?; + + let status = r.status(); + let headers = r.headers().clone(); + + if status == http::StatusCode::NOT_MODIFIED { + if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.to_vec().into()) + .map_err(Into::into) + } else { + for (name, value) in &headers { + builder = builder.header(name, value); + } + + builder + .status(status) + .body(Vec::new().into()) + .map_err(Into::into) + } + } else { + let body = r.bytes().await?; + let response = CachedResponse { + status, + headers, + body, + }; + + { + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + } + + for (name, value) in &response.headers { + builder = builder.header(name, value); + } + + builder + .status(response.status) + .body(response.body.to_vec().into()) + .map_err(Into::into) + } +} From 4ff1f4ed3c05e312a58b4aa9fda7680cdcbd7c01 Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 1 Jun 2026 13:39:11 +0800 Subject: [PATCH 4/5] Clean up and don't return empty if cache not hit --- crates/tauri/src/protocol/tauri.rs | 68 ++++++++++++------------------ 1 file changed, 27 insertions(+), 41 deletions(-) diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index 0da8b195b946..0759dfaa04a6 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -21,7 +21,7 @@ use std::{collections::HashMap, sync::Mutex}; struct CachedResponse { status: http::StatusCode, headers: http::HeaderMap, - body: bytes::Bytes, + body: Vec, } pub fn get( @@ -80,7 +80,7 @@ pub fn get( } let client = client_builder.build().unwrap(); - let response_cache = Arc::new(Mutex::new(HashMap::new())); + let response_cache = Mutex::new(HashMap::new()); (url, client, response_cache) }; @@ -125,7 +125,7 @@ struct Context { #[cfg(all(dev, mobile))] client: reqwest::Client, #[cfg(all(dev, mobile))] - response_cache: Arc>>, + response_cache: Mutex>, } async fn get_response( @@ -198,7 +198,7 @@ async fn get_response( async fn proxy_dev_request( client: &reqwest::Client, url: &String, - response_cache: &Arc>>, + response_cache: &Mutex>, path: String, mut builder: http::response::Builder, request: &Request>, @@ -218,7 +218,7 @@ async fn proxy_dev_request( } proxy_builder = proxy_builder.body(request.body().clone()); - let r = proxy_builder.send().await.map_err(|e|{ + let response = proxy_builder.send().await.map_err(|e|{ let error_message = format!( "Failed to request {url}: {e}{}", if let Some(s) = e.status() { @@ -233,8 +233,7 @@ async fn proxy_dev_request( error_message })?; - let status = r.status(); - let headers = r.headers().clone(); + let status = response.status(); if status == http::StatusCode::NOT_MODIFIED { if let Some(response) = response_cache.lock().unwrap().get(&url).cloned() { @@ -242,42 +241,29 @@ async fn proxy_dev_request( builder = builder.header(name, value); } - builder - .status(response.status) - .body(response.body.to_vec().into()) - .map_err(Into::into) - } else { - for (name, value) in &headers { - builder = builder.header(name, value); - } - - builder - .status(status) - .body(Vec::new().into()) - .map_err(Into::into) - } - } else { - let body = r.bytes().await?; - let response = CachedResponse { - status, - headers, - body, - }; - - { - response_cache - .lock() - .unwrap() - .insert(url.clone(), response.clone()); + return Ok(builder.status(response.status).body(response.body.into())?); } + } - for (name, value) in &response.headers { - builder = builder.header(name, value); - } + let headers = response.headers().clone(); + let body = response.bytes().await?.to_vec(); + let response = CachedResponse { + status, + headers, + body, + }; - builder - .status(response.status) - .body(response.body.to_vec().into()) - .map_err(Into::into) + response_cache + .lock() + .unwrap() + .insert(url.clone(), response.clone()); + + for (name, value) in &response.headers { + builder = builder.header(name, value); } + + builder + .status(response.status) + .body(response.body.into()) + .map_err(Into::into) } From 9e2f2f16d0537ef7e722af31de3d3977617a142f Mon Sep 17 00:00:00 2001 From: Tony Date: Mon, 1 Jun 2026 14:33:16 +0800 Subject: [PATCH 5/5] Add change file --- .changes/load-tauri-protocol-async.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/load-tauri-protocol-async.md diff --git a/.changes/load-tauri-protocol-async.md b/.changes/load-tauri-protocol-async.md new file mode 100644 index 000000000000..7338a74a8216 --- /dev/null +++ b/.changes/load-tauri-protocol-async.md @@ -0,0 +1,5 @@ +--- +tauri: patch:perf +--- + +Load `tauri://` custom protocol handlers asynchronously to speed up load time