Repository: nodejs/node
Pull request: inspector: return errors when CDP protocol event emission fails

Previously, there was no feedback when functions that emit events such as `Network.webSocketCreated` failed. This change modifies the behavior so that feedback can be obtained when such failures occur.

Since an event may be emitted to multiple sessions, instead of throwing an error directly, the errors are returned as an array.

<!--
Before submitting a pull request, please read:

- the CONTRIBUTING guide at https://github.com/nodejs/node/blob/HEAD/CONTRIBUTING.md
- the commit message formatting guidelines at
  https://github.com/nodejs/node/blob/HEAD/doc/contributing/pull-requests.md#commit-message-guidelines

For code changes:
1. Include tests for any bug fixes or new features.
2. Update documentation if relevant.
3. Ensure that `make -j4 test` (UNIX), or `vcbuild test` (Windows) passes.

If you believe this PR should be highlighted in the Node.js CHANGELOG
please add the `notable-change` label.

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
    have the right to submit it under the open source license
    indicated in the file; or

(b) The contribution is based upon previous work that, to the best
    of my knowledge, is covered under an appropriate open source
    license and I have the right under that license to submit that
    work with modifications, whether created in whole or in part
    by me, under the same open source license (unless I am
    permitted to submit under a different license), as indicated
    in the file; or

(c) The contribution was provided directly to me by some other
    person who certified (a), (b) or (c) and I have not modified
    it.

(d) I understand and agree that this project and the contribution
    are public and that a record of the contribution (including all
    personal information I submit with it, including my sign-off) is
    maintained indefinitely and may be redistributed consistent with
    this project or the open source license(s) involved.
-->


Changed files:
filename: src/inspector/dom_storage_agent.cc
patch: @@ -17,18 +17,26 @@ using v8::Local;
 using v8::Object;
 using v8::Value;
 
+static void ThrowEventError(v8::Isolate* isolate, const std::string& message) {
+  isolate->ThrowException(v8::Exception::TypeError(
+      v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked()));
+}
+
 std::unique_ptr<protocol::DOMStorage::StorageId> createStorageIdFromObject(
     Local<Context> context, Local<Object> storage_id_obj) {
   protocol::String security_origin;
+  Isolate* isolate = Isolate::GetCurrent();
   if (!ObjectGetProtocolString(context, storage_id_obj, "securityOrigin")
            .To(&security_origin)) {
+    ThrowEventError(isolate, "Missing securityOrigin in storageId");
     return {};
   }
   bool is_local_storage =
       ObjectGetBool(context, storage_id_obj, "isLocalStorage").FromMaybe(false);
   protocol::String storageKey;
   if (!ObjectGetProtocolString(context, storage_id_obj, "storageKey")
            .To(&storageKey)) {
+    ThrowEventError(isolate, "Missing storageKey in storageId");
     return {};
   }
 
@@ -135,8 +143,10 @@ protocol::DispatchResponse DOMStorageAgent::clear(
 
 void DOMStorageAgent::domStorageItemAdded(Local<Context> context,
                                           Local<Object> params) {
+  Isolate* isolate = env_->isolate();
   Local<Object> storage_id_obj;
   if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
+    ThrowEventError(isolate, "Missing storageId in event");
     return;
   }
 
@@ -148,19 +158,23 @@ void DOMStorageAgent::domStorageItemAdded(Local<Context> context,
 
   protocol::String key;
   if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
+    ThrowEventError(isolate, "Missing key in event");
     return;
   }
   protocol::String new_value;
   if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) {
+    ThrowEventError(isolate, "Missing newValue in event");
     return;
   }
   frontend_->domStorageItemAdded(std::move(storage_id), key, new_value);
 }
 
 void DOMStorageAgent::domStorageItemRemoved(Local<Context> context,
                                             Local<Object> params) {
+  Isolate* isolate = env_->isolate();
   Local<Object> storage_id_obj;
   if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
+    ThrowEventError(isolate, "Missing storageId in event");
     return;
   }
   std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
