Repository: tauri-apps/wry (language: rust)

Issue:
[macOS] APP crash when a web custom protocol request is aborted before or during response
### Describe the bug

When initiating a `register_uri_scheme_protocol` request from the web side, if the web aborts the request before or during the response, the macOS APP crashes.


### Reproduction

- Register a `register_asynchronous_uri_scheme_protocol`.
- From the web side, initiate a request using this protocol and abort the request before the response.
- The APP crashes 100% of the time.

### Expected behavior

- Before responding, the `responder` should check if the request has been canceled. If the request has been canceled, do not send the response.
- Or, during the response, catch exceptions to prevent the APP from crashing and quitting abruptly when the request is terminated.

### Full `tauri info` output

```text
[✔] Environment
    - OS: Mac OS 13.6.3 X64
    ✔ Xcode Command Line Tools: installed
    ✔ rustc: 1.76.0 (07dca489a 2024-02-04)
    ✔ cargo: 1.76.0 (c84b36747 2024-01-18)
    ✔ rustup: 1.26.0 (5af9b9484 2023-04-05)
    ✔ Rust toolchain: stable-x86_64-apple-darwin (default)
    - node: 18.17.1
    - pnpm: 8.15.4
    - npm: 9.6.7

[-] Packages
    - tauri [RUST]: 2.0.0-beta.10
    - tauri-build [RUST]: 2.0.0-beta.8
    - wry [RUST]: 0.37.0
    - tao [RUST]: 0.26.1
    - @tauri-apps/api [NPM]: 2.0.0-beta.5
    - @tauri-apps/cli [NPM]: 2.0.0-beta.9

[-] App
    - build-type: bundle
    - CSP: default-src x-http: x-https: blob: data: filesystem: ws: http: https: 'unsafe-eval' 'unsafe-inline'
    - frontendDist: ../feiyu/dist
    - devUrl: http://localhost:3000/
```


### Stack trace

```text
thread 'tokio-runtime-worker' panicked at /Users/mac/.cargo/registry/src/index.crates.io-6f17d22bba15001f/wry-0.37.0/src/wkwebview/mod.rs:262:28:
Uncaught exception <NSException: 0x10428cf30>
```


### Additional context

Related code: 
https://github.com/tauri-apps/wry/blob/5789bf759ce94e4dad5ff26a08fe81521658a4e4/src/wkwebview/mod.rs#L272
<img width="1802" alt="图片" src="https://github.com/tauri-apps/tauri/assets/35302658/767b0a0a-3c21-42fa-837f-fc4ed4153f57">

Related issue: https://github.com/tauri-apps/tauri/issues/9177


Discussion / hints:
Seems like this is the cause of #1142?
Do you have an idea on how to fix this issue? 
https://github.com/tauri-apps/wry/pull/1214
any update? this bug prevents me from developing on a Mac and requires me to develop in a Windows virtual machine
@darkskygit Would you happen to have a minimal reproduction example (or any kind of reproduction)? 
> @darkskygit Would you happen to have a minimal reproduction example (or any kind of reproduction)?

https://github.com/darkskygit/tauri-custom-uri-schema-crash

the latest commit will make tauri crash in mac
Does this only happen with `http` / `https` and other known/native schemes? WKWebView does not allow registering schemes that it already handles by default so it makes sense if it crashes then.
> Does this only happen with `http` / `https` and other known/native schemes? WKWebView does not allow registering schemes that it already handles by default so it makes sense if it crashes then.

i tried to register a custom schema, and it indeed no longer causes a crash
Is there any other way to forward requests under certain paths (e.g., `/api/*`) to Rust? Using custom paths or invoke requires a lot of changes to the front-end code, and registering `http://` works well on windows
Not that i know of. Windows also only works by accident here. Once we drop win7 support tauri will use a different api for the custom protocol which would break your implementation.

