Repository: servo/servo
Pull request #35668: libservo: Notify delegates of send errors in request objects
Author: login: delan
Created: 2025-02-26T06:48:05Z | Merged: 2025-03-19T07:36:11Z

This patch centralises IPC error handling for delegate request objects by adding a new ServoError variant that gets passed to ServoDelegate::notify_error(). These errors come from request objects via an internal crossbeam channel.


---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] ~~These changes fix #___ (GitHub issue number if applicable)~~

<!-- Either: -->
- [x] There are tests for these changes OR
- [ ] These changes do not require tests because ___


Diff:
diff --git a/Cargo.lock b/Cargo.lock
index 700b9a01cb834..320316168fd9e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4341,6 +4341,7 @@ dependencies = [
  "gaol",
  "gleam",
  "gstreamer",
+ "http 1.3.1",
  "ipc-channel",
  "keyboard-types",
  "layout_thread_2020",
diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml
index abccccece8b82..1b11c059f057a 100644
--- a/components/servo/Cargo.toml
+++ b/components/servo/Cargo.toml
@@ -124,6 +124,7 @@ gaol = "0.2.1"
 webxr = { path = "../webxr", features = ["ipc", "glwindow", "headless", "openxr-api"] }
 
 [dev-dependencies]
+http = { workspace = true }
 libservo = { path = ".", features = ["tracing"] }
 rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] }
 tracing = { workspace = true }
diff --git a/components/servo/lib.rs b/components/servo/lib.rs
index c8fb3ec88ac63..917b83f584c67 100644
--- a/components/servo/lib.rs
+++ b/components/servo/lib.rs
@@ -19,6 +19,7 @@
 
 mod clipboard_delegate;
 mod proxies;
+mod responders;
 mod servo_delegate;
 mod webview;
 mod webview_delegate;
@@ -118,6 +119,7 @@ pub use {
 pub use {bluetooth, bluetooth_traits};
 
 use crate::proxies::ConstellationProxy;
+use crate::responders::ServoErrorChannel;
 pub use crate::servo_delegate::{ServoDelegate, ServoError};
 pub use crate::webview::WebView;
 pub use crate::webview_delegate::{
@@ -205,6 +207,7 @@ pub struct Servo {
     /// When accessed, `Servo` will be reponsible for cleaning up the invalid `Weak`
     /// references.
     webviews: RefCell<HashMap<WebViewId, Weak<RefCell<WebViewInner>>>>,
+    servo_errors: ServoErrorChannel,
     /// For single-process Servo instances, this field controls the initialization
     /// and deinitialization of the JS Engine. Multiprocess Servo instances have their
     /// own instance that exists in the content process instead.
@@ -536,6 +539,7 @@ impl Servo {
             embedder_receiver,
             shutdown_state,
             webviews: Default::default(),
+            servo_errors: ServoErrorChannel::default(),
             _js_engine_setup: js_engine_setup,
         }
     }
@@ -585,6 +589,7 @@ impl Servo {
 
         self.compositor.borrow_mut().perform_updates();
         self.send_new_frame_ready_messages();
+        self.handle_delegate_errors();
         self.clean_up_destroyed_webview_handles();
 
         if self.shutdown_state.get() == ShutdownState::FinishedShuttingDown {
@@ -609,6 +614,12 @@ impl Servo {
         }
     }
 
+    fn handle_delegate_errors(&self) {
+        while let Some(error) = self.servo_errors.try_recv() {
+            self.delegate().notify_error(self, error);
+        }
+    }
+
     fn clean_up_destroyed_webview_handles(&self) {
         // Remove any webview handles that have been destroyed and would not be upgradable.
         // Note that `retain` is O(capacity) because it visits empty buckets, so it may be worth
@@ -758,7 +769,11 @@ impl Servo {
             },
             EmbedderMsg::AllowUnload(webview_id, response_sender) => {
                 if let Some(webview) = self.get_webview_handle(webview_id) {
-                    let request = AllowOrDenyRequest::new(response_sender, AllowOrDeny::Allow);
+                    let request = AllowOrDenyRequest::new(
+                        response_sender,
+                        AllowOrDeny::Allow,
+                        self.servo_errors.sender(),
+                    );
                     webview.delegate().request_unload(webview, request);
                 }
             },
@@ -824,12 +839,24 @@ impl Servo {
                 web_resource_request,
                 response_sender,
             ) => {
-                let web_resource_load = WebResourceLoad::new(web_resource_request, response_sender);
-                match webview_id.and_then(|webview_id| self.get_webview_handle(webview_id)) {
-                    Some(webview) => webview
+                if let Some(webview) =
+                    webview_id.and_then(|webview_id| self.get_webview_handle(webview_id))
+                {
+                    let web_resource_load = WebResourceLoad::new(
+                        web_resource_request,
+                        response_sender,
+                        self.servo_errors.sender(),
+                    );
+                    webview
                         .delegate()
-                        .load_web_resource(webview, web_resource_load),
-                    None => self.delegate().load_web_resource(web_resource_load),
+                        .load_web_resource(webview, web_resource_load);
+                } else {
+                    let web_resource_load = WebResourceLoad::new(
+                        web_resource_request,
+                        response_sender,
+                        self.servo_errors.sender(),
+                    );
+                    self.delegate().load_web_resource(web_resource_load);
                 }
             },
             EmbedderMsg::Panic(webview_id, reason, backtrace) => {
@@ -864,9 +891,13 @@ impl Servo {
                 }
             },
             EmbedderMsg::RequestAuthentication(webview_id, url, for_proxy, response_sender) => {
-                let authentication_request =
-                    AuthenticationRequest::new(url.into_url(), for_proxy, response_sender);
                 if let Some(webview) = self.get_webview_handle(webview_id) {
+                    let authentication_request = AuthenticationRequest::new(
+                        url.into_url(),
+                        for_proxy,
+                        response_sender,
+                        self.servo_errors.sender(),
+                    );
                     webview
                         .delegate()
                         .request_authentication(webview, authentication_request);
@@ -879,6 +910,7 @@ impl Servo {
                         allow_deny_request: AllowOrDenyRequest::new(
                             response_sender,
                             AllowOrDeny::Deny,
+                            self.servo_errors.sender(),
                         ),
                     };
                     webview
@@ -921,7 +953,11 @@ impl Servo {
             EmbedderMsg::RequestDevtoolsConnection(response_sender) => {
                 self.delegate().request_devtools_connection(
                     self,
-                    AllowOrDenyRequest::new(response_sender, AllowOrDeny::Deny),
+                    AllowOrDenyRequest::new(
+                        response_sender,
+                        AllowOrDeny::Deny,
+                        self.servo_errors.sender(),
+                    ),
                 );
             },
             EmbedderMsg::PlayGamepadHapticEffect(
diff --git a/components/servo/responders.rs b/components/servo/responders.rs
new file mode 100644
index 0000000000000..6606552454303
--- /dev/null
+++ b/components/servo/responders.rs
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
+use log::warn;
+
+use crate::ServoError;
+
+/// Sender for errors raised by delegate request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorSender {
+    sender: Sender<ServoError>,
+}
+
+impl ServoErrorSender {
+    pub(crate) fn raise_response_send_error(&self, error: bincode::Error) {
+        if let Err(error) = self.sender.send(ServoError::ResponseFailedToSend(error)) {
+            warn!("Failed to send Servo error: {error:?}");
+        }
+    }
+}
+
+/// Channel for errors raised by [`WebViewDelegate`] request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorChannel {
+    sender: Sender<ServoError>,
+    receiver: Receiver<ServoError>,
+}
+
+impl Default for ServoErrorChannel {
+    fn default() -> Self {
+        let (sender, receiver) = unbounded();
+        Self { sender, receiver }
+    }
+}
+
+impl ServoErrorChannel {
+    pub(crate) fn sender(&self) -> ServoErrorSender {
+        ServoErrorSender {
+            sender: self.sender.clone(),
+        }
+    }
+
+    pub(crate) fn try_recv(&self) -> Option<ServoError> {
+        match self.receiver.try_recv() {
+            Ok(result) => Some(result),
+            Err(error) => {
+                debug_assert_eq!(error, TryRecvError::Empty);
+                None
+            },
+        }
+    }
+}
diff --git a/components/servo/servo_delegate.rs b/components/servo/servo_delegate.rs
index 5ddd10367357f..34d5c50bda510 100644
--- a/components/servo/servo_delegate.rs
+++ b/components/servo/servo_delegate.rs
@@ -5,7 +5,7 @@
 use crate::Servo;
 use crate::webview_delegate::{AllowOrDenyRequest, WebResourceLoad};
 
-#[derive(Clone, Copy, Debug, Hash, PartialEq)]
+#[derive(Debug)]
 pub enum ServoError {
     /// The channel to the off-the-main-thread web engine has been lost. No further
     /// attempts to communicate will happen. This is an unrecoverable error in Servo.
@@ -13,6 +13,8 @@ pub enum ServoError {
     /// The devtools server, used to expose pages to remote web inspectors has failed
     /// to start.
     DevtoolsFailedToStart,
+    /// Failed to send response to delegate request.
+    ResponseFailedToSend(bincode::Error),
 }
 
 pub trait ServoDelegate {
diff --git a/components/servo/webview_delegate.rs b/components/servo/webview_delegate.rs
index 1a1d6e180a167..bb41b3e2c4107 100644
--- a/components/servo/webview_delegate.rs
+++ b/components/servo/webview_delegate.rs
@@ -17,6 +17,7 @@ use serde::Serialize;
 use url::Url;
 use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize};
 
+use crate::responders::ServoErrorSender;
 use crate::{ConstellationProxy, WebView};
 
 /// A request to navigate a [`WebView`] or one of its inner frames. This can be handled
@@ -95,6 +96,8 @@ impl<T: Serialize> Drop for IpcResponder<T> {
                 .default_response
                 .take()
                 .expect("Guaranteed by inherent impl");
+            // Don’t notify embedder about send errors for the default response,
+            // since they didn’t send anything and probably don’t care.
             let _ = self.response_sender.send(response);
         }
     }
@@ -122,22 +125,30 @@ impl PermissionRequest {
     }
 }
 
-pub struct AllowOrDenyRequest(IpcResponder<AllowOrDeny>);
+pub struct AllowOrDenyRequest(IpcResponder<AllowOrDeny>, ServoErrorSender);
 
 impl AllowOrDenyRequest {
     pub(crate) fn new(
         response_sender: IpcSender<AllowOrDeny>,
         default_response: AllowOrDeny,
+        error_sender: ServoErrorSender,
     ) -> Self {
-        Self(IpcResponder::new(response_sender, default_response))
+        Self(
+            IpcResponder::new(response_sender, default_response),
+            error_sender,
+        )
     }
 
     pub fn allow(mut self) {
-        let _ = self.0.send(AllowOrDeny::Allow);
+        if let Err(error) = self.0.send(AllowOrDeny::Allow) {
+            self.1.raise_response_send_error(error);
+        }
     }
 
     pub fn deny(mut self) {
-        let _ = self.0.send(AllowOrDeny::Deny);
+        if let Err(error) = self.0.send(AllowOrDeny::Deny) {
+            self.1.raise_response_send_error(error);
+        }
     }
 }
 
@@ -148,6 +159,7 @@ pub struct AuthenticationRequest {
     pub(crate) url: Url,
     pub(crate) for_proxy: bool,
     pub(crate) responder: IpcResponder<Option<AuthenticationResponse>>,
+    pub(crate) error_sender: ServoErrorSender,
 }
 
 impl AuthenticationRequest {
@@ -155,11 +167,13 @@ impl AuthenticationRequest {
         url: Url,
         for_proxy: bool,
         response_sender: IpcSender<Option<AuthenticationResponse>>,
+        error_sender: ServoErrorSender,
     ) -> Self {
         Self {
             url,
             for_proxy,
             responder: IpcResponder::new(response_sender, None),
+            error_sender,
         }
     }
 
@@ -173,9 +187,12 @@ impl AuthenticationRequest {
     }
     /// Respond to the [`AuthenticationRequest`] with the given username and password.
     pub fn authenticate(mut self, username: String, password: String) {
-        let _ = self
+        if let Err(error) = self
             .responder
-            .send(Some(AuthenticationResponse { username, password }));
+            .send(Some(AuthenticationResponse { username, password }))
+        {
+            self.error_sender.raise_response_send_error(error);
+        }
     }
 }
 
@@ -185,16 +202,19 @@ impl AuthenticationRequest {
 pub struct WebResourceLoad {
     pub request: WebResourceRequest,
     pub(crate) responder: IpcResponder<WebResourceResponseMsg>,
+    pub(crate) error_sender: ServoErrorSender,
 }
 
 impl WebResourceLoad {
     pub(crate) fn new(
         web_resource_request: WebResourceRequest,
         response_sender: IpcSender<WebResourceResponseMsg>,
+        error_sender: ServoErrorSender,
     ) -> Self {
         Self {
             request: web_resource_request,
             responder: IpcResponder::new(response_sender, WebResourceResponseMsg::DoNotIntercept),
+            error_sender,
         }
     }
 
@@ -205,11 +225,14 @@ impl WebResourceLoad {
     /// Intercept this [`WebResourceLoad`] and control the response via the returned
     /// [`InterceptedWebResourceLoad`].
     pub fn intercept(mut self, response: WebResourceResponse) -> InterceptedWebResourceLoad {
-        let _ = self.responder.send(WebResourceResponseMsg::Start(response));
+        if let Err(error) = self.responder.send(WebResourceResponseMsg::Start(response)) {
+            self.error_sender.raise_response_send_error(error);
+        }
         InterceptedWebResourceLoad {
             request: self.request.clone(),
             response_sender: self.responder.into_inner(),
             finished: false,
+            error_sender: self.error_sender,
         }
     }
 }
@@ -223,28 +246,38 @@ pub struct InterceptedWebResourceLoad {
     pub request: WebResourceRequest,
     pub(crate) response_sender: IpcSender<WebResourceResponseMsg>,
     pub(crate) finished: bool,
+    pub(crate) error_sender: ServoErrorSender,
 }
 
 impl InterceptedWebResourceLoad {
     /// Send a chunk of response body data. It's possible to make subsequent calls to
     /// this method when streaming body data.
     pub fn send_body_data(&self, data: Vec<u8>) {
-        let _ = self
+        if let Err(error) = self
             .response_sender
-            .send(WebResourceResponseMsg::SendBodyData(data));
+            .send(WebResourceResponseMsg::SendBodyData(data))
+        {
+            self.error_sender.raise_response_send_error(error);
+        }
     }
     /// Finish this [`InterceptedWebResourceLoad`] and complete the response.
     pub fn finish(mut self) {
-        let _ = self
+        if let Err(error) = self
             .response_sender
-            .send(WebResourceResponseMsg::FinishLoad);
+            .send(WebResourceResponseMsg::FinishLoad)
+        {
+            self.error_sender.raise_response_send_error(error);
+        }
         self.finished = true;
     }
     /// Cancel this [`InterceptedWebResourceLoad`], which will trigger a network error.
     pub fn cancel(mut self) {
-        let _ = self
+        if let Err(error) = self
             .response_sender
-            .send(WebResourceResponseMsg::CancelLoad);
+            .send(WebResourceResponseMsg::CancelLoad)
+        {
+            self.error_sender.raise_response_send_error(error);
+        }
         self.finished = true;
     }
 }
@@ -252,9 +285,12 @@ impl InterceptedWebResourceLoad {
 impl Drop for InterceptedWebResourceLoad {
     fn drop(&mut self) {
         if !self.finished {
-            let _ = self
+            if let Err(error) = self
                 .response_sender
-                .send(WebResourceResponseMsg::FinishLoad);
+                .send(WebResourceResponseMsg::FinishLoad)
+            {
+                self.error_sender.raise_response_send_error(error);
+            }
         }
     }
 }
@@ -430,3 +466,200 @@ pub trait WebViewDelegate {
 
 pub(crate) struct DefaultWebViewDelegate;
 impl WebViewDelegate for DefaultWebViewDelegate {}
+
+#[test]
+fn test_allow_deny_request() {
+    use ipc_channel::ipc;
+
+    use crate::ServoErrorChannel;
+
+    for default_response in [AllowOrDeny::Allow, AllowOrDeny::Deny] {
+        // Explicit allow yields allow and nothing else
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        request.allow();
+        assert_eq!(receiver.try_recv().ok(), Some(AllowOrDeny::Allow));
+        assert_eq!(receiver.try_recv().ok(), None);
+        assert!(errors.try_recv().is_none());
+
+        // Explicit deny yields deny and nothing else
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        request.deny();
+        assert_eq!(receiver.try_recv().ok(), Some(AllowOrDeny::Deny));
+        assert_eq!(receiver.try_recv().ok(), None);
+        assert!(errors.try_recv().is_none());
+
+        // No response yields default response and nothing else
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        drop(request);
+        assert_eq!(receiver.try_recv().ok(), Some(default_response));
+        assert_eq!(receiver.try_recv().ok(), None);
+        assert!(errors.try_recv().is_none());
+
+        // Explicit allow when receiver disconnected yields error
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        drop(receiver);
+        request.allow();
+        assert!(errors.try_recv().is_some());
+
+        // Explicit deny when receiver disconnected yields error
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        drop(receiver);
+        request.deny();
+        assert!(errors.try_recv().is_some());
+
+        // No response when receiver disconnected yields no error
+        let errors = ServoErrorChannel::default();
+        let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+        let request = AllowOrDenyRequest::new(sender, default_response, errors.sender());
+        drop(receiver);
+        drop(request);
+        assert!(errors.try_recv().is_none());
+    }
+}
+
+#[test]
+fn test_authentication_request() {
+    use ipc_channel::ipc;
+
+    use crate::ServoErrorChannel;
+
+    let url = Url::parse("https://example.com").expect("Guaranteed by argument");
+
+    // Explicit response yields that response and nothing else
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = AuthenticationRequest::new(url.clone(), false, sender, errors.sender());
+    request.authenticate("diffie".to_owned(), "hunter2".to_owned());
+    assert_eq!(
+        receiver.try_recv().ok(),
+        Some(Some(AuthenticationResponse {
+            username: "diffie".to_owned(),
+            password: "hunter2".to_owned(),
+        }))
+    );
+    assert_eq!(receiver.try_recv().ok(), None);
+    assert!(errors.try_recv().is_none());
+
+    // No response yields None response and nothing else
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = AuthenticationRequest::new(url.clone(), false, sender, errors.sender());
+    drop(request);
+    assert_eq!(receiver.try_recv().ok(), Some(None));
+    assert_eq!(receiver.try_recv().ok(), None);
+    assert!(errors.try_recv().is_none());
+
+    // Explicit response when receiver disconnected yields error
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = AuthenticationRequest::new(url.clone(), false, sender, errors.sender());
+    drop(receiver);
+    request.authenticate("diffie".to_owned(), "hunter2".to_owned());
+    assert!(errors.try_recv().is_some());
+
+    // No response when receiver disconnected yields no error
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = AuthenticationRequest::new(url.clone(), false, sender, errors.sender());
+    drop(receiver);
+    drop(request);
+    assert!(errors.try_recv().is_none());
+}
+
+#[test]
+fn test_web_resource_load() {
+    use http::{HeaderMap, Method, StatusCode};
+    use ipc_channel::ipc;
+
+    use crate::ServoErrorChannel;
+
+    let web_resource_request = || WebResourceRequest {
+        method: Method::GET,
+        headers: HeaderMap::default(),
+        url: Url::parse("https://example.com").expect("Guaranteed by argument"),
+        is_for_main_frame: false,
+        is_redirect: false,
+    };
+    let web_resource_response = || {
+        WebResourceResponse::new(Url::parse("https://diffie.test").expect("Guaranteed by argument"))
+            .status_code(StatusCode::IM_A_TEAPOT)
+    };
+
+    // Explicit intercept with explicit cancel yields Start and Cancel and nothing else
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    request.intercept(web_resource_response()).cancel();
+    assert!(matches!(
+        receiver.try_recv(),
+        Ok(WebResourceResponseMsg::Start(_))
+    ));
+    assert!(matches!(
+        receiver.try_recv(),
+        Ok(WebResourceResponseMsg::CancelLoad)
+    ));
+    assert!(matches!(receiver.try_recv(), Err(_)));
+    assert!(errors.try_recv().is_none());
+
+    // Explicit intercept with no further action yields Start and FinishLoad and nothing else
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    drop(request.intercept(web_resource_response()));
+    assert!(matches!(
+        receiver.try_recv(),
+        Ok(WebResourceResponseMsg::Start(_))
+    ));
+    assert!(matches!(
+        receiver.try_recv(),
+        Ok(WebResourceResponseMsg::FinishLoad)
+    ));
+    assert!(matches!(receiver.try_recv(), Err(_)));
+    assert!(errors.try_recv().is_none());
+
+    // No response yields DoNotIntercept and nothing else
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    drop(request);
+    assert!(matches!(
+        receiver.try_recv(),
+        Ok(WebResourceResponseMsg::DoNotIntercept)
+    ));
+    assert!(matches!(receiver.try_recv(), Err(_)));
+    assert!(errors.try_recv().is_none());
+
+    // Explicit intercept with explicit cancel when receiver disconnected yields error
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    drop(receiver);
+    request.intercept(web_resource_response()).cancel();
+    assert!(errors.try_recv().is_some());
+
+    // Explicit intercept with no further action when receiver disconnected yields error
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    drop(receiver);
+    drop(request.intercept(web_resource_response()));
+    assert!(errors.try_recv().is_some());
+
+    // No response when receiver disconnected yields no error
+    let errors = ServoErrorChannel::default();
+    let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel");
+    let request = WebResourceLoad::new(web_resource_request(), sender, errors.sender());
+    drop(receiver);
+    drop(request);
+    assert!(errors.try_recv().is_none());
+}
diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs
index c1c45c5241714..a0ec13b2f1ea4 100644
--- a/components/shared/embedder/lib.rs
+++ b/components/shared/embedder/lib.rs
@@ -150,7 +150,7 @@ pub enum SimpleDialog {
     },
 }
 
-#[derive(Debug, Default, Deserialize, Serialize)]
+#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
 pub struct AuthenticationResponse {
     /// Username for http request authentication
     pub username: String,
@@ -208,7 +208,7 @@ impl Default for PromptResponse {
 }
 
 /// A response to a request to allow or deny an action.
-#[derive(Clone, Copy, Deserialize, PartialEq, Serialize)]
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub enum AllowOrDeny {
     Allow,
     Deny,
diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py
index 155728b3a52b1..4f9b573182f8c 100644
--- a/python/servo/testing_commands.py
+++ b/python/servo/testing_commands.py
@@ -165,6 +165,7 @@ def test_unit(self, build_type: BuildType, test_name=None, package=None, bench=F
             "fonts",
             "hyper_serde",
             "layout_2020",
+            "libservo",
             "net",
             "net_traits",
             "pixels",


Reviews:
user: login: mrobinson
body: This is pretty close to what I was thinking! I wonder if should initially be simpler now though. It isn't actually so interesting to differentiate between the failure for a  `WebViewDelegate` versus the `ServoDelegate`. Right now, the backend is shared amongst all `WebViews`s, so I doubt the error recovery is any different between the two. At least, we can start with something much simpler and only have a single type of channel that doesn't know anything about `WebView`s. It will just report when communication has failed. Later, if we find that we need another channel for `WebView` we can add that.
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T14:17:09Z
user: login: delan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-14T09:28:46Z
user: login: mrobinson
body: Looks good to me with some minor nits. Sorry for the long wait on this one!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-18T18:56:53Z
user: login: delan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T04:58:51Z
user: login: delan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-19T04:58:58Z

Review comments:
diff_hunk: @@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/// Sender for errors raised by delegate request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) trait DelegateErrorSender {
+    fn raise_response_send_error(&self, error: bincode::Error);
+}
path: components/servo/delegate.rs
user: login: mrobinson
body: I have been thinking that we should move all the "responder" pattern structs to a file called `responders.rs`. Maybe this would be a good fit for that file rather than making a new one.
created_at: 2025-03-10T14:08:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,10 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+/// Sender for errors raised by delegate request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) trait DelegateErrorSender {
+    fn raise_response_send_error(&self, error: bincode::Error);
+}
path: components/servo/delegate.rs
user: login: delan
body: done in ba1257ea036775dba44226773c62e8f6c558edc6
created_at: 2025-03-14T09:28:46Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
+use log::warn;
+
+use crate::ServoError;
+
+/// Sender for errors raised by delegate request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorSender {
+    sender: Sender<ServoError>,
+}
+
+impl ServoErrorSender {
+    pub(crate) fn raise_response_send_error(&self, error: bincode::Error) {
+        if let Err(error) = self.sender.send(ServoError::ResponseSend(error)) {
+            warn!("Failed to send Servo error: {error:?}");
+        }
+    }
+}
+
+/// Channel for errors raised by [`WebViewDelegate`] request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorChannel {
+    sender: Sender<ServoError>,
+    receiver: Receiver<ServoError>,
+}
+
+impl Default for ServoErrorChannel {
+    fn default() -> Self {
+        let (sender, receiver) = unbounded();
+        Self { sender, receiver }
+    }
+}
+
+impl ServoErrorChannel {
+    pub(crate) fn sender(&self) -> ServoErrorSender {
+        ServoErrorSender {
+            sender: self.sender.clone(),
+        }
+    }
path: components/servo/responders.rs
user: login: mrobinson
body: Nit: missing space
created_at: 2025-03-18T18:54:42Z
author_association: MEMBER
diff_hunk: @@ -5,14 +5,16 @@
 use crate::Servo;
 use crate::webview_delegate::{AllowOrDenyRequest, WebResourceLoad};
 
-#[derive(Clone, Copy, Debug, Hash, PartialEq)]
+#[derive(Debug)]
 pub enum ServoError {
     /// The channel to the off-the-main-thread web engine has been lost. No further
     /// attempts to communicate will happen. This is an unrecoverable error in Servo.
     LostConnectionWithBackend,
     /// The devtools server, used to expose pages to remote web inspectors has failed
     /// to start.
     DevtoolsFailedToStart,
+    /// Failed to send response to delegate request.
+    ResponseSend(bincode::Error),
path: components/servo/servo_delegate.rs
user: login: mrobinson
body: `ResponseFailedToSend` to match the naming of the other variants?
created_at: 2025-03-18T18:55:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
+use log::warn;
+
+use crate::ServoError;
+
+/// Sender for errors raised by delegate request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorSender {
+    sender: Sender<ServoError>,
+}
+
+impl ServoErrorSender {
+    pub(crate) fn raise_response_send_error(&self, error: bincode::Error) {
+        if let Err(error) = self.sender.send(ServoError::ResponseSend(error)) {
+            warn!("Failed to send Servo error: {error:?}");
+        }
+    }
+}
+
+/// Channel for errors raised by [`WebViewDelegate`] request objects.
+///
+/// This allows errors to be raised asynchronously.
+pub(crate) struct ServoErrorChannel {
+    sender: Sender<ServoError>,
+    receiver: Receiver<ServoError>,
+}
+
+impl Default for ServoErrorChannel {
+    fn default() -> Self {
+        let (sender, receiver) = unbounded();
+        Self { sender, receiver }
+    }
+}
+
+impl ServoErrorChannel {
+    pub(crate) fn sender(&self) -> ServoErrorSender {
+        ServoErrorSender {
+            sender: self.sender.clone(),
+        }
+    }
path: components/servo/responders.rs
user: login: delan
body: done in ae523bf518fa641fb89d685b99a9e8f15508708d
created_at: 2025-03-19T04:58:51Z
author_association: MEMBER
diff_hunk: @@ -5,14 +5,16 @@
 use crate::Servo;
 use crate::webview_delegate::{AllowOrDenyRequest, WebResourceLoad};
 
-#[derive(Clone, Copy, Debug, Hash, PartialEq)]
+#[derive(Debug)]
 pub enum ServoError {
     /// The channel to the off-the-main-thread web engine has been lost. No further
     /// attempts to communicate will happen. This is an unrecoverable error in Servo.
     LostConnectionWithBackend,
     /// The devtools server, used to expose pages to remote web inspectors has failed
     /// to start.
     DevtoolsFailedToStart,
+    /// Failed to send response to delegate request.
+    ResponseSend(bincode::Error),
path: components/servo/servo_delegate.rs
user: login: delan
body: done in ae523bf518fa641fb89d685b99a9e8f15508708d
created_at: 2025-03-19T04:58:58Z
author_association: MEMBER

PR comments:
user: login: delan
created_at: 2025-02-28T05:18:18Z
body: @mrobinson this isn’t quite polished yet, but I would appreciate some feedback on the general approach! I remember you mentioning we could use a crossbeam channel or shared state to convey errors from request objects back to the delegates that created them, so I went with a crossbeam channel.
author_association: MEMBER
user: login: delan
created_at: 2025-03-14T10:06:59Z
body: > At least, we can start with something much simpler and only have a single type of channel that doesn't know anything about `WebView`s. It will just report when communication has failed.

Done in 0576256b5db3e8639336b2f4c4456fc36d481431 + 13b9f5d6b9fe923babc09ce2ac592d4747f50f50 :)
author_association: MEMBER
user: login: delan
created_at: 2025-03-19T04:59:05Z
body: Let me see if I can add some tests…
author_association: MEMBER
user: login: delan
created_at: 2025-03-19T06:30:27Z
body: Done!
author_association: MEMBER