@@ -172,15 +186,18 @@ void DOMStorageAgent::domStorageItemRemoved(Local<Context> context,
 
   protocol::String key;
   if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
+    ThrowEventError(isolate, "Missing key in event");
     return;
   }
   frontend_->domStorageItemRemoved(std::move(storage_id), key);
 }
 
 void DOMStorageAgent::domStorageItemUpdated(Local<Context> context,
                                             Local<Object> params) {
+  Isolate* isolate = env_->isolate();
   Local<Object> storage_id_obj;
   if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
+    ThrowEventError(isolate, "Missing storageId in event");
     return;
   }
 
@@ -193,14 +210,17 @@ void DOMStorageAgent::domStorageItemUpdated(Local<Context> context,
 
   protocol::String key;
   if (!ObjectGetProtocolString(context, params, "key").To(&key)) {
+    ThrowEventError(isolate, "Missing key in event");
     return;
   }
   protocol::String old_value;
   if (!ObjectGetProtocolString(context, params, "oldValue").To(&old_value)) {
+    ThrowEventError(isolate, "Missing oldValue in event");
     return;
   }
   protocol::String new_value;
   if (!ObjectGetProtocolString(context, params, "newValue").To(&new_value)) {
+    ThrowEventError(isolate, "Missing newValue in event");
     return;
   }
   frontend_->domStorageItemUpdated(
@@ -209,8 +229,10 @@ void DOMStorageAgent::domStorageItemUpdated(Local<Context> context,
 
 void DOMStorageAgent::domStorageItemsCleared(Local<Context> context,
                                              Local<Object> params) {
+  Isolate* isolate = env_->isolate();
   Local<Object> storage_id_obj;
   if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) {
+    ThrowEventError(isolate, "Missing storageId in event");
     return;
   }
   std::unique_ptr<protocol::DOMStorage::StorageId> storage_id =
@@ -228,27 +250,32 @@ void DOMStorageAgent::registerStorage(Local<Context> context,
   HandleScope handle_scope(isolate);
   bool is_local_storage;
   if (!ObjectGetBool(context, params, "isLocalStorage").To(&is_local_storage)) {
+    ThrowEventError(isolate, "Missing isLocalStorage in event");
     return;
   }
   Local<Object> storage_map_obj;
   if (!ObjectGetObject(context, params, "storageMap")
            .ToLocal(&storage_map_obj)) {
+    ThrowEventError(isolate, "Missing storageMap in event");
     return;
   }
   StorageMap& storage_map =
       is_local_storage ? local_storage_map_ : session_storage_map_;
   Local<Array> property_names;
   if (!storage_map_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) {
+    ThrowEventError(isolate, "Failed to get property names from storageMap");
     return;
   }
   uint32_t length = property_names->Length();
   for (uint32_t i = 0; i < length; ++i) {
     Local<Value> key_value;
     if (!property_names->Get(context, i).ToLocal(&key_value)) {
+      ThrowEventError(isolate, "Failed to get key from storageMap");
       return;
     }
     Local<Value> value_value;
     if (!storage_map_obj->Get(context, key_value).ToLocal(&value_value)) {
+      ThrowEventError(isolate, "Failed to get value from storageMap");
       return;
     }
     node::TwoByteValue key_utf16(isolate, key_value);

filename: src/inspector/network_agent.cc
patch: @@ -24,28 +24,38 @@ using v8::Value;
 
 constexpr size_t kDefaultMaxTotalBufferSize = 100 * 1024 * 1024;  // 100MB
 
+static void ThrowEventError(v8::Isolate* isolate, const std::string& message) {
+  isolate->ThrowException(v8::Exception::TypeError(
+      v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked()));
+}
+
 // Create a protocol::Network::Headers from the v8 object.
-std::unique_ptr<protocol::Network::Headers> createHeadersFromObject(
-    v8::Local<v8::Context> context, Local<Object> headers_obj) {
+std::unique_ptr<protocol::Network::Headers>
+NetworkAgent::createHeadersFromObject(v8::Local<v8::Context> context,
+                                      Local<Object> headers_obj) {
   HandleScope handle_scope(Isolate::GetCurrent());
+  Isolate* isolate = env_->isolate();
 
   std::unique_ptr<protocol::DictionaryValue> dict =
       protocol::DictionaryValue::create();
   Local<v8::Array> property_names;
   if (!headers_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) {
+    ThrowEventError(isolate, "Missing response headers in event");
     return {};
   }
 
   for (size_t idx = 0; idx < property_names->Length(); idx++) {
     Local<v8::Value> property_name_val;
     if (!property_names->Get(context, idx).ToLocal(&property_name_val) ||
         !property_name_val->IsString()) {
+      ThrowEventError(isolate, "Invalid header name in event");
       return {};
     }
     Local<v8::String> property_name = property_name_val.As<v8::String>();
     protocol::String property_value;
     if (!ObjectGetProtocolString(context, headers_obj, property_name)
              .To(&property_value)) {
+      ThrowEventError(isolate, "Invalid header value in event");
       return {};
     }
     dict->setString(ToProtocolString(Isolate::GetCurrent(), property_name),
@@ -56,19 +66,24 @@ std::unique_ptr<protocol::Network::Headers> createHeadersFromObject(
 }
 
 // Create a protocol::Network::Request from the v8 object.
-std::unique_ptr<protocol::Network::Request> createRequestFromObject(
-    v8::Local<v8::Context> context, Local<Object> request) {
+std::unique_ptr<protocol::Network::Request>
+NetworkAgent::createRequestFromObject(v8::Local<v8::Context> context,
+                                      Local<Object> request) {
   HandleScope handle_scope(Isolate::GetCurrent());
+  Isolate* isolate = env_->isolate();
   protocol::String url;
   if (!ObjectGetProtocolString(context, request, "url").To(&url)) {
+    ThrowEventError(isolate, "Missing request.url in event");
     return {};
   }
   protocol::String method;
   if (!ObjectGetProtocolString(context, request, "method").To(&method)) {
+    ThrowEventError(isolate, "Missing request.method in event");
     return {};
   }
   Local<Object> headers_obj;
   if (!ObjectGetObject(context, request, "headers").ToLocal(&headers_obj)) {
+    ThrowEventError(isolate, "Missing request.headers in event");
     return {};
   }
   std::unique_ptr<protocol::Network::Headers> headers =
@@ -88,24 +103,30 @@ std::unique_ptr<protocol::Network::Request> createRequestFromObject(
 }
 
 // Create a protocol::Network::Response from the v8 object.
-std::unique_ptr<protocol::Network::Response> createResponseFromObject(
-    v8::Local<v8::Context> context, Local<Object> response) {
+std::unique_ptr<protocol::Network::Response>
+NetworkAgent::createResponseFromObject(v8::Local<v8::Context> context,
+                                       Local<Object> response) {
   HandleScope handle_scope(Isolate::GetCurrent());
+  Isolate* isolate = env_->isolate();
   protocol::String url;
   if (!ObjectGetProtocolString(context, response, "url").To(&url)) {
+    ThrowEventError(isolate, "Missing response.url in event");
     return {};
   }
   int status;
   if (!ObjectGetInt(context, response, "status").To(&status)) {
+    ThrowEventError(isolate, "Missing response.status in event");
     return {};
   }
   protocol::String statusText;
   if (!ObjectGetProtocolString(context, response, "statusText")
            .To(&statusText)) {
+    ThrowEventError(isolate, "Missing response.statusText in event");
     return {};
   }
   Local<Object> headers_obj;
   if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) {
+    ThrowEventError(isolate, "Missing response.headers in event");
     return {};
   }
   std::unique_ptr<protocol::Network::Headers> headers =
@@ -129,20 +150,25 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
       .build();
 }
 
-std::unique_ptr<protocol::Network::WebSocketResponse> createWebSocketResponse(
-    v8::Local<v8::Context> context, Local<Object> response) {
+std::unique_ptr<protocol::Network::WebSocketResponse>
+NetworkAgent::createWebSocketResponse(v8::Local<v8::Context> context,
+                                      Local<Object> response) {
   HandleScope handle_scope(Isolate::GetCurrent());
+  Isolate* isolate = env_->isolate();
   int status;
   if (!ObjectGetInt(context, response, "status").To(&status)) {
+    ThrowEventError(isolate, "Missing response.status in event");
     return {};
   }
   protocol::String statusText;
   if (!ObjectGetProtocolString(context, response, "statusText")
            .To(&statusText)) {
+    ThrowEventError(isolate, "Missing response.statusText in event");
     return {};
   }
   Local<Object> headers_obj;
   if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) {
+    ThrowEventError(isolate, "Missing response.headers in event");
     return {};
   }
   std::unique_ptr<protocol::Network::Headers> headers =
@@ -182,12 +208,15 @@ NetworkAgent::NetworkAgent(
 
 void NetworkAgent::webSocketCreated(v8::Local<v8::Context> context,
                                     v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   protocol::String url;
   if (!ObjectGetProtocolString(context, params, "url").To(&url)) {
+    ThrowEventError(isolate, "Missing url in event");
     return;
   }
   std::unique_ptr<protocol::Network::Initiator> initiator =
@@ -201,29 +230,36 @@ void NetworkAgent::webSocketCreated(v8::Local<v8::Context> context,
 
 void NetworkAgent::webSocketClosed(v8::Local<v8::Context> context,
                                    v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   frontend_->webSocketClosed(request_id, timestamp);
 }
 
 void NetworkAgent::webSocketHandshakeResponseReceived(
     v8::Local<v8::Context> context, v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   Local<Object> response_obj;
   if (!ObjectGetObject(context, params, "response").ToLocal(&response_obj)) {
+    ThrowEventError(isolate, "Missing response in event");
     return;
   }
   auto response = createWebSocketResponse(context, response_obj);
@@ -395,22 +431,27 @@ protocol::DispatchResponse NetworkAgent::loadNetworkResource(
 
 void NetworkAgent::requestWillBeSent(v8::Local<v8::Context> context,
                                      v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   double wall_time;
   if (!ObjectGetDouble(context, params, "wallTime").To(&wall_time)) {
+    ThrowEventError(isolate, "Missing wallTime in event");
     return;
   }
   protocol::String charset =
       ObjectGetProtocolString(context, params, "charset").FromMaybe("");
   Local<v8::Object> request_obj;
   if (!ObjectGetObject(context, params, "request").ToLocal(&request_obj)) {
+    ThrowEventError(isolate, "Missing request in event");
     return;
   }
   std::unique_ptr<protocol::Network::Request> request =
@@ -446,20 +487,25 @@ void NetworkAgent::requestWillBeSent(v8::Local<v8::Context> context,
 
 void NetworkAgent::responseReceived(v8::Local<v8::Context> context,
                                     v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   protocol::String type;
   if (!ObjectGetProtocolString(context, params, "type").To(&type)) {
+    ThrowEventError(isolate, "Missing type in event");
     return;
   }
   Local<Object> response_obj;
   if (!ObjectGetObject(context, params, "response").ToLocal(&response_obj)) {
+    ThrowEventError(isolate, "Missing response in event");
     return;
   }
   auto response = createResponseFromObject(context, response_obj);
@@ -479,20 +525,25 @@ void NetworkAgent::responseReceived(v8::Local<v8::Context> context,
 
 void NetworkAgent::loadingFailed(v8::Local<v8::Context> context,
                                  v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   protocol::String type;
   if (!ObjectGetProtocolString(context, params, "type").To(&type)) {
+    ThrowEventError(isolate, "Missing type in event");
     return;
   }
   protocol::String error_text;
   if (!ObjectGetProtocolString(context, params, "errorText").To(&error_text)) {
+    ThrowEventError(isolate, "Missing errorText in event");
     return;
   }
 
@@ -503,12 +554,15 @@ void NetworkAgent::loadingFailed(v8::Local<v8::Context> context,
 
 void NetworkAgent::loadingFinished(v8::Local<v8::Context> context,
                                    Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
 
@@ -530,8 +584,10 @@ void NetworkAgent::loadingFinished(v8::Local<v8::Context> context,
 
 void NetworkAgent::dataSent(v8::Local<v8::Context> context,
                             v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
 
@@ -550,17 +606,21 @@ void NetworkAgent::dataSent(v8::Local<v8::Context> context,
 
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   int data_length;
   if (!ObjectGetInt(context, params, "dataLength").To(&data_length)) {
+    ThrowEventError(isolate, "Missing dataLength in event");
     return;
   }
   Local<Object> data_obj;
   if (!ObjectGetObject(context, params, "data").ToLocal(&data_obj)) {
+    ThrowEventError(isolate, "Missing data in event");
     return;
   }
   if (!data_obj->IsUint8Array()) {
+    ThrowEventError(isolate, "Expected data to be Uint8Array in event");
     return;
   }
   Local<Uint8Array> data = data_obj.As<Uint8Array>();
@@ -570,28 +630,35 @@ void NetworkAgent::dataSent(v8::Local<v8::Context> context,
 
 void NetworkAgent::dataReceived(v8::Local<v8::Context> context,
                                 v8::Local<v8::Object> params) {
+  Isolate* isolate = env_->isolate();
   protocol::String request_id;
   if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
+    ThrowEventError(isolate, "Missing requestId in event");
     return;
   }
   double timestamp;
   if (!ObjectGetDouble(context, params, "timestamp").To(&timestamp)) {
+    ThrowEventError(isolate, "Missing timestamp in event");
     return;
   }
   int data_length;
   if (!ObjectGetInt(context, params, "dataLength").To(&data_length)) {
+    ThrowEventError(isolate, "Missing dataLength in event");
     return;
   }
   int encoded_data_length;
   if (!ObjectGetInt(context, params, "encodedDataLength")
            .To(&encoded_data_length)) {
+    ThrowEventError(isolate, "Missing encodedDataLength in event");
     return;
   }
   Local<Object> data_obj;
   if (!ObjectGetObject(context, params, "data").ToLocal(&data_obj)) {
+    ThrowEventError(isolate, "Missing data in event");
     return;
   }
   if (!data_obj->IsUint8Array()) {
+    ThrowEventError(isolate, "Expected data to be Uint8Array in event");
     return;
   }
   Local<Uint8Array> data = data_obj.As<Uint8Array>();

filename: src/inspector/network_agent.h
patch: @@ -79,6 +79,15 @@ class NetworkAgent : public protocol::Network::Backend {
                                           v8::Local<v8::Object> params);
 
  private:
+  std::unique_ptr<protocol::Network::Headers> createHeadersFromObject(
+      v8::Local<v8::Context> context, v8::Local<v8::Object> headers_obj);
+  std::unique_ptr<protocol::Network::Request> createRequestFromObject(
+      v8::Local<v8::Context> context, v8::Local<v8::Object> request);
+  std::unique_ptr<protocol::Network::Response> createResponseFromObject(
+      v8::Local<v8::Context> context, v8::Local<v8::Object> response);
+  std::unique_ptr<protocol::Network::WebSocketResponse> createWebSocketResponse(
+      v8::Local<v8::Context> context, v8::Local<v8::Object> response);
+
   NetworkInspector* inspector_;
   v8_inspector::V8Inspector* v8_inspector_;
   std::shared_ptr<protocol::Network::Frontend> frontend_;

filename: test/parallel/test-inspector-emit-protocol-event-errors.js
patch: @@ -0,0 +1,503 @@
+// Flags: --inspect=0 --experimental-network-inspection --experimental-storage-inspection
+'use strict';
+const common = require('../common');
+
+common.skipIfInspectorDisabled();
+
+const inspector = require('node:inspector/promises');
+const assert = require('node:assert');
+
+function omit(object, ...keys) {
+  const copy = { ...object };
+  for (const key of keys) {
+    delete copy[key];
+  }
+  return copy;
+}
+
+function networkRequest(overrides = {}) {
+  return {
+    requestId: 'request-id',
+    timestamp: 1000,
+    wallTime: 1000,
+    request: {
+      url: 'https://nodejs.org/en',
+      method: 'GET',
+      headers: {},
+    },
+    ...overrides,
+  };
+}
+
+function networkResponse(overrides = {}) {
+  return {
+    requestId: 'response-id',
+    timestamp: 1000,
+    type: 'Other',
+    response: {
+      url: 'https://nodejs.org/en',
+      status: 200,
+      statusText: 'OK',
+      headers: {},
+    },
+    ...overrides,
+  };
+}
+
+function loadingFailed(overrides = {}) {
+  return {
+    requestId: 'loading-failed-id',
+    timestamp: 1000,
+    type: 'Document',
+    errorText: 'Failed to load resource',
+    ...overrides,
+  };
+}
+
+function loadingFinished(overrides = {}) {
+  return {
+    requestId: 'loading-finished-id',
+    timestamp: 1000,
+    ...overrides,
+  };
+}
+
+function webSocketCreated(overrides = {}) {
+  return {
+    requestId: 'websocket-created-id',
+    url: 'ws://example.com:8080',
+    ...overrides,
+  };
+}
+
+function webSocketClosed(overrides = {}) {
+  return {
+    requestId: 'websocket-closed-id',
+    timestamp: 1000,
+    ...overrides,
+  };
+}
+
+function webSocketResponse(overrides = {}) {
+  return {
+    requestId: 'websocket-response-id',
+    timestamp: 1000,
+    response: {
+      status: 101,
+      statusText: 'Switching Protocols',
+      headers: {},
+    },
+    ...overrides,
+  };
+}
+
+function storageId(overrides = {}) {
+  return {
+    securityOrigin: '',
+    isLocalStorage: true,
+    storageKey: 'node-inspector://default-dom-storage',
+    ...overrides,
+  };
+}
+
+function domStorageItemAdded(overrides = {}) {
+  return {
+    storageId: storageId(),
+    key: 'testKey',
+    newValue: 'testValue',
+    ...overrides,
+  };
+}
+
+function domStorageItemRemoved(overrides = {}) {
+  return {
+    storageId: storageId(),
+    key: 'testKey',
+    ...overrides,
+  };
+}
+
+function domStorageItemUpdated(overrides = {}) {
+  return {
+    storageId: storageId(),
+    key: 'testKey',
+    oldValue: 'oldValue',
+    newValue: 'newValue',
+    ...overrides,
+  };
+}
+
+function registerStorage(overrides = {}) {
+  return {
+    isLocalStorage: true,
+    storageMap: {},
+    ...overrides,
+  };
+}
+
+const NETWORK_ERROR_CASES = [
+  [
+    'requestWillBeSent',
+    omit(networkRequest(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'requestWillBeSent',
+    omit(networkRequest(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+  [
+    'requestWillBeSent',
+    omit(networkRequest(), 'wallTime'),
+    'Missing wallTime in event',
+  ],
+  [
+    'requestWillBeSent',
+    omit(networkRequest(), 'request'),
+    'Missing request in event',
+  ],
+  [
+    'requestWillBeSent',
+    networkRequest({ request: omit(networkRequest().request, 'url') }),
+    'Missing request.url in event',
+  ],
+  [
+    'requestWillBeSent',
+    networkRequest({ request: omit(networkRequest().request, 'method') }),
+    'Missing request.method in event',
+  ],
+  [
+    'requestWillBeSent',
+    networkRequest({ request: omit(networkRequest().request, 'headers') }),
+    'Missing request.headers in event',
+  ],
+
+  [
+    'responseReceived',
+    omit(networkResponse(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'responseReceived',
+    omit(networkResponse(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+  [
+    'responseReceived',
+    omit(networkResponse(), 'type'),
+    'Missing type in event',
+  ],
+  [
+    'responseReceived',
+    omit(networkResponse(), 'response'),
+    'Missing response in event',
+  ],
+  [
+    'responseReceived',
+    networkResponse({ response: omit(networkResponse().response, 'url') }),
+    'Missing response.url in event',
+  ],
+  [
+    'responseReceived',
+    networkResponse({ response: omit(networkResponse().response, 'status') }),
+    'Missing response.status in event',
+  ],
+  [
+    'responseReceived',
+    networkResponse({
+      response: omit(networkResponse().response, 'statusText'),
+    }),
+    'Missing response.statusText in event',
+  ],
+  [
+    'responseReceived',
+    networkResponse({ response: omit(networkResponse().response, 'headers') }),
+    'Missing response.headers in event',
+  ],
+  [
+    'responseReceived',
+    networkResponse({
+      response: { ...networkResponse().response, headers: { host: 1 } },
+    }),
+    'Invalid header value in event',
+  ],
+
+  [
+    'loadingFailed',
+    omit(loadingFailed(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'loadingFailed',
+    omit(loadingFailed(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+  ['loadingFailed', omit(loadingFailed(), 'type'), 'Missing type in event'],
+  [
+    'loadingFailed',
+    omit(loadingFailed(), 'errorText'),
+    'Missing errorText in event',
+  ],
+
+  [
+    'loadingFinished',
+    omit(loadingFinished(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'loadingFinished',
+    omit(loadingFinished(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+
+  [
+    'webSocketCreated',
+    omit(webSocketCreated(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  ['webSocketCreated', omit(webSocketCreated(), 'url'), 'Missing url in event'],
+
+  [
+    'webSocketClosed',
+    omit(webSocketClosed(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'webSocketClosed',
+    omit(webSocketClosed(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+
+  [
+    'webSocketHandshakeResponseReceived',
+    omit(webSocketResponse(), 'requestId'),
+    'Missing requestId in event',
+  ],
+  [
+    'webSocketHandshakeResponseReceived',
+    omit(webSocketResponse(), 'timestamp'),
+    'Missing timestamp in event',
+  ],
+  [
+    'webSocketHandshakeResponseReceived',
+    omit(webSocketResponse(), 'response'),
+    'Missing response in event',
+  ],
+  [
+    'webSocketHandshakeResponseReceived',
+    webSocketResponse({
+      response: omit(webSocketResponse().response, 'status'),
+    }),
+    'Missing response.status in event',
+  ],
+  [
+    'webSocketHandshakeResponseReceived',
+    webSocketResponse({
+      response: omit(webSocketResponse().response, 'statusText'),
+    }),
+    'Missing response.statusText in event',
+  ],
+  [
+    'webSocketHandshakeResponseReceived',
+    webSocketResponse({
+      response: omit(webSocketResponse().response, 'headers'),
+    }),
+    'Missing response.headers in event',
+  ],
+];
+
+const DOM_STORAGE_ERROR_CASES = [
+  [
+    'domStorageItemAdded',
+    omit(domStorageItemAdded(), 'storageId'),
+    'Missing storageId in event',
+  ],
+  [
+    'domStorageItemAdded',
+    domStorageItemAdded({ storageId: omit(storageId(), 'securityOrigin') }),
+    'Missing securityOrigin in storageId',
+  ],
+  [
+    'domStorageItemAdded',
+    domStorageItemAdded({ storageId: omit(storageId(), 'storageKey') }),
+    'Missing storageKey in storageId',
+  ],
+  [
+    'domStorageItemAdded',
+    omit(domStorageItemAdded(), 'key'),
+    'Missing key in event',
+  ],
+  [
+    'domStorageItemAdded',
+    omit(domStorageItemAdded(), 'newValue'),
+    'Missing newValue in event',
+  ],
+
+  [
+    'domStorageItemRemoved',
+    omit(domStorageItemRemoved(), 'storageId'),
+    'Missing storageId in event',
+  ],
+  [
+    'domStorageItemRemoved',
+    omit(domStorageItemRemoved(), 'key'),
+    'Missing key in event',
+  ],
+
+  [
+    'domStorageItemUpdated',
+    omit(domStorageItemUpdated(), 'storageId'),
+    'Missing storageId in event',
+  ],
+  [
+    'domStorageItemUpdated',
+    omit(domStorageItemUpdated(), 'key'),
+    'Missing key in event',
+  ],
+  [
+    'domStorageItemUpdated',
+    omit(domStorageItemUpdated(), 'oldValue'),
+    'Missing oldValue in event',
+  ],
+  [
+    'domStorageItemUpdated',
+    omit(domStorageItemUpdated(), 'newValue'),
+    'Missing newValue in event',
+  ],
+
+  ['domStorageItemsCleared', {}, 'Missing storageId in event'],
+
+  [
+    'registerStorage',
+    omit(registerStorage(), 'isLocalStorage'),
+    'Missing isLocalStorage in event',
+  ],
+  [
+    'registerStorage',
+    omit(registerStorage(), 'storageMap'),
+    'Missing storageMap in event',
+  ],
+  [
+    'registerStorage',
+    registerStorage({
+      storageMap: new Proxy(
+        {},
+        {
+          ownKeys() {
+            throw new Error('boom');
+          },
+        },
+      ),
+    }),
+    'Failed to get property names from storageMap',
+  ],
+  [
+    'registerStorage',
+    registerStorage({
+      storageMap: new Proxy(
+        { testKey: 'testValue' },
+        {
+          get(target, property, receiver) {
+            if (property === 'testKey') throw new Error('boom');
+            return Reflect.get(target, property, receiver);
+          },
+        },
+      ),
+    }),
+    'Failed to get value from storageMap',
+  ],
+];
+
+const DATA_SENT_REQUEST_ID = 'data-sent-id';
+const DATA_SENT_ERROR_CASES = [
+  [{ finished: false }, 'Missing requestId in event'],
+  [{ requestId: DATA_SENT_REQUEST_ID }, 'Missing timestamp in event'],
+  [
+    { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000 },
+    'Missing dataLength in event',
+  ],
+  [
+    { requestId: DATA_SENT_REQUEST_ID, timestamp: 1000, dataLength: 1 },
+    'Missing data in event',
+  ],
+  [
+    {
+      requestId: DATA_SENT_REQUEST_ID,
+      timestamp: 1000,
+      dataLength: 1,
+      data: {},
+    },
+    'Expected data to be Uint8Array in event',
+  ],
+];
+
+const DATA_RECEIVED_ERROR_CASES = [
+  [{}, 'Missing requestId in event'],
+  [{ requestId: 'data-received-id' }, 'Missing timestamp in event'],
+  [
+    { requestId: 'data-received-id', timestamp: 1000 },
+    'Missing dataLength in event',
+  ],
+  [
+    { requestId: 'data-received-id', timestamp: 1000, dataLength: 1 },
+    'Missing encodedDataLength in event',
+  ],
+  [
+    {
+      requestId: 'data-received-id',
+      timestamp: 1000,
+      dataLength: 1,
+      encodedDataLength: 1,
+    },
+    'Missing data in event',
+  ],
+  [
+    {
+      requestId: 'data-received-id',
+      timestamp: 1000,
+      dataLength: 1,
+      encodedDataLength: 1,
+      data: {},
+    },
+    'Expected data to be Uint8Array in event',
+  ],
+];
+
+function assertEventErrors(domain, name, params, message) {
+  assert.throws(
+    () => inspector[domain][name](params),
+    {
+      message,
+    },
+    `Expected ${domain}.${name} to throw`,
+  );
+}
+
+function startRequest(requestId) {
+  inspector.Network.requestWillBeSent(networkRequest({ requestId }));
+}
+
+(async () => {
+  const session = new inspector.Session();
+  session.connect();
+
+  await session.post('Network.enable');
+  await session.post('DOMStorage.enable');
+
+  for (const [name, params, message] of NETWORK_ERROR_CASES) {
+    assertEventErrors('Network', name, params, message);
+  }
+
+  startRequest(DATA_SENT_REQUEST_ID);
+  for (const [params, message] of DATA_SENT_ERROR_CASES) {
+    assertEventErrors('Network', 'dataSent', params, message);
+  }
+
+  for (const [params, message] of DATA_RECEIVED_ERROR_CASES) {
+    assertEventErrors('Network', 'dataReceived', params, message);
+  }
+
+  for (const [name, params, message] of DOM_STORAGE_ERROR_CASES) {
+    assertEventErrors('DOMStorage', name, params, message);
+  }
+})().then(common.mustCall());