After that you could use the request intercept api we currently use on windows for the custom protocol of course ([example](https://github.com/tauri-apps/tauri/discussions/4912)) but there's no equivalent on linux/macos as long as we're stuck with the current webviews.
> Not that i know of. Windows also only works by accident here. Once we drop win7 support tauri will use a different api for the custom protocol which would break your implementation.
> 
> After that you could use the request intercept api we currently use on windows for the custom protocol of course ([example](https://github.com/tauri-apps/tauri/discussions/4912)) but there's no equivalent on linux/macos as long as we're stuck with the current webviews.

if it is not possible to register a custom protocol for http under linux/macos, how does tauri respond to static files?
can I use the same method to register a custom handler?
my current implementation can also return frontend static assets, so it is acceptable for my custom handler to respond to all requests but not only `/api/*`
Tauri serves the frontend via the `tauri://` scheme via the custom protocol webview apis (the same ones that do not allow overwriting existing schemes). Requests to those schemes can be handled just fine.

> my current implementation can also return frontend static assets, so it is acceptable for my custom handler to respond to all requests but not only /api/*

Then you'd have to change the url of the window to your custom scheme.
> Tauri serves the frontend via the `tauri://` scheme via the custom protocol webview apis (the same ones that do not allow overwriting existing schemes). Requests to those schemes can be handled just fine.
> 
> > my current implementation can also return frontend static assets, so it is acceptable for my custom handler to respond to all requests but not only /api/*
> 
> Then you'd have to change the url of the window to your custom scheme.

Thank you for your prompt response. To help others quickly understand this problem, I write a summary of the above discussion:
- if register a handler to native protocol (e.g. `http`, `https`), this will make webview crash in macos
- if want to integrate a front-end project that includes rust api server, you can follow these guide:
   1. register a custom protocol
       ``` rust
        pub fn run() {
            tauri::Builder::default()
                .plugin(tauri_plugin_shell::init())
                .register_asynchronous_uri_scheme_protocol("custom_schema", |app, req, resp| {
                            let path = req.uri().path();
                            // response static files or api response here
                            resp.respond(
                                Response::builder()
                                    .status(StatusCode::BAD_REQUEST)
                                    .header(CONTENT_TYPE, TEXT_PLAIN.essence_str())
                                    .body("failed to generate response".as_bytes().to_vec())
                                    .unwrap(),
                            );
                })
                .invoke_handler(tauri::generate_handler![])
                .run(tauri::generate_context!())
                .expect("error while running tauri application");
        }
        ````
   2. change default url in `tauri.conf.json`:
       ``` json
       {
           "build": {
               "frontendDist": "custom_protocol://localhost"
           }
       }
       ```
Is there any update? I experience the same crash on macos with custom protocol. Once the response returned, it crash.
For me it crash if I read file inside the custom protocol callback. (only read without unwrap it no matter where in the callback)

Fix patch:
diff --git a/.changes/objc-exceptions.md b/.changes/objc-exceptions.md
new file mode 100644
index 000000000..97a72fab3
--- /dev/null
+++ b/.changes/objc-exceptions.md
@@ -0,0 +1,5 @@
+---
+wry: minor
+---
+
+Removed `obj-exception` feature.
diff --git a/Cargo.toml b/Cargo.toml
index 7590c1212..b86142f45 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -26,9 +26,8 @@ rustc-args = ["--cfg", "docsrs"]
 rustdoc-args = ["--cfg", "docsrs"]
 
 [features]
-default = ["drag-drop", "objc-exception", "protocol", "os-webview"]
+default = ["drag-drop", "protocol", "os-webview"]
 serde = ["dpi/serde"]
-objc-exception = ["objc2/catch-all"]
 drag-drop = []
 protocol = []
 devtools = []
diff --git a/src/wkwebview/class/url_scheme_handler.rs b/src/wkwebview/class/url_scheme_handler.rs
index 8300dd9d6..819c08bda 100644
--- a/src/wkwebview/class/url_scheme_handler.rs
+++ b/src/wkwebview/class/url_scheme_handler.rs
@@ -16,7 +16,7 @@ use http::{
 use objc2::{
   rc::Retained,
   runtime::{AnyClass, AnyObject, ClassBuilder, ProtocolObject},
-  AllocAnyThread, ClassType,
+  AllocAnyThread, ClassType, Message,
 };
 use objc2_foundation::{
   NSData, NSHTTPURLResponse, NSMutableDictionary, NSObject, NSObjectProtocol, NSString, NSURL,
@@ -54,8 +54,8 @@ pub fn create(name: &str) -> &AnyClass {
 extern "C" fn start_task(
   this: &AnyObject,
   _sel: objc2::runtime::Sel,
-  webview: &'static WryWebView,
-  task: &'static ProtocolObject<dyn WKURLSchemeTask>,
+  webview: &WryWebView,
+  task: &ProtocolObject<dyn WKURLSchemeTask>,
 ) {
   unsafe {
     #[cfg(feature = "tracing")]
@@ -180,12 +180,14 @@ extern "C" fn start_task(
       // send response
       match http_request.body(sent_form_body) {
         Ok(final_request) => {
+          let webview = webview.retain();
+          let task = task.retain();
           let responder: Box<dyn FnOnce(HttpResponse<Cow<'static, [u8]>>)> =
             Box::new(move |sent_response| {
               // Consolidate checks before calling into `did*` methods.
               let validate = || -> crate::Result<()> {
                 check_webview_id_valid(webview_id)?;
-                check_task_is_valid(webview, task_key, task_uuid.clone())?;
+                check_task_is_valid(&webview, task_key, task_uuid.clone())?;
                 Ok(())
               };
 
@@ -198,9 +200,9 @@ extern "C" fn start_task(
 
               unsafe fn response(
                 // FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
-                task: &'static ProtocolObject<dyn WKURLSchemeTask>,
+                task: Retained<ProtocolObject<dyn WKURLSchemeTask>>,
                 // FIXME: though we give it a static lifetime, it's not guaranteed to be valid.
-                webview: &'static WryWebView,
+                webview: Retained<WryWebView>,
                 task_key: usize,
                 task_uuid: Retained<NSUUID>,
                 webview_id: &str,
@@ -209,7 +211,7 @@ extern "C" fn start_task(
               ) -> crate::Result<()> {
                 // Validate
                 check_webview_id_valid(webview_id)?;
-                check_task_is_valid(webview, task_key, task_uuid.clone())?;
+                check_task_is_valid(&webview, task_key, task_uuid.clone())?;
 
                 let content = sent_response.body();
                 // default: application/octet-stream, but should be provided by the client
@@ -253,7 +255,7 @@ extern "C" fn start_task(
 
                 // Re-validate before calling didReceiveResponse
                 check_webview_id_valid(webview_id)?;
-                check_task_is_valid(webview, task_key, task_uuid.clone())?;
+                check_task_is_valid(&webview, task_key, task_uuid.clone())?;
 
                 // Use map_err to convert Option<Retained<Exception>> to crate::Error
                 objc2::exception::catch(AssertUnwindSafe(|| {
@@ -273,7 +275,7 @@ extern "C" fn start_task(
 
                 // Check validity again
                 check_webview_id_valid(webview_id)?;
-                check_task_is_valid(webview, task_key, task_uuid.clone())?;
+                check_task_is_valid(&webview, task_key, task_uuid.clone())?;
 
                 objc2::exception::catch(AssertUnwindSafe(|| {
                   task.didReceiveData(&data);
@@ -281,7 +283,7 @@ extern "C" fn start_task(
                 .map_err(|_e| crate::Error::CustomProtocolTaskInvalid)?;
 
                 check_webview_id_valid(webview_id)?;
-                check_task_is_valid(webview, task_key, task_uuid.clone())?;
+                check_task_is_valid(&webview, task_key, task_uuid.clone())?;
 
                 objc2::exception::catch(AssertUnwindSafe(|| {
                   task.didFinish();
diff --git a/src/wkwebview/class/wry_web_view.rs b/src/wkwebview/class/wry_web_view.rs
index f96a934ab..8ae5f88d3 100644
--- a/src/wkwebview/class/wry_web_view.rs
+++ b/src/wkwebview/class/wry_web_view.rs
@@ -2,7 +2,7 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
-use std::{cell::RefCell, collections::HashMap};
+use std::{cell::RefCell, collections::HashMap, sync::Mutex};
 
 #[cfg(target_os = "macos")]
 use objc2::runtime::ProtocolObject;
@@ -29,7 +29,7 @@ pub struct WryWebViewIvars {
   pub(crate) drag_drop_handler: Box<dyn Fn(DragDropEvent) -> bool>,
   #[cfg(target_os = "macos")]
   pub(crate) accept_first_mouse: objc2::runtime::Bool,
-  pub(crate) custom_protocol_task_ids: RefCell<HashMap<usize, Retained<NSUUID>>>,
+  pub(crate) custom_protocol_task_ids: Mutex<HashMap<usize, Retained<NSUUID>>>,
 }
 
 define_class!(
@@ -117,7 +117,8 @@ impl WryWebView {
     self
       .ivars()
       .custom_protocol_task_ids
-      .borrow_mut()
+      .lock()
+      .unwrap()
       .insert(task_id, task_uuid.clone());
     task_uuid
   }
@@ -125,14 +126,16 @@ impl WryWebView {
     self
       .ivars()
       .custom_protocol_task_ids
-      .borrow_mut()
+      .lock()
+      .unwrap()
       .remove(&task_id);
   }
   pub(crate) fn get_custom_task_uuid(&self, task_id: usize) -> Option<Retained<NSUUID>> {
     self
       .ivars()
       .custom_protocol_task_ids
-      .borrow()
+      .lock()
+      .unwrap()
       .get(&task_id)
       .cloned()
   }


Test patch: