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

Issue:
want an API can get webview cookie
**Is your feature request related to a problem? Please describe.**
I want use wry load a remote url, wait user login, and get cookie to use user cookie to call thrid-service api, so I need get cookie on user login sucesss.


**Describe the solution you'd like**

I use electron session to finish it, the sesion can give all webview  cookies, cache, proxy setting...
but I can't found tauri have similar API


**Describe alternatives you've considered**
I found wry can preload script , I try init script and send event to main process, but the remote url will run 302 redict and callback , my init script will loss.


**Would you assign yourself to implement this feature?**
- [ x ] No

**Additional context**
electron session : https://www.electronjs.org/docs/latest/api/session




Discussion / hints:
related to https://github.com/tauri-apps/wry/issues/444.

I think you can access non secure cookie from HTTP request by using custom protocol but can't get secure cookie.
Because wry is not supporting HTTPS request yet. See related issue for more details.
Do we need to implement feature to access webview cookie even if secure cookie can not be handled?
This is useful.
I need this one as well.
I need this one as well.
please just tick the thumbs up button at the top instead of adding "+1" comments that will notify _all_ users subscribed to this issue!
I need like this
https://developer.chrome.com/docs/extensions/reference/cookies/#method-getAll
Hello :).
Hey is this still open? 
I have exactly the same problem. We have an authentication server that renders a login mask. The client will open this webpage for login (tauri opens the external webpage and renders it). As soon as the user logins it set's a private cookie that can not be obtained by javascript and redirect to the webpages from tauri (localhost). 

In another electron app we are using it already and it is quite simple to get the cookie then in the backend and handle all secure relevant authentication functions inside the electron backend. I want to implement the same in tauri. How can I achieve it if I have no possibility to obtain the cookie itself?

for example in electron I can obtain the cookies like this:
<img width="579" alt="image" src="https://github.com/tauri-apps/wry/assets/20928617/44ee1d13-a056-46c3-a20f-f8d6edd8fc9e">

Thx in advance <3



For those who absolutely want to read cookies from a window's browsing context, you can still do the following:

```rust
let window = WindowBuilder::new(
        app, // your AppHandle
        "some-window",
        WindowUrl::External("https://somewhere.com".parse().unwrap()),
    )
    .data_directory("/home/me/session-data") // pass custom path here
    .build()
    .expect("could not create window");

let cookies = std::fs::read_to_string("/home/me/session-data/cookies").expect("could not read cookies");

println!("{cookies}");
```

This code would display something like that:

```
somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_1      COOKIE_VALUE       None
somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_2      COOKIE_VALUE       None
somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_3      COOKIE_VALUE       None
... etc
```

I don't know what all the fields are but at least you can easily retrieve the name and value for each cookie.

It's a terrible solution but it works for me !

I believe the feature should really be available, just like in Electron.
I would really love this feature as well. Love the tauri "ecosystem" btw thanks for everything.
Is there any progress on this issue?
Is there any progress on this issue?

Not on our side at least, meaning no maintainer is actively working on it. No idea if there's a community member working on it but at least nobody reached out to us :)
> For those who absolutely want to read cookies from a window's browsing context, you can still do the following:
> 
> ```rust
> let window = WindowBuilder::new(
>         app, // your AppHandle
>         "some-window",
>         WindowUrl::External("https://somewhere.com".parse().unwrap()),
>     )
>     .data_directory("/home/me/session-data") // pass custom path here
>     .build()
>     .expect("could not create window");
> 
> let cookies = std::fs::read_to_string("/home/me/session-data/cookies").expect("could not read cookies");
> 
> println!("{cookies}");
> ```
> 
> This code would display something like that:
> 
> ```
> somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_1      COOKIE_VALUE       None
> somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_2      COOKIE_VALUE       None
> somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_3      COOKIE_VALUE       None
> ... etc
> ```
> 
> I don't know what all the fields are but at least you can easily retrieve the name and value for each cookie.
> 
> It's a terrible solution but it works for me !
> 
> I believe the feature should really be available, just like in Electron.

 It's quite strange, the contents of my data folder are empty.
> > For those who absolutely want to read cookies from a window's browsing context, you can still do the following:
> > ```rust
> > let window = WindowBuilder::new(
> >         app, // your AppHandle
> >         "some-window",
> >         WindowUrl::External("https://somewhere.com".parse().unwrap()),
> >     )
> >     .data_directory("/home/me/session-data") // pass custom path here
> >     .build()
> >     .expect("could not create window");
> > 
> > let cookies = std::fs::read_to_string("/home/me/session-data/cookies").expect("could not read cookies");
> > 
> > println!("{cookies}");
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > This code would display something like that:
> > ```
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_1      COOKIE_VALUE       None
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_2      COOKIE_VALUE       None
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_3      COOKIE_VALUE       None
> > ... etc
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > I don't know what all the fields are but at least you can easily retrieve the name and value for each cookie.
> > It's a terrible solution but it works for me !
> > I believe the feature should really be available, just like in Electron.
> 
> It's quite strange, the contents of my data folder are empty.

+1

I tried to do the same thing but the data folder never gets populated. 
Is there any progress on the issue?
I want to fix my insecure workarounds on our auth server that I had to do in order to make it working with tauri. It really would make me sad to go back to electron for our applications...😢.
> > For those who absolutely want to read cookies from a window's browsing context, you can still do the following:
> > ```rust
> > let window = WindowBuilder::new(
> >         app, // your AppHandle
> >         "some-window",
> >         WindowUrl::External("https://somewhere.com".parse().unwrap()),
> >     )
> >     .data_directory("/home/me/session-data") // pass custom path here
> >     .build()
> >     .expect("could not create window");
> > 
> > let cookies = std::fs::read_to_string("/home/me/session-data/cookies").expect("could not read cookies");
> > 
> > println!("{cookies}");
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > This code would display something like that:
> > ```
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_1      COOKIE_VALUE       None
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_2      COOKIE_VALUE       None
> > somewhere.com   FALSE   /       FALSE   1722097323      COOKIE_3      COOKIE_VALUE       None
> > ... etc
> > ```
> > 
> > 
> >     
> >       
> >     
> > 
> >       
> >     
> > 
> >     
> >   
> > I don't know what all the fields are but at least you can easily retrieve the name and value for each cookie.
> > It's a terrible solution but it works for me !
> > I believe the feature should really be available, just like in Electron.
> 
> It's quite strange, the contents of my data folder are empty.

Me neither.
I also tried to find the default path, since It would also be okay for me if I can use the default window from tauri.config.json. But I was not able to find where the default data_directory is. I used 

```javascript
    const appDataDirPath = await appDataDir();
    console.log("appDataDirPath", appDataDirPath);
```

on frontend side and it tells me a path that simply does not exist:

/Users/XXX/Library/Application Support/XXX-app.xyz/


I really don't get it. In another blog post I read that tauri does not preserve cookies for localhost domain when chrom or safari is used as web engine. 

![image](https://github.com/tauri-apps/wry/assets/20928617/68f4fb6f-f887-44fb-ae31-8b7a73b125a7)

https://github.com/tauri-apps/tauri/issues/2490



So is there really no workaround available for this issue?

I asked a half year ago and others even earlier, is there no solution to this issue?

Besides: I had tried the solution proposed by [Hakhenaton](https://github.com/Hakhenaton). But the code gave me compilation errors. I needed to use a pathBuf object. str is not compatible with data_directory() method. Here is my adapted rust code:
```rust

tauri::Builder::default()
        .setup(|app| {
            let path_str = "/Users/xxx/session_data/cookies";
            let data_directory: PathBuf = PathBuf::from(path_str);
            let path_string: String = data_directory.to_string_lossy().to_string();
            let window = tauri::WindowBuilder::new(app, "label", tauri::WindowUrl::App("index.html".into()))
            .data_directory(data_directory)
            .build()?;

            // let cookies = std::fs::read_to_string(path_string).expect("could not read cookies");
            // println!("{cookies}");
            Ok(())
        })
        .manage(...)
```

The command for cookie reading always fails (I assume since the file does not exist, the cookie data does not get populated as [yetone](https://github.com/yetone) and  [prabhupant](https://github.com/prabhupant) stateted), so I commented it out in my tests.
Where are the cookies stored and how do I access them? If I know this, I can write my own get cookie handler on rust side.
The only way for a user to sign in to my app is through a remote URL which I do not control. Being able to extract cookies from the webview is the only way I could ever port my Electron app to Tauri.
It's somtimes possible to grab cookies indirectly by intercepting WebView network requests (`Set-Cookie`), but there seems to be no readily available implementation.
@Zihan-Hu Do you think it would be hard to implement cross-platform reliably?
> @Zihan-Hu Do you think it would be hard to implement cross-platform reliably?

Sometimes it feels like herding cats and sometimes very easy 🤔 In my opinion, it depends on the tech stack you're using and your target user base.
Getting cookies by reading Chrome's cookie database on your computer. [https://github.com/lei4519/extract-chrome-cookies](extract-chrome-cookies)

Fix patch:
diff --git a/.changes/cookies-api.md b/.changes/cookies-api.md
new file mode 100644
index 000000000..de2225bbe
--- /dev/null
+++ b/.changes/cookies-api.md
@@ -0,0 +1,5 @@
+---
+"wry": "patch"
+---
+
+Add `WebView::cookies` and `WebView::cookies_for_url` APIs.
diff --git a/Cargo.toml b/Cargo.toml
index 33fca77c8..e413dea31 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -53,6 +53,7 @@ thiserror = "1.0"
 http = "1.1"
 raw-window-handle = { version = "0.6", features = ["std"] }
 dpi = "0.1"
+cookie = "0.18"
 
 [target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
 javascriptcore-rs = { version = "=1.1.2", features = [
@@ -93,6 +94,7 @@ features = [
 ]
 
 [target."cfg(any(target_os = \"ios\", target_os = \"macos\"))".dependencies]
+url = "2.5"
 block2 = "0.5"
 objc2 = { version = "0.5", features = ["exception"] }
 objc2-web-kit = { version = "0.2.0", features = [
@@ -119,6 +121,7 @@ objc2-web-kit = { version = "0.2.0", features = [
   "WKWebpagePreferences",
   "WKNavigationResponse",
   "WKUserScript",
+  "WKHTTPCookieStore",
 ] }
 objc2-foundation = { version = "0.2.0", features = [
   "NSURLRequest",
@@ -137,6 +140,7 @@ objc2-foundation = { version = "0.2.0", features = [
   "NSProcessInfo",
   "NSValue",
   "NSRange",
+  "NSRunLoop",
 ] }
 
 [target."cfg(target_os = \"ios\")".dependencies]
diff --git a/examples/custom_protocol.rs b/examples/custom_protocol.rs
index e19a9a759..b31fbcf41 100644
--- a/examples/custom_protocol.rs
+++ b/examples/custom_protocol.rs
@@ -19,46 +19,6 @@ fn main() -> wry::Result<()> {
   let window = WindowBuilder::new().build(&event_loop).unwrap();
 
   let builder = WebViewBuilder::new()
-    .with_id("id2")
-    .with_custom_protocol(
-      "wry".into(),
-      move |_webview_id, request| match get_wry_response(request) {
-        Ok(r) => r.map(Into::into),
-        Err(e) => http::Response::builder()
-          .header(CONTENT_TYPE, "text/plain")
-          .status(500)
-          .body(e.to_string().as_bytes().to_vec())
-          .unwrap()
-          .map(Into::into),
-      },
-    )
-    // tell the webview to load the custom protocol
-    .with_url("wry://localhost");
-
-  #[cfg(any(
-    target_os = "windows",
-    target_os = "macos",
-    target_os = "ios",
-    target_os = "android"
-  ))]
-  let _webview = builder.build(&window)?;
-  #[cfg(not(any(
-    target_os = "windows",
-    target_os = "macos",
-    target_os = "ios",
-    target_os = "android"
-  )))]
-  let _webview = {
-    use tao::platform::unix::WindowExtUnix;
-    use wry::WebViewBuilderExtUnix;
-    let vbox = window.default_vbox().unwrap();
-    builder.build_gtk(vbox)?
-  };
-
-  let window = WindowBuilder::new().build(&event_loop).unwrap();
-
-  let builder = WebViewBuilder::new()
-    .with_id("id1")
     .with_custom_protocol(
       "wry".into(),
       move |_webview_id, request| match get_wry_response(request) {
diff --git a/examples/simple.rs b/examples/simple.rs
index d5d632b24..2cf47f6d9 100644
--- a/examples/simple.rs
+++ b/examples/simple.rs
@@ -59,7 +59,7 @@ fn main() -> wry::Result<()> {
       ..
     } = event
     {
-      *control_flow = ControlFlow::Exit
+      *control_flow = ControlFlow::Exit;
     }
   });
 }
diff --git a/src/android/kotlin/RustWebView.kt b/src/android/kotlin/RustWebView.kt
index 1486791a6..cb7b0014e 100644
--- a/src/android/kotlin/RustWebView.kt
+++ b/src/android/kotlin/RustWebView.kt
@@ -97,6 +97,11 @@ class RustWebView(context: Context, val initScripts: Array<String>, val id: Stri
         settings.userAgentString = ua
     }
 
+    fun getCookies(url: String): String {
+        val cookieManager = CookieManager.getInstance()
+        return cookieManager.getCookie(url)
+    }
+
     private external fun shouldOverride(url: String): Boolean
     private external fun onEval(id: Int, result: String)
 
diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs
index e5da92a51..c82232e36 100644
--- a/src/android/main_pipe.rs
+++ b/src/android/main_pipe.rs
@@ -10,7 +10,7 @@ use jni::{
   JNIEnv,
 };
 use once_cell::sync::Lazy;
-use std::{os::unix::prelude::*, sync::atomic::Ordering};
+use std::{os::unix::prelude::*, str::FromStr, sync::atomic::Ordering};
 
 use super::{find_class, EvalCallback, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE};
 
@@ -267,24 +267,6 @@ impl<'a> MainPipe<'a> {
             Err(e) => tx.send(Err(e.into())).unwrap(),
           }
         }
-        WebViewMessage::GetId(tx) => {
-          if let Some(webview) = &self.webview {
-            let url = self
-              .env
-              .call_method(webview.as_obj(), "getUrl", "()Ljava/lang/String;", &[])
-              .and_then(|v| v.l())
-              .and_then(|s| {
-                let s = JString::from(s);
-                self
-                  .env
-                  .get_string(&s)
-                  .map(|v| v.to_string_lossy().to_string())
-              })
-              .unwrap_or_default();
-
-            tx.send(url).unwrap()
-          }
-        }
         WebViewMessage::GetUrl(tx) => {
           if let Some(webview) = &self.webview {
             let url = self
@@ -329,6 +311,36 @@ impl<'a> MainPipe<'a> {
             load_html(&mut self.env, webview.as_obj(), &html)?;
           }
         }
+        WebViewMessage::GetCookies(tx, url) => {
+          if let Some(webview) = &self.webview {
+            let url = self.env.new_string(url)?;
+            let cookies = self
+              .env
+              .call_method(
+                webview,
+                "getCookies",
+                "(Ljava/lang/String;)Ljava/lang/String;",
+                &[(&url).into()],
+              )
+              .and_then(|v| v.l())
+              .and_then(|s| {
+                let s = JString::from(s);
+                self
+                  .env
+                  .get_string(&s)
+                  .map(|v| v.to_string_lossy().to_string())
+              })
+              .unwrap_or_default();
+
+            tx.send(
+              cookies
+                .split("; ")
+                .flat_map(|c| cookie::Cookie::parse(c.to_string()))
+                .collect(),
+            )
+            .unwrap();
+          }
+        }
       }
     }
     Ok(())
@@ -395,8 +407,8 @@ pub(crate) enum WebViewMessage {
   Eval(String, Option<EvalCallback>),
   SetBackgroundColor(RGBA),
   GetWebViewVersion(Sender<Result<String, Error>>),
-  GetId(Sender<String>),
   GetUrl(Sender<String>),
+  GetCookies(Sender<Vec<cookie::Cookie<'static>>>, String),
   Jni(Box<dyn FnOnce(&mut JNIEnv, &JObject, &JObject) + Send>),
   LoadUrl(String, Option<http::HeaderMap>),
   LoadHtml(String),
diff --git a/src/android/mod.rs b/src/android/mod.rs
index ae96095ee..32aecd856 100644
--- a/src/android/mod.rs
+++ b/src/android/mod.rs
@@ -375,6 +375,16 @@ impl InnerWebView {
     Ok(())
   }
 
+  pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
+    let (tx, rx) = bounded(1);
+    MainPipe::send(WebViewMessage::GetCookies(tx, url.to_string()));
+    rx.recv().map_err(Into::into)
+  }
+
+  pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
+    Ok(Vec::new())
+  }
+
   pub fn bounds(&self) -> Result<crate::Rect> {
     Ok(crate::Rect::default())
   }
diff --git a/src/error.rs b/src/error.rs
index e3b026117..a7fad37e7 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -65,4 +65,7 @@ pub enum Error {
   DuplicateCustomProtocol(String),
   #[error("Duplicate custom protocol registered on the same web context on Linux: {0}")]
   ContextDuplicateCustomProtocol(String),
+  #[error(transparent)]
+  #[cfg(any(target_os = "macos", target_os = "ios"))]
+  UrlPrase(#[from] url::ParseError),
 }
diff --git a/src/lib.rs b/src/lib.rs
index bf257c662..3c6344825 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -248,6 +248,7 @@ use std::{borrow::Cow, collections::HashMap, path::PathBuf, rc::Rc};
 
 use http::{Request, Response};
 
+pub use cookie;
 pub use dpi;
 pub use error::*;
 pub use http;
@@ -1517,6 +1518,20 @@ impl WebView {
     self.webview.print()
   }
 
+  /// Get a list of cookies for specific url.
+  pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
+    self.webview.cookies_for_url(url)
+  }
+
+  /// Get the list of cookies.
+  ///
+  /// ## Platform-specific
+  ///
+  /// - **Android**: Unsupported, always returns an empty [`Vec`].
+  pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
+    self.webview.cookies()
+  }
+
   /// Open the web inspector which is usually called dev tool.
   ///
   /// ## Platform-specific
diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs
index 9cbf75b88..38b99dfc3 100644
--- a/src/webkitgtk/mod.rs
+++ b/src/webkitgtk/mod.rs
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: MIT
 
 use dpi::{LogicalPosition, LogicalSize};
+use ffi::CookieManageExt;
 use gdkx11::{
   ffi::{gdk_x11_window_foreign_new_for_display, GdkX11Display},
   X11Display,
@@ -25,7 +26,7 @@ use std::{
 #[cfg(any(debug_assertions, feature = "devtools"))]
 use webkit2gtk::WebInspectorExt;
 use webkit2gtk::{
-  AutoplayPolicy, InputMethodContextExt, LoadEvent, NavigationPolicyDecision,
+  AutoplayPolicy, CookieManagerExt, InputMethodContextExt, LoadEvent, NavigationPolicyDecision,
   NavigationPolicyDecisionExt, NetworkProxyMode, NetworkProxySettings, PolicyDecisionType,
   PrintOperationExt, SettingsExt, URIRequest, URIRequestExt, UserContentInjectedFrames,
   UserContentManager, UserContentManagerExt, UserScript, UserScriptInjectionTime,
@@ -804,6 +805,103 @@ impl InnerWebView {
     Ok(())
   }
 
+  fn cookie_from_soup_cookie(mut cookie: soup::Cookie) -> cookie::Cookie<'static> {
+    let name = cookie.name().map(|n| n.to_string()).unwrap_or_default();
+    let value = cookie.value().map(|n| n.to_string()).unwrap_or_default();
+
+    let mut cookie_builder = cookie::CookieBuilder::new(name, value);
+
+    if let Some(domain) = cookie.domain().map(|n| n.to_string()) {
+      cookie_builder = cookie_builder.domain(domain);
+    }
+
+    if let Some(path) = cookie.path().map(|n| n.to_string()) {
+      cookie_builder = cookie_builder.path(path);
+    }
+
+    let http_only = cookie.is_http_only();
+    cookie_builder = cookie_builder.http_only(http_only);
+
+    let secure = cookie.is_secure();
+    cookie_builder = cookie_builder.secure(secure);
+
+    let same_site = cookie.same_site_policy();
+    let same_site = match same_site {
+      soup::SameSitePolicy::Lax => cookie::SameSite::Lax,
+      soup::SameSitePolicy::Strict => cookie::SameSite::Strict,
+      soup::SameSitePolicy::None => cookie::SameSite::None,
+      _ => cookie::SameSite::None,
+    };
+    cookie_builder = cookie_builder.same_site(same_site);
+
+    let expires = cookie.expires();
+    let expires = match expires {
+      Some(datetime) => cookie::time::OffsetDateTime::from_unix_timestamp(datetime.to_unix())
+        .ok()
+        .map(cookie::Expiration::DateTime),
+      None => Some(cookie::Expiration::Session),
+    };
+    if let Some(expires) = expires {
+      cookie_builder = cookie_builder.expires(expires);
+    }
+
+    cookie_builder.build()
+  }
+
+  pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
+    let (tx, rx) = std::sync::mpsc::channel();
+    self
+      .webview
+      .website_data_manager()
+      .and_then(|manager| manager.cookie_manager())
+      .map(|cookies_manager| {
+        cookies_manager.cookies(url, None::<&Cancellable>, move |cookies| {
+          let cookies = cookies.map(|cookies| {
+            cookies
+              .into_iter()
+              .map(Self::cookie_from_soup_cookie)
+              .collect()
+          });
+          let _ = tx.send(cookies);
+        })
+      });
+
+    loop {
+      gtk::main_iteration();
+
+      if let Ok(response) = rx.try_recv() {
+        return response.map_err(Into::into);
+      }
+    }
+  }
+
+  pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
+    let (tx, rx) = std::sync::mpsc::channel();
+    self
+      .webview
+      .website_data_manager()
+      .and_then(|manager| manager.cookie_manager())
+      .map(|cookies_manager| {
+        cookies_manager.all_cookies(None::<&Cancellable>, move |cookies| {
+          let cookies = cookies.map(|cookies| {
+            cookies
+              .into_iter()
+              .map(Self::cookie_from_soup_cookie)
+              .collect()
+          });
+          let _ = tx.send(cookies);
+        })
+      });
+
+    loop {
+      gtk::main_iteration();
+
+      if let Ok(response) = rx.try_recv() {
+        return response.map_err(Into::into);
+      }
+    }
+  }
+
   pub fn reparent<W>(&self, container: &W) -> Result<()>
   where
     W: gtk::prelude::IsA<gtk::Container>,
@@ -860,3 +958,88 @@ fn scale_factor_from_x11(xlib: &Xlib, display: *mut _XDisplay, parent: c_ulong)
   let scale_factor = unsafe { (*attrs.screen).width as f64 * 25.4 / (*attrs.screen).mwidth as f64 };
   scale_factor / BASE_DPI
 }
+
+mod ffi {
+  use gtk::{
+    gdk,
+    gio::{
+      self,
+      ffi::{GAsyncReadyCallback, GCancellable},
+      prelude::*,
+      Cancellable,
+    },
+    glib::{
+      self,
+      translate::{FromGlibPtrContainer, ToGlibPtr},
+    },
+  };
+  use webkit2gtk::CookieManager;
+  use webkit2gtk_sys::WebKitCookieManager;
+
+  pub trait CookieManageExt: IsA<CookieManager> + 'static {
+    fn all_cookies<P: FnOnce(std::result::Result<Vec<soup::Cookie>, glib::Error>) + 'static>(
+      &self,
+      cancellable: Option<&impl IsA<Cancellable>>,
+      callback: P,
+    ) {
+      let main_context = glib::MainContext::ref_thread_default();
+      let is_main_context_owner = main_context.is_owner();
+      let has_acquired_main_context = (!is_main_context_owner)
+        .then(|| main_context.acquire().ok())
+        .flatten();
+      assert!(
+        is_main_context_owner || has_acquired_main_context.is_some(),
+        "Async operations only allowed if the thread is owning the MainContext"
+      );
+
+      let user_data: Box<glib::thread_guard::ThreadGuard<P>> =
+        Box::new(glib::thread_guard::ThreadGuard::new(callback));
+      unsafe extern "C" fn cookies_trampoline<
+        P: FnOnce(std::result::Result<Vec<soup::Cookie>, glib::Error>) + 'static,
+      >(
+        _source_object: *mut glib::gobject_ffi::GObject,
+        res: *mut gdk::gio::ffi::GAsyncResult,
+        user_data: glib::ffi::gpointer,
+      ) {
+        let mut error = std::ptr::null_mut();
+        let ret =
+          webkit_cookie_manager_get_all_cookies_finish(_source_object as *mut _, res, &mut error);
+        let result = if error.is_null() {
+          Ok(FromGlibPtrContainer::from_glib_full(ret))
+        } else {
+          Err(glib::translate::from_glib_full(error))
+        };
+        let callback: Box<glib::thread_guard::ThreadGuard<P>> = Box::from_raw(user_data as *mut _);
+        let callback: P = callback.into_inner();
+        callback(result);
+      }
+      let callback = cookies_trampoline::<P>;
+
+      unsafe {
+        webkit_cookie_manager_get_all_cookies(
+          self.as_ref().to_glib_none().0,
+          cancellable.map(|p| p.as_ref()).to_glib_none().0,
+          Some(callback),
+          Box::into_raw(user_data) as *mut _,
+        );
+      }
+    }
+  }
+
+  impl CookieManageExt for CookieManager {}
+
+  extern "C" {
+    pub fn webkit_cookie_manager_get_all_cookies(
+      cookie_manager: *mut webkit2gtk_sys::WebKitCookieManager,
+      cancellable: *mut GCancellable,
+      callback: GAsyncReadyCallback,
+      user_data: glib::ffi::gpointer,
+    );
+
+    pub fn webkit_cookie_manager_get_all_cookies_finish(
+      cookie_manager: *mut WebKitCookieManager,
+      result: *mut gio::ffi::GAsyncResult,
+      error: *mut *mut glib::ffi::GError,
+    ) -> *mut glib::ffi::GList;
+  }
+}
diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs
index dcdc46a77..d4e294360 100644
--- a/src/webview2/mod.rs
+++ b/src/webview2/mod.rs
@@ -293,42 +293,42 @@ impl InnerWebView {
     });
 
     let (tx, rx) = mpsc::channel();
-    CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async_operation(
-      Box::new(move |environmentcreatedhandler| unsafe {
-        let options = CoreWebView2EnvironmentOptions::default();
-
-        options.set_additional_browser_arguments(additional_browser_args);
-        options.set_are_browser_extensions_enabled(pl_attrs.browser_extensions_enabled);
-
-        // Get user's system language
-        let lcid = GetUserDefaultUILanguage();
-        let mut lang = [0; MAX_LOCALE_NAME as usize];
-        LCIDToLocaleName(lcid as u32, Some(&mut lang), LOCALE_ALLOW_NEUTRAL_NAMES);
-        options.set_language(String::from_utf16_lossy(&lang));
-
-        let scroll_bar_style = match pl_attrs.scroll_bar_style {
-          ScrollBarStyle::Default => COREWEBVIEW2_SCROLLBAR_STYLE_DEFAULT,
-          ScrollBarStyle::FluentOverlay => COREWEBVIEW2_SCROLLBAR_STYLE_FLUENT_OVERLAY,
-        };
+    let options = CoreWebView2EnvironmentOptions::default();
+    unsafe {
+      options.set_additional_browser_arguments(additional_browser_args);
+      options.set_are_browser_extensions_enabled(pl_attrs.browser_extensions_enabled);
+
+      // Get user's system language
+      let lcid = GetUserDefaultUILanguage();
+      let mut lang = [0; MAX_LOCALE_NAME as usize];
+      LCIDToLocaleName(lcid as u32, Some(&mut lang), LOCALE_ALLOW_NEUTRAL_NAMES);
+      options.set_language(String::from_utf16_lossy(&lang));
+
+      let scroll_bar_style = match pl_attrs.scroll_bar_style {
+        ScrollBarStyle::Default => COREWEBVIEW2_SCROLLBAR_STYLE_DEFAULT,
+        ScrollBarStyle::FluentOverlay => COREWEBVIEW2_SCROLLBAR_STYLE_FLUENT_OVERLAY,
+      };
 
-        options.set_scroll_bar_style(scroll_bar_style);
+      options.set_scroll_bar_style(scroll_bar_style);
 
-        CreateCoreWebView2EnvironmentWithOptions(
-          PCWSTR::null(),
-          &data_directory.unwrap_or_default(),
-          &ICoreWebView2EnvironmentOptions::from(options),
-          &environmentcreatedhandler,
-        )
-        .map_err(Into::into)
-      }),
-      Box::new(move |error_code, environment| {
-        error_code?;
-        tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER)))
-          .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
-      }),
-    )?;
+      CreateCoreWebView2EnvironmentWithOptions(
+        PCWSTR::null(),
+        &data_directory.unwrap_or_default(),
+        &ICoreWebView2EnvironmentOptions::from(options),
+        // we don't use CreateCoreWebView2EnvironmentCompletedHandler::wait_for_async
+        // as it uses an mspc::channel under the hood, so we can avoid using two channels
+        // by manually creating the callback handler and use webview2_com::with_with_bump
+        &CreateCoreWebView2EnvironmentCompletedHandler::create(Box::new(
+          move |error_code, environment| {
+            error_code?;
+            tx.send(environment.ok_or_else(|| windows::core::Error::from(E_POINTER)))
+              .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
+          },
+        )),
+      )?;
+    }
 
-    rx.recv()?.map_err(Into::into)
+    webview2_com::wait_with_pump(rx)?.map_err(Into::into)
   }
 
   #[inline]
@@ -341,34 +341,28 @@ impl InnerWebView {
     let env = env.clone();
     let env10 = env.cast::<ICoreWebView2Environment10>();
 
-    CreateCoreWebView2ControllerCompletedHandler::wait_for_async_operation(
-      if let Ok(env10) = env10 {
-        let controller_opts = unsafe { env10.CreateCoreWebView2ControllerOptions()? };
-        unsafe { controller_opts.SetIsInPrivateModeEnabled(incognito)? }
-        Box::new(
-          move |handler: ICoreWebView2CreateCoreWebView2ControllerCompletedHandler| unsafe {
-            env10
-              .CreateCoreWebView2ControllerWithOptions(hwnd, &controller_opts, &handler)
-              .map_err(Into::into)
-          },
-        )
-      } else {
-        Box::new(
-          move |handler: ICoreWebView2CreateCoreWebView2ControllerCompletedHandler| unsafe {
-            env
-              .CreateCoreWebView2Controller(hwnd, &handler)
-              .map_err(Into::into)
-          },
-        )
-      },
-      Box::new(move |error_code, controller| {
+    // we don't use CreateCoreWebView2ControllerCompletedHandler::wait_for_async
+    // as it uses an mspc::channel under the hood, so we can avoid using two channels
+    // by manually creating the callback handler and use webview2_com::with_with_bump
+    let handler = CreateCoreWebView2ControllerCompletedHandler::create(Box::new(
+      move |error_code, controller| {
         error_code?;
         tx.send(controller.ok_or_else(|| windows::core::Error::from(E_POINTER)))
           .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
-      }),
-    )?;
+      },
+    ));
+
+    unsafe {
+      if let Ok(env10) = env10 {
+        let controller_opts = env10.CreateCoreWebView2ControllerOptions()?;
+        controller_opts.SetIsInPrivateModeEnabled(incognito)?;
+        env10.CreateCoreWebView2ControllerWithOptions(hwnd, &controller_opts, &handler)?;
+      } else {
+        env.CreateCoreWebView2Controller(hwnd, &handler)?
+      }
+    }
 
-    rx.recv()?.map_err(Into::into)
+    webview2_com::wait_with_pump(rx)?.map_err(Into::into)
   }
 
   #[inline]
@@ -1326,6 +1320,112 @@ impl InnerWebView {
     Ok(())
   }
 
+  unsafe fn cookie_from_win32(cookie: ICoreWebView2Cookie) -> Result<cookie::Cookie<'static>> {
+    let mut name = PWSTR::null();
+    cookie.Name(&mut name)?;
+    let name = take_pwstr(name);
+
+    let mut value = PWSTR::null();
+    cookie.Value(&mut value)?;
+    let value = take_pwstr(value);
+
+    let mut cookie_builder = cookie::CookieBuilder::new(name, value);
+
+    let mut domain = PWSTR::null();
+    cookie.Domain(&mut domain)?;
+    cookie_builder = cookie_builder.domain(take_pwstr(domain));
+
+    let mut path = PWSTR::null();
+    cookie.Path(&mut path)?;
+    cookie_builder = cookie_builder.path(take_pwstr(path));
+
+    let mut http_only: BOOL = false.into();
+    cookie.IsHttpOnly(&mut http_only)?;
+    cookie_builder = cookie_builder.http_only(http_only.as_bool());
+
+    let mut secure: BOOL = false.into();
+    cookie.IsSecure(&mut secure)?;
+    cookie_builder = cookie_builder.secure(secure.as_bool());
+
+    let mut same_site = COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX;
+    cookie.SameSite(&mut same_site)?;
+    let same_site = match same_site {
+      COREWEBVIEW2_COOKIE_SAME_SITE_KIND_LAX => cookie::SameSite::Lax,
+      COREWEBVIEW2_COOKIE_SAME_SITE_KIND_STRICT => cookie::SameSite::Strict,
+      COREWEBVIEW2_COOKIE_SAME_SITE_KIND_NONE => cookie::SameSite::None,
+      _ => cookie::SameSite::None,
+    };
+    cookie_builder = cookie_builder.same_site(same_site);
+
+    let mut is_session: BOOL = false.into();
+    cookie.IsSession(&mut is_session)?;
+
+    let mut expires = 0.0;
+    cookie.Expires(&mut expires)?;
+
+    let expires = match expires {
+      -1.0 | _ if is_session.as_bool() => Some(cookie::Expiration::Session),
+      datetime => cookie::time::OffsetDateTime::from_unix_timestamp(datetime as _)
+        .ok()
+        .map(cookie::Expiration::DateTime),
+    };
+    if let Some(expires) = expires {
+      cookie_builder = cookie_builder.expires(expires);
+    }
+
+    Ok(cookie_builder.build())
+  }
+
+  pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
+    let uri = HSTRING::from(url);
+    self.cookies_inner(PCWSTR::from_raw(uri.as_ptr()))
+  }
+
+  pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
+    self.cookies_inner(PCWSTR::null())
+  }
+
+  fn cookies_inner(&self, uri: PCWSTR) -> Result<Vec<cookie::Cookie<'static>>> {
+    let (tx, rx) = mpsc::channel();
+
+    let webview = self.webview.cast::<ICoreWebView2_2>()?;
+    unsafe {
+      webview.CookieManager()?.GetCookies(
+        uri,
+        // we don't use GetCookiesCompletedHandler::wait_for_async
+        // as it uses an mspc::channel under the hood, so we can avoid using two channels
+        // by manually creating the callback handler and use webview2_com::with_with_bump
+        &GetCookiesCompletedHandler::create(Box::new(move |error_code, cookies| {
+          error_code?;
+
+          let cookies = if let Some(cookies) = cookies {
+            let mut count = 0;
+            cookies.Count(&mut count)?;
+
+            let mut out = Vec::with_capacity(count as _);
+
+            for idx in 0..count {
+              let cookie = cookies.GetValueAtIndex(idx)?;
+
+              if let Ok(cookie) = Self::cookie_from_win32(cookie) {
+                out.push(cookie)
+              }
+            }
+
+            out
+          } else {
+            Vec::new()
+          };
+
+          tx.send(cookies)
+            .map_err(|_| windows::core::Error::from(E_UNEXPECTED))
+        })),
+      )?;
+    }
+
+    webview2_com::wait_with_pump(rx).map_err(Into::into)
+  }
+
   pub fn reparent(&self, parent: isize) -> Result<()> {
     let parent = HWND(parent as _);
 
diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs
index 8fd3b96fc..61c8baf64 100644
--- a/src/wkwebview/mod.rs
+++ b/src/wkwebview/mod.rs
@@ -42,9 +42,10 @@ use objc2_app_kit::{NSApplication, NSAutoresizingMaskOptions, NSTitlebarSeparato
 #[cfg(target_os = "macos")]
 use objc2_foundation::CGSize;
 use objc2_foundation::{
-  ns_string, CGPoint, CGRect, MainThreadMarker, NSBundle, NSDate, NSError, NSJSONSerialization,
-  NSMutableURLRequest, NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString,
-  NSUTF8StringEncoding, NSURL, NSUUID,
+  ns_string, CGPoint, CGRect, MainThreadMarker, NSArray, NSBundle, NSDate, NSError, NSHTTPCookie,
+  NSHTTPCookieSameSiteLax, NSHTTPCookieSameSiteStrict, NSJSONSerialization, NSMutableURLRequest,
+  NSNumber, NSObjectNSKeyValueCoding, NSObjectProtocol, NSString, NSUTF8StringEncoding, NSURL,
+  NSUUID,
 };
 #[cfg(target_os = "ios")]
 use objc2_ui_kit::{UIScrollView, UIViewAutoresizing};
@@ -71,10 +72,11 @@ use raw_window_handle::{HasWindowHandle, RawWindowHandle};
 use std::{
   collections::{HashMap, HashSet},
   ffi::{c_void, CString},
+  net::Ipv4Addr,
   os::raw::c_char,
   panic::AssertUnwindSafe,
-  ptr::null_mut,
-  str,
+  ptr::{null_mut, NonNull},
+  str::{self, FromStr},
   sync::{Arc, Mutex},
 };
 
@@ -112,6 +114,7 @@ pub(crate) struct InnerWebView {
   id: String,
   pub webview: Retained<WryWebView>,
   pub manager: Retained<WKUserContentController>,
+  data_store: Retained<WKWebsiteDataStore>,
   ns_view: Retained<NSView>,
   #[allow(dead_code)]
   is_child: bool,
@@ -268,8 +271,7 @@ impl InnerWebView {
           }
         };
 
-        let proxies: Retained<objc2_foundation::NSArray<NSObject>> =
-          objc2_foundation::NSArray::arrayWithObject(&*proxy_config);
+        let proxies: Retained<NSArray<NSObject>> = NSArray::arrayWithObject(&*proxy_config);
         data_store.setValue_forKey(Some(&proxies), ns_string!("proxyConfigurations"));
       }
 
@@ -462,6 +464,7 @@ impl InnerWebView {
         webview: webview.clone(),
         manager: manager.clone(),
         ns_view: ns_view.retain(),
+        data_store,
         pending_scripts,
         ipc_handler_delegate,
         document_title_changed_observer,
@@ -825,6 +828,95 @@ r#"Object.defineProperty(window, 'ipc', {
     Ok(())
   }
 
+  unsafe fn cookie_from_wkwebview(cookie: &NSHTTPCookie) -> cookie::Cookie<'static> {
+    let name = cookie.name().to_string();
+    let value = cookie.value().to_string();
+
+    let mut cookie_builder = cookie::CookieBuilder::new(name, value);
+
+    let domain = cookie.domain().to_string();
+    cookie_builder = cookie_builder.domain(domain);
+
+    let path = cookie.path().to_string();
+    cookie_builder = cookie_builder.path(path);
+
+    let http_only = cookie.isHTTPOnly();
+    cookie_builder = cookie_builder.http_only(http_only);
+
+    let secure = cookie.isSecure();
+    cookie_builder = cookie_builder.secure(secure);
+
+    let same_site = cookie.sameSitePolicy();
+    let same_site = match same_site {
+      Some(policy) if policy.as_ref() == NSHTTPCookieSameSiteLax => cookie::SameSite::Lax,
+      Some(policy) if policy.as_ref() == NSHTTPCookieSameSiteStrict => cookie::SameSite::Strict,
+      _ => cookie::SameSite::None,
+    };
+    cookie_builder = cookie_builder.same_site(same_site);
+
+    let expires = cookie.expiresDate();
+    let expires = match expires {
+      Some(datetime) => {
+        cookie::time::OffsetDateTime::from_unix_timestamp(datetime.timeIntervalSince1970() as i64)
+          .ok()
+          .map(cookie::Expiration::DateTime)
+      }
+      None => Some(cookie::Expiration::Session),
+    };
+    if let Some(expires) = expires {
+      cookie_builder = cookie_builder.expires(expires);
+    }
+
+    cookie_builder.build()
+  }
+
+  pub fn cookies_for_url(&self, url: &str) -> Result<Vec<cookie::Cookie<'static>>> {
+    let url = url::Url::parse(url)?;
+
+    self.cookies().map(|cookies| {
+      cookies.into_iter().filter(|cookie: &cookie::Cookie| {
+        let secure = cookie.secure().unwrap_or_default();
+        // domain is the same
+        cookie.domain() == url.domain()
+          // and one of
+          && (
+            // cookie is secure and url is https
+            (secure && url.scheme() == "https") ||
+            // or cookie is secure and is localhost
+            (
+              secure && url.scheme() == "http" && 
+              (url.domain() == Some("localhost") || url.domain().and_then(|d| Ipv4Addr::from_str(d).ok()).map(|ip| ip.is_loopback()).unwrap_or(false))
+            ) ||
+            // or cookie is not secure
+            (!secure)
+          )
+      }).collect()
+    })
+  }
+
+  pub fn cookies(&self) -> Result<Vec<cookie::Cookie<'static>>> {
+    let (tx, rx) = std::sync::mpsc::channel();
+
+    unsafe {
+      self
+        .data_store
+        .httpCookieStore()
+        .getAllCookies(&block2::RcBlock::new(
+          move |cookies: NonNull<NSArray<NSHTTPCookie>>| {
+            let cookies = cookies.as_ref();
+            let cookies = cookies
+              .to_vec()
+              .into_iter()
+              .map(|cookie| Self::cookie_from_wkwebview(cookie))
+              .collect();
+            let _ = tx.send(cookies);
+          },
+        ));
+
+      wait_for_blocking_operation(rx)
+    }
+  }
+
   #[cfg(target_os = "macos")]
   pub(crate) fn reparent(&self, window: *mut NSWindow) -> crate::Result<()> {
     unsafe {
@@ -905,3 +997,25 @@ unsafe fn window_position(view: &NSView, x: i32, y: i32, height: f64) -> CGPoint
   let frame: CGRect = view.frame();
   CGPoint::new(x as f64, frame.size.height - y as f64 - height)
 }
+
+unsafe fn wait_for_blocking_operation<T>(rx: std::sync::mpsc::Receiver<T>) -> Result<T> {
+  let interval = 0.0002;
+  let limit = 1.;
+  let mut elapsed = 0.;
+  // run event loop until we get the response back, blocking for at most 3 seconds
+  loop {
+    let rl = objc2_foundation::NSRunLoop::mainRunLoop();
+    let d = NSDate::dateWithTimeIntervalSinceNow(interval);
+    rl.runUntilDate(&d);
+    if let Ok(response) = rx.try_recv() {
+      return Ok(response);
+    }
+    elapsed += interval;
+    if elapsed >= limit {
+      return Err(Error::Io(std::io::Error::new(
+        std::io::ErrorKind::TimedOut,
+        "timed out waiting for cookies response",
+      )));
+    }
+  }
+}


Test patch: