From 7c625815c7ae366b6194a89f71844cff9a3f5afa Mon Sep 17 00:00:00 2001 From: ryuhei shima Date: Mon, 9 Mar 2026 19:53:10 +0900 Subject: [PATCH] inspector: return errors when CDP protocol event emission fails --- doc/api/inspector.md | 14 + lib/inspector.js | 2 +- src/inspector/dom_storage_agent.cc | 27 + src/inspector/network_agent.cc | 83 ++- src/inspector/network_agent.h | 9 + src/inspector_agent.cc | 24 +- src/inspector_agent.h | 6 +- src/inspector_js_api.cc | 6 +- ...st-inspector-emit-protocol-event-errors.js | 508 ++++++++++++++++++ 9 files changed, 659 insertions(+), 20 deletions(-) create mode 100644 test/parallel/test-inspector-emit-protocol-event-errors.js diff --git a/doc/api/inspector.md b/doc/api/inspector.md index d5401e83789435..362537b04f6741 100644 --- a/doc/api/inspector.md +++ b/doc/api/inspector.md @@ -520,6 +520,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -537,6 +538,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -551,6 +553,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -566,6 +569,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -581,6 +585,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -596,6 +601,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -610,6 +616,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -624,6 +631,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -638,6 +646,7 @@ added: --> * `params` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-network-inspection` flag enabled. @@ -696,6 +705,7 @@ added: * `isLocalStorage` {boolean} * `key` {string} * `newValue` {string} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-storage-inspection` flag enabled. @@ -716,6 +726,7 @@ added: * `storageKey` {string} * `isLocalStorage` {boolean} * `key` {string} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-storage-inspection` flag enabled. @@ -738,6 +749,7 @@ added: * `key` {string} * `oldValue` {string} * `newValue` {string} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-storage-inspection` flag enabled. @@ -757,6 +769,7 @@ added: * `securityOrigin` {string} * `storageKey` {string} * `isLocalStorage` {boolean} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-storage-inspection` flag enabled. @@ -775,6 +788,7 @@ added: * `params` {Object} * `isLocalStorage` {boolean} * `storageMap` {Object} +* Returns: {Array} An array of errors from each session This feature is only available with the `--experimental-storage-inspection` flag enabled. diff --git a/lib/inspector.js b/lib/inspector.js index 1c440794f3932f..fa3a7e1712b630 100644 --- a/lib/inspector.js +++ b/lib/inspector.js @@ -209,7 +209,7 @@ function inspectorWaitForDebugger() { function broadcastToFrontend(eventName, params = kEmptyObject) { validateString(eventName, 'eventName'); validateObject(params, 'params'); - emitProtocolEvent(eventName, params); + return emitProtocolEvent(eventName, params); } const Network = { diff --git a/src/inspector/dom_storage_agent.cc b/src/inspector/dom_storage_agent.cc index d300266548ca87..f603768e647f24 100644 --- a/src/inspector/dom_storage_agent.cc +++ b/src/inspector/dom_storage_agent.cc @@ -14,11 +14,18 @@ using v8::Local; using v8::Object; using v8::Value; +static void ThrowEventError(v8::Isolate* isolate, const std::string& message) { + isolate->ThrowException(v8::Exception::Error( + v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked())); +} + std::unique_ptr createStorageIdFromObject( Local context, Local 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 = @@ -26,6 +33,7 @@ std::unique_ptr createStorageIdFromObject( protocol::String storageKey; if (!ObjectGetProtocolString(context, storage_id_obj, "storageKey") .To(&storageKey)) { + ThrowEventError(isolate, "Missing storageKey in storageId"); return {}; } @@ -119,8 +127,10 @@ protocol::DispatchResponse DOMStorageAgent::clear( void DOMStorageAgent::domStorageItemAdded(Local context, Local params) { + Isolate* isolate = env_->isolate(); Local storage_id_obj; if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); return; } @@ -132,10 +142,12 @@ void DOMStorageAgent::domStorageItemAdded(Local 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); @@ -143,8 +155,10 @@ void DOMStorageAgent::domStorageItemAdded(Local context, void DOMStorageAgent::domStorageItemRemoved(Local context, Local params) { + Isolate* isolate = env_->isolate(); Local storage_id_obj; if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); return; } std::unique_ptr storage_id = @@ -156,6 +170,7 @@ void DOMStorageAgent::domStorageItemRemoved(Local 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); @@ -163,8 +178,10 @@ void DOMStorageAgent::domStorageItemRemoved(Local context, void DOMStorageAgent::domStorageItemUpdated(Local context, Local params) { + Isolate* isolate = env_->isolate(); Local storage_id_obj; if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); return; } @@ -177,14 +194,17 @@ void DOMStorageAgent::domStorageItemUpdated(Local 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( @@ -193,8 +213,10 @@ void DOMStorageAgent::domStorageItemUpdated(Local context, void DOMStorageAgent::domStorageItemsCleared(Local context, Local params) { + Isolate* isolate = env_->isolate(); Local storage_id_obj; if (!ObjectGetObject(context, params, "storageId").ToLocal(&storage_id_obj)) { + ThrowEventError(isolate, "Missing storageId in event"); return; } std::unique_ptr storage_id = @@ -212,27 +234,32 @@ void DOMStorageAgent::registerStorage(Local 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 storage_map_obj; if (!ObjectGetObject(context, params, "storageMap") .ToLocal(&storage_map_obj)) { + ThrowEventError(isolate, "Missing storageMap in event"); return; } std::unordered_map& storage_map = is_local_storage ? local_storage_map_ : session_storage_map_; Local 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 key_value; if (!property_names->Get(context, i).ToLocal(&key_value)) { + ThrowEventError(isolate, "Failed to get key from storageMap"); return; } Local value_value; if (!storage_map_obj->Get(context, key_value).ToLocal(&value_value)) { + ThrowEventError(isolate, "Failed to get value from storageMap"); return; } node::Utf8Value key_utf8(isolate, key_value); diff --git a/src/inspector/network_agent.cc b/src/inspector/network_agent.cc index bca1413b85af03..a3542aad86d246 100644 --- a/src/inspector/network_agent.cc +++ b/src/inspector/network_agent.cc @@ -24,15 +24,23 @@ 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::Error( + v8::String::NewFromUtf8(isolate, message.c_str()).ToLocalChecked())); +} + // Create a protocol::Network::Headers from the v8 object. -std::unique_ptr createHeadersFromObject( - v8::Local context, Local headers_obj) { +std::unique_ptr +NetworkAgent::createHeadersFromObject(v8::Local context, + Local headers_obj) { HandleScope handle_scope(Isolate::GetCurrent()); + Isolate* isolate = env_->isolate(); std::unique_ptr dict = protocol::DictionaryValue::create(); Local property_names; if (!headers_obj->GetOwnPropertyNames(context).ToLocal(&property_names)) { + ThrowEventError(isolate, "Missing response headers in event"); return {}; } @@ -40,12 +48,14 @@ std::unique_ptr createHeadersFromObject( Local 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 property_name = property_name_val.As(); 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 createHeadersFromObject( } // Create a protocol::Network::Request from the v8 object. -std::unique_ptr createRequestFromObject( - v8::Local context, Local request) { +std::unique_ptr +NetworkAgent::createRequestFromObject(v8::Local context, + Local 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 headers_obj; if (!ObjectGetObject(context, request, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing request.headers in event"); return {}; } std::unique_ptr headers = @@ -88,24 +103,30 @@ std::unique_ptr createRequestFromObject( } // Create a protocol::Network::Response from the v8 object. -std::unique_ptr createResponseFromObject( - v8::Local context, Local response) { +std::unique_ptr +NetworkAgent::createResponseFromObject(v8::Local context, + Local 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 headers_obj; if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing response.headers in event"); return {}; } std::unique_ptr headers = @@ -129,20 +150,25 @@ std::unique_ptr createResponseFromObject( .build(); } -std::unique_ptr createWebSocketResponse( - v8::Local context, Local response) { +std::unique_ptr +NetworkAgent::createWebSocketResponse(v8::Local context, + Local 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 headers_obj; if (!ObjectGetObject(context, response, "headers").ToLocal(&headers_obj)) { + ThrowEventError(isolate, "Missing response.headers in event"); return {}; } std::unique_ptr headers = @@ -182,12 +208,15 @@ NetworkAgent::NetworkAgent( void NetworkAgent::webSocketCreated(v8::Local context, v8::Local 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 initiator = @@ -201,12 +230,15 @@ void NetworkAgent::webSocketCreated(v8::Local context, void NetworkAgent::webSocketClosed(v8::Local context, v8::Local 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(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } frontend_->webSocketClosed(request_id, timestamp); @@ -214,16 +246,20 @@ void NetworkAgent::webSocketClosed(v8::Local context, void NetworkAgent::webSocketHandshakeResponseReceived( v8::Local context, v8::Local 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(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } Local 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 context, v8::Local 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(×tamp)) { + 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 request_obj; if (!ObjectGetObject(context, params, "request").ToLocal(&request_obj)) { + ThrowEventError(isolate, "Missing request in event"); return; } std::unique_ptr request = @@ -446,20 +487,25 @@ void NetworkAgent::requestWillBeSent(v8::Local context, void NetworkAgent::responseReceived(v8::Local context, v8::Local 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(×tamp)) { + 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 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 context, void NetworkAgent::loadingFailed(v8::Local context, v8::Local 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(×tamp)) { + 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 context, void NetworkAgent::loadingFinished(v8::Local context, Local 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(×tamp)) { + ThrowEventError(isolate, "Missing timestamp in event"); return; } @@ -530,8 +584,10 @@ void NetworkAgent::loadingFinished(v8::Local context, void NetworkAgent::dataSent(v8::Local context, v8::Local 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 context, double timestamp; if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) { + 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 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 data = data_obj.As(); @@ -570,28 +630,35 @@ void NetworkAgent::dataSent(v8::Local context, void NetworkAgent::dataReceived(v8::Local context, v8::Local 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(×tamp)) { + 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 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 data = data_obj.As(); diff --git a/src/inspector/network_agent.h b/src/inspector/network_agent.h index 7a5d545cb8d499..2136a45baf45f6 100644 --- a/src/inspector/network_agent.h +++ b/src/inspector/network_agent.h @@ -79,6 +79,15 @@ class NetworkAgent : public protocol::Network::Backend { v8::Local params); private: + std::unique_ptr createHeadersFromObject( + v8::Local context, v8::Local headers_obj); + std::unique_ptr createRequestFromObject( + v8::Local context, v8::Local request); + std::unique_ptr createResponseFromObject( + v8::Local context, v8::Local response); + std::unique_ptr createWebSocketResponse( + v8::Local context, v8::Local response); + NetworkInspector* inspector_; v8_inspector::V8Inspector* v8_inspector_; std::shared_ptr frontend_; diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc index 068da4c01bc1c9..2225d244296314 100644 --- a/src/inspector_agent.cc +++ b/src/inspector_agent.cc @@ -29,6 +29,7 @@ #include "timer_wrap-inl.h" #include "util-inl.h" #include "v8-inspector.h" +#include "v8-isolate.h" #include "v8-platform.h" #include "libplatform/libplatform.h" @@ -743,12 +744,21 @@ class NodeInspectorClient : public V8InspectorClient { return retaining_context; } - void emitNotification(v8::Local context, - const StringView& event, - Local params) { + v8::Local emitNotification(v8::Local context, + const StringView& event, + Local params) { + Isolate* isolate = env_->isolate(); + v8::EscapableHandleScope handle_scope(isolate); + v8::Local results = v8::Array::New(isolate); for (const auto& id_channel : channels_) { + v8::TryCatch try_catch(isolate); id_channel.second->emitNotificationFromBackend(context, event, params); + if (try_catch.HasCaught()) { + Local exception = try_catch.Exception(); + results->Set(context, results->Length(), exception).Check(); + } } + return handle_scope.Escape(results); } std::shared_ptr getThreadHandle() { @@ -964,10 +974,10 @@ std::unique_ptr Agent::ConnectToMainThread( prevent_shutdown); } -void Agent::EmitProtocolEvent(v8::Local context, - const StringView& event, - Local params) { - client_->emitNotification(context, event, params); +v8::Local Agent::EmitProtocolEvent(v8::Local context, + const StringView& event, + Local params) { + return client_->emitNotification(context, event, params); } void Agent::SetupNetworkTracking(Local enable_function, diff --git a/src/inspector_agent.h b/src/inspector_agent.h index 5ace72a64012a0..7ccd13fee880cc 100644 --- a/src/inspector_agent.h +++ b/src/inspector_agent.h @@ -70,9 +70,9 @@ class Agent { void ReportUncaughtException(v8::Local error, v8::Local message); - void EmitProtocolEvent(v8::Local context, - const v8_inspector::StringView& event, - v8::Local params); + v8::Local EmitProtocolEvent(v8::Local context, + const v8_inspector::StringView& event, + v8::Local params); void SetupNetworkTracking(v8::Local enable_function, v8::Local disable_function); diff --git a/src/inspector_js_api.cc b/src/inspector_js_api.cc index 54a91765ac6b1f..62a887232dce8b 100644 --- a/src/inspector_js_api.cc +++ b/src/inspector_js_api.cc @@ -7,6 +7,7 @@ #include "node_external_reference.h" #include "util-inl.h" #include "v8-inspector.h" +#include "v8-local-handle.h" #include "v8.h" #include @@ -15,6 +16,7 @@ namespace node { namespace inspector { namespace { +using v8::Array; using v8::Context; using v8::Function; using v8::FunctionCallbackInfo; @@ -274,10 +276,12 @@ void EmitProtocolEvent(const FunctionCallbackInfo& args) { CHECK(args[1]->IsObject()); Local params = args[1].As(); - env->inspector_agent()->EmitProtocolEvent( + v8::HandleScope handle_scope(env->isolate()); + Local results = env->inspector_agent()->EmitProtocolEvent( args.GetIsolate()->GetCurrentContext(), ToInspectorString(env->isolate(), eventName)->string(), params); + args.GetReturnValue().Set(results); } void SetupNetworkTracking(const FunctionCallbackInfo& args) { diff --git a/test/parallel/test-inspector-emit-protocol-event-errors.js b/test/parallel/test-inspector-emit-protocol-event-errors.js new file mode 100644 index 00000000000000..0e913d386d1c18 --- /dev/null +++ b/test/parallel/test-inspector-emit-protocol-event-errors.js @@ -0,0 +1,508 @@ +// 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) { + const result = inspector[domain][name](params); + assert.strictEqual( + result.length, + 1, + `Expected ${domain}.${name} to return exactly one error`, + ); + assert.strictEqual( + result[0] instanceof Error, + true, + `Expected ${domain}.${name} to return Error`, + ); + assert.strictEqual(result[0].message, message); +} + +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());