mirror of
https://github.com/nodejs/node.git
synced 2025-08-15 13:48:44 +02:00
inspector: add protocol methods retrieving sent/received data
Add protocol method `Network.dataSent` to buffer request data. And expose protocol methods `Network.getRequestPostData` and `Network.getResponseBody` allowing devtool to retrieve buffered data. PR-URL: https://github.com/nodejs/node/pull/58645 Reviewed-By: Ryuhei Shima <shimaryuhei@gmail.com>
This commit is contained in:
parent
b5ff3f42b8
commit
be93091694
11 changed files with 483 additions and 88 deletions
|
@ -524,6 +524,20 @@ This feature is only available with the `--experimental-network-inspection` flag
|
|||
Broadcasts the `Network.dataReceived` event to connected frontends, or buffers the data if
|
||||
`Network.streamResourceContent` command was not invoked for the given request yet.
|
||||
|
||||
Also enables `Network.getResponseBody` command to retrieve the response data.
|
||||
|
||||
### `inspector.Network.dataSent([params])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `params` {Object}
|
||||
|
||||
This feature is only available with the `--experimental-network-inspection` flag enabled.
|
||||
|
||||
Enables `Network.getRequestPostData` command to retrieve the request data.
|
||||
|
||||
### `inspector.Network.requestWillBeSent([params])`
|
||||
|
||||
<!-- YAML
|
||||
|
|
|
@ -214,6 +214,7 @@ const Network = {
|
|||
responseReceived: (params) => broadcastToFrontend('Network.responseReceived', params),
|
||||
loadingFinished: (params) => broadcastToFrontend('Network.loadingFinished', params),
|
||||
loadingFailed: (params) => broadcastToFrontend('Network.loadingFailed', params),
|
||||
dataSent: (params) => broadcastToFrontend('Network.dataSent', params),
|
||||
dataReceived: (params) => broadcastToFrontend('Network.dataReceived', params),
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ const {
|
|||
} = primordials;
|
||||
|
||||
const { now } = require('internal/perf/utils');
|
||||
const { MIMEType } = require('internal/mime');
|
||||
const kInspectorRequestId = Symbol('kInspectorRequestId');
|
||||
|
||||
// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
|
||||
|
@ -46,9 +47,29 @@ function getNextRequestId() {
|
|||
return `node-network-event-${++requestId}`;
|
||||
};
|
||||
|
||||
function sniffMimeType(contentType) {
|
||||
let mimeType;
|
||||
let charset;
|
||||
try {
|
||||
const mimeTypeObj = new MIMEType(contentType);
|
||||
mimeType = mimeTypeObj.essence || '';
|
||||
charset = mimeTypeObj.params.get('charset') || '';
|
||||
} catch {
|
||||
mimeType = '';
|
||||
charset = '';
|
||||
}
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
mimeType,
|
||||
charset,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
kInspectorRequestId,
|
||||
kResourceType,
|
||||
getMonotonicTime,
|
||||
getNextRequestId,
|
||||
sniffMimeType,
|
||||
};
|
||||
|
|
|
@ -13,10 +13,10 @@ const {
|
|||
kResourceType,
|
||||
getMonotonicTime,
|
||||
getNextRequestId,
|
||||
sniffMimeType,
|
||||
} = require('internal/inspector/network');
|
||||
const dc = require('diagnostics_channel');
|
||||
const { Network } = require('inspector');
|
||||
const { MIMEType } = require('internal/mime');
|
||||
|
||||
const kRequestUrl = Symbol('kRequestUrl');
|
||||
|
||||
|
@ -24,24 +24,32 @@ const kRequestUrl = Symbol('kRequestUrl');
|
|||
const convertHeaderObject = (headers = {}) => {
|
||||
// The 'host' header that contains the host and port of the URL.
|
||||
let host;
|
||||
let charset;
|
||||
let mimeType;
|
||||
const dict = {};
|
||||
for (const { 0: key, 1: value } of ObjectEntries(headers)) {
|
||||
if (key.toLowerCase() === 'host') {
|
||||
const lowerCasedKey = key.toLowerCase();
|
||||
if (lowerCasedKey === 'host') {
|
||||
host = value;
|
||||
}
|
||||
if (lowerCasedKey === 'content-type') {
|
||||
const result = sniffMimeType(value);
|
||||
charset = result.charset;
|
||||
mimeType = result.mimeType;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
dict[key] = value;
|
||||
} else if (ArrayIsArray(value)) {
|
||||
if (key.toLowerCase() === 'cookie') dict[key] = value.join('; ');
|
||||
if (lowerCasedKey === 'cookie') dict[key] = value.join('; ');
|
||||
// ChromeDevTools frontend treats 'set-cookie' as a special case
|
||||
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
|
||||
else if (key.toLowerCase() === 'set-cookie') dict[key] = value.join('\n');
|
||||
else if (lowerCasedKey === 'set-cookie') dict[key] = value.join('\n');
|
||||
else dict[key] = value.join(', ');
|
||||
} else {
|
||||
dict[key] = String(value);
|
||||
}
|
||||
}
|
||||
return [host, dict];
|
||||
return [dict, host, charset, mimeType];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -52,7 +60,7 @@ const convertHeaderObject = (headers = {}) => {
|
|||
function onClientRequestCreated({ request }) {
|
||||
request[kInspectorRequestId] = getNextRequestId();
|
||||
|
||||
const { 0: host, 1: headers } = convertHeaderObject(request.getHeaders());
|
||||
const { 0: headers, 1: host, 2: charset } = convertHeaderObject(request.getHeaders());
|
||||
const url = `${request.protocol}//${host}${request.path}`;
|
||||
request[kRequestUrl] = url;
|
||||
|
||||
|
@ -60,6 +68,7 @@ function onClientRequestCreated({ request }) {
|
|||
requestId: request[kInspectorRequestId],
|
||||
timestamp: getMonotonicTime(),
|
||||
wallTime: DateNow(),
|
||||
charset,
|
||||
request: {
|
||||
url,
|
||||
method: request.method,
|
||||
|
@ -95,16 +104,7 @@ function onClientResponseFinish({ request, response }) {
|
|||
return;
|
||||
}
|
||||
|
||||
let mimeType;
|
||||
let charset;
|
||||
try {
|
||||
const mimeTypeObj = new MIMEType(response.headers['content-type']);
|
||||
mimeType = mimeTypeObj.essence || '';
|
||||
charset = mimeTypeObj.params.get('charset') || '';
|
||||
} catch {
|
||||
mimeType = '';
|
||||
charset = '';
|
||||
}
|
||||
const { 0: headers, 2: charset, 3: mimeType } = convertHeaderObject(response.headers);
|
||||
|
||||
Network.responseReceived({
|
||||
requestId: request[kInspectorRequestId],
|
||||
|
@ -114,7 +114,7 @@ function onClientResponseFinish({ request, response }) {
|
|||
url: request[kRequestUrl],
|
||||
status: response.statusCode,
|
||||
statusText: response.statusMessage ?? '',
|
||||
headers: convertHeaderObject(response.headers)[1],
|
||||
headers,
|
||||
mimeType,
|
||||
charset,
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
const {
|
||||
ArrayPrototypeFindIndex,
|
||||
DateNow,
|
||||
} = primordials;
|
||||
|
||||
|
@ -10,10 +9,10 @@ const {
|
|||
kResourceType,
|
||||
getMonotonicTime,
|
||||
getNextRequestId,
|
||||
sniffMimeType,
|
||||
} = require('internal/inspector/network');
|
||||
const dc = require('diagnostics_channel');
|
||||
const { Network } = require('inspector');
|
||||
const { MIMEType } = require('internal/mime');
|
||||
|
||||
// Convert an undici request headers array to a plain object (Map<string, string>)
|
||||
function requestHeadersArrayToDictionary(headers) {
|
||||
|
@ -29,21 +28,30 @@ function requestHeadersArrayToDictionary(headers) {
|
|||
// Convert an undici response headers array to a plain object (Map<string, string>)
|
||||
function responseHeadersArrayToDictionary(headers) {
|
||||
const dict = {};
|
||||
let charset;
|
||||
let mimeType;
|
||||
for (let idx = 0; idx < headers.length; idx += 2) {
|
||||
const key = `${headers[idx]}`;
|
||||
const lowerCasedKey = key.toLowerCase();
|
||||
const value = `${headers[idx + 1]}`;
|
||||
const prevValue = dict[key];
|
||||
|
||||
if (lowerCasedKey === 'content-type') {
|
||||
const result = sniffMimeType(value);
|
||||
charset = result.charset;
|
||||
mimeType = result.mimeType;
|
||||
}
|
||||
|
||||
if (typeof prevValue === 'string') {
|
||||
// ChromeDevTools frontend treats 'set-cookie' as a special case
|
||||
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
|
||||
if (key.toLowerCase() === 'set-cookie') dict[key] = `${prevValue}\n${value}`;
|
||||
if (lowerCasedKey === 'set-cookie') dict[key] = `${prevValue}\n${value}`;
|
||||
else dict[key] = `${prevValue}, ${value}`;
|
||||
} else {
|
||||
dict[key] = value;
|
||||
}
|
||||
}
|
||||
return dict;
|
||||
return [dict, charset, mimeType];
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -54,10 +62,15 @@ function responseHeadersArrayToDictionary(headers) {
|
|||
function onClientRequestStart({ request }) {
|
||||
const url = `${request.origin}${request.path}`;
|
||||
request[kInspectorRequestId] = getNextRequestId();
|
||||
|
||||
const headers = requestHeadersArrayToDictionary(request.headers);
|
||||
const { charset } = sniffMimeType(headers);
|
||||
|
||||
Network.requestWillBeSent({
|
||||
requestId: request[kInspectorRequestId],
|
||||
timestamp: getMonotonicTime(),
|
||||
wallTime: DateNow(),
|
||||
charset,
|
||||
request: {
|
||||
url,
|
||||
method: request.method,
|
||||
|
@ -94,19 +107,7 @@ function onClientResponseHeaders({ request, response }) {
|
|||
return;
|
||||
}
|
||||
|
||||
let mimeType;
|
||||
let charset;
|
||||
try {
|
||||
const contentTypeKeyIndex =
|
||||
ArrayPrototypeFindIndex(response.headers, (header) => header.toString().toLowerCase() === 'content-type');
|
||||
const contentType = contentTypeKeyIndex !== -1 ? response.headers[contentTypeKeyIndex + 1].toString() : '';
|
||||
const mimeTypeObj = new MIMEType(contentType);
|
||||
mimeType = mimeTypeObj.essence || '';
|
||||
charset = mimeTypeObj.params.get('charset') || '';
|
||||
} catch {
|
||||
mimeType = '';
|
||||
charset = '';
|
||||
}
|
||||
const { 0: headers, 1: charset, 2: mimeType } = responseHeadersArrayToDictionary(response.headers);
|
||||
|
||||
const url = `${request.origin}${request.path}`;
|
||||
Network.responseReceived({
|
||||
|
@ -118,7 +119,7 @@ function onClientResponseHeaders({ request, response }) {
|
|||
url,
|
||||
status: response.statusCode,
|
||||
statusText: response.statusText,
|
||||
headers: responseHeadersArrayToDictionary(response.headers),
|
||||
headers,
|
||||
mimeType,
|
||||
charset,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
#include "network_agent.h"
|
||||
#include "debug_utils-inl.h"
|
||||
#include "inspector/protocol_helper.h"
|
||||
#include "network_inspector.h"
|
||||
#include "util-inl.h"
|
||||
|
@ -68,6 +69,20 @@ Maybe<int> ObjectGetInt(v8::Local<v8::Context> context,
|
|||
return Just(value.As<v8::Int32>()->Value());
|
||||
}
|
||||
|
||||
// Get a protocol bool property from the object.
|
||||
Maybe<bool> ObjectGetBool(v8::Local<v8::Context> context,
|
||||
Local<Object> object,
|
||||
const char* property) {
|
||||
HandleScope handle_scope(context->GetIsolate());
|
||||
Local<Value> value;
|
||||
if (!object->Get(context, OneByteString(context->GetIsolate(), property))
|
||||
.ToLocal(&value) ||
|
||||
!value->IsBoolean()) {
|
||||
return Nothing<bool>();
|
||||
}
|
||||
return Just(value.As<v8::Boolean>()->Value());
|
||||
}
|
||||
|
||||
// Get an object property from the object.
|
||||
MaybeLocal<v8::Object> ObjectGetObject(v8::Local<v8::Context> context,
|
||||
Local<Object> object,
|
||||
|
@ -134,10 +149,13 @@ std::unique_ptr<protocol::Network::Request> createRequestFromObject(
|
|||
if (!headers) {
|
||||
return {};
|
||||
}
|
||||
bool has_post_data =
|
||||
ObjectGetBool(context, request, "hasPostData").FromMaybe(false);
|
||||
|
||||
return protocol::Network::Request::create()
|
||||
.setUrl(url)
|
||||
.setMethod(method)
|
||||
.setHasPostData(has_post_data)
|
||||
.setHeaders(std::move(headers))
|
||||
.build();
|
||||
}
|
||||
|
@ -169,15 +187,10 @@ std::unique_ptr<protocol::Network::Response> createResponseFromObject(
|
|||
return {};
|
||||
}
|
||||
|
||||
protocol::String mimeType;
|
||||
if (!ObjectGetProtocolString(context, response, "mimeType").To(&mimeType)) {
|
||||
mimeType = protocol::String("");
|
||||
}
|
||||
|
||||
protocol::String charset = protocol::String();
|
||||
if (!ObjectGetProtocolString(context, response, "charset").To(&charset)) {
|
||||
charset = protocol::String("");
|
||||
}
|
||||
protocol::String mimeType =
|
||||
ObjectGetProtocolString(context, response, "mimeType").FromMaybe("");
|
||||
protocol::String charset =
|
||||
ObjectGetProtocolString(context, response, "charset").FromMaybe("");
|
||||
|
||||
return protocol::Network::Response::create()
|
||||
.setUrl(url)
|
||||
|
@ -196,6 +209,7 @@ NetworkAgent::NetworkAgent(NetworkInspector* inspector,
|
|||
event_notifier_map_["responseReceived"] = &NetworkAgent::responseReceived;
|
||||
event_notifier_map_["loadingFailed"] = &NetworkAgent::loadingFailed;
|
||||
event_notifier_map_["loadingFinished"] = &NetworkAgent::loadingFinished;
|
||||
event_notifier_map_["dataSent"] = &NetworkAgent::dataSent;
|
||||
event_notifier_map_["dataReceived"] = &NetworkAgent::dataReceived;
|
||||
}
|
||||
|
||||
|
@ -225,23 +239,93 @@ protocol::DispatchResponse NetworkAgent::disable() {
|
|||
return protocol::DispatchResponse::Success();
|
||||
}
|
||||
|
||||
protocol::DispatchResponse NetworkAgent::streamResourceContent(
|
||||
const protocol::String& in_requestId, protocol::Binary* out_bufferedData) {
|
||||
if (!requests_.contains(in_requestId)) {
|
||||
protocol::DispatchResponse NetworkAgent::getRequestPostData(
|
||||
const protocol::String& in_requestId, protocol::String* out_postData) {
|
||||
auto request_entry = requests_.find(in_requestId);
|
||||
if (request_entry == requests_.end()) {
|
||||
// Request not found, ignore it.
|
||||
return protocol::DispatchResponse::InvalidParams("Request not found");
|
||||
}
|
||||
|
||||
auto& it = requests_[in_requestId];
|
||||
|
||||
it.is_streaming = true;
|
||||
if (!request_entry->second.is_request_finished) {
|
||||
// Request not finished yet.
|
||||
return protocol::DispatchResponse::InvalidParams(
|
||||
"Request data is not finished yet");
|
||||
}
|
||||
if (request_entry->second.request_charset == Charset::kBinary) {
|
||||
// The protocol does not support binary request bodies yet.
|
||||
return protocol::DispatchResponse::ServerError(
|
||||
"Unable to serialize binary request body");
|
||||
}
|
||||
// If the response is UTF-8, we return it as a concatenated string.
|
||||
CHECK_EQ(request_entry->second.request_charset, Charset::kUTF8);
|
||||
|
||||
// Concat response bodies.
|
||||
*out_bufferedData = protocol::Binary::concat(it.response_data_blobs);
|
||||
// Clear buffered data.
|
||||
it.response_data_blobs.clear();
|
||||
protocol::Binary buf =
|
||||
protocol::Binary::concat(request_entry->second.request_data_blobs);
|
||||
*out_postData = protocol::StringUtil::fromUTF8(buf.data(), buf.size());
|
||||
return protocol::DispatchResponse::Success();
|
||||
}
|
||||
|
||||
if (it.is_finished) {
|
||||
protocol::DispatchResponse NetworkAgent::getResponseBody(
|
||||
const protocol::String& in_requestId,
|
||||
protocol::String* out_body,
|
||||
bool* out_base64Encoded) {
|
||||
auto request_entry = requests_.find(in_requestId);
|
||||
if (request_entry == requests_.end()) {
|
||||
// Request not found, ignore it.
|
||||
return protocol::DispatchResponse::InvalidParams("Request not found");
|
||||
}
|
||||
|
||||
if (request_entry->second.is_streaming) {
|
||||
// Streaming request, data is not buffered.
|
||||
return protocol::DispatchResponse::InvalidParams(
|
||||
"Response body of the request is been streamed");
|
||||
}
|
||||
|
||||
if (!request_entry->second.is_response_finished) {
|
||||
// Response not finished yet.
|
||||
return protocol::DispatchResponse::InvalidParams(
|
||||
"Response data is not finished yet");
|
||||
}
|
||||
|
||||
// Concat response bodies.
|
||||
protocol::Binary buf =
|
||||
protocol::Binary::concat(request_entry->second.response_data_blobs);
|
||||
if (request_entry->second.response_charset == Charset::kBinary) {
|
||||
// If the response is binary, we return base64 encoded data.
|
||||
*out_body = buf.toBase64();
|
||||
*out_base64Encoded = true;
|
||||
} else if (request_entry->second.response_charset == Charset::kUTF8) {
|
||||
// If the response is UTF-8, we return it as a concatenated string.
|
||||
*out_body = protocol::StringUtil::fromUTF8(buf.data(), buf.size());
|
||||
*out_base64Encoded = false;
|
||||
} else {
|
||||
UNREACHABLE("Response charset not implemented");
|
||||
}
|
||||
|
||||
requests_.erase(request_entry);
|
||||
return protocol::DispatchResponse::Success();
|
||||
}
|
||||
|
||||
protocol::DispatchResponse NetworkAgent::streamResourceContent(
|
||||
const protocol::String& in_requestId, protocol::Binary* out_bufferedData) {
|
||||
auto it = requests_.find(in_requestId);
|
||||
if (it == requests_.end()) {
|
||||
// Request not found, ignore it.
|
||||
return protocol::DispatchResponse::InvalidParams("Request not found");
|
||||
}
|
||||
auto& request_entry = it->second;
|
||||
|
||||
request_entry.is_streaming = true;
|
||||
|
||||
// Concat response bodies.
|
||||
*out_bufferedData =
|
||||
protocol::Binary::concat(request_entry.response_data_blobs);
|
||||
// Clear buffered data.
|
||||
request_entry.response_data_blobs.clear();
|
||||
|
||||
if (request_entry.is_response_finished) {
|
||||
// If the request is finished, remove the entry.
|
||||
requests_.erase(in_requestId);
|
||||
}
|
||||
|
@ -263,6 +347,8 @@ void NetworkAgent::requestWillBeSent(v8::Local<v8::Context> context,
|
|||
if (!ObjectGetDouble(context, params, "wallTime").To(&wall_time)) {
|
||||
return;
|
||||
}
|
||||
protocol::String charset =
|
||||
ObjectGetProtocolString(context, params, "charset").FromMaybe("");
|
||||
Local<v8::Object> request_obj;
|
||||
if (!ObjectGetObject(context, params, "request").ToLocal(&request_obj)) {
|
||||
return;
|
||||
|
@ -280,17 +366,20 @@ void NetworkAgent::requestWillBeSent(v8::Local<v8::Context> context,
|
|||
v8_inspector_->captureStackTrace(true)->buildInspectorObject(0))
|
||||
.build();
|
||||
|
||||
if (requests_.contains(request_id)) {
|
||||
// Duplicate entry, ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
auto request_charset = charset == "utf-8" ? Charset::kUTF8 : Charset::kBinary;
|
||||
requests_.emplace(
|
||||
request_id,
|
||||
RequestEntry(timestamp, request_charset, request->getHasPostData()));
|
||||
frontend_->requestWillBeSent(request_id,
|
||||
std::move(request),
|
||||
std::move(initiator),
|
||||
timestamp,
|
||||
wall_time);
|
||||
|
||||
if (requests_.contains(request_id)) {
|
||||
// Duplicate entry, ignore it.
|
||||
return;
|
||||
}
|
||||
requests_.emplace(request_id, RequestEntry{timestamp, false, false, {}});
|
||||
}
|
||||
|
||||
void NetworkAgent::responseReceived(v8::Local<v8::Context> context,
|
||||
|
@ -316,6 +405,13 @@ void NetworkAgent::responseReceived(v8::Local<v8::Context> context,
|
|||
return;
|
||||
}
|
||||
|
||||
auto request_entry = requests_.find(request_id);
|
||||
if (request_entry == requests_.end()) {
|
||||
// No entry found. Ignore it.
|
||||
return;
|
||||
}
|
||||
request_entry->second.response_charset =
|
||||
response->getCharset() == "utf-8" ? Charset::kUTF8 : Charset::kBinary;
|
||||
frontend_->responseReceived(request_id, timestamp, type, std::move(response));
|
||||
}
|
||||
|
||||
|
@ -366,12 +462,12 @@ void NetworkAgent::loadingFinished(v8::Local<v8::Context> context,
|
|||
// Streaming finished, remove the entry.
|
||||
requests_.erase(request_id);
|
||||
} else {
|
||||
request_entry->second.is_finished = true;
|
||||
request_entry->second.is_response_finished = true;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkAgent::dataReceived(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Object> params) {
|
||||
void NetworkAgent::dataSent(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Object> params) {
|
||||
protocol::String request_id;
|
||||
if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
|
||||
return;
|
||||
|
@ -383,6 +479,39 @@ void NetworkAgent::dataReceived(v8::Local<v8::Context> context,
|
|||
return;
|
||||
}
|
||||
|
||||
bool is_finished =
|
||||
ObjectGetBool(context, params, "finished").FromMaybe(false);
|
||||
if (is_finished) {
|
||||
request_entry->second.is_request_finished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
double timestamp;
|
||||
if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) {
|
||||
return;
|
||||
}
|
||||
int data_length;
|
||||
if (!ObjectGetInt(context, params, "dataLength").To(&data_length)) {
|
||||
return;
|
||||
}
|
||||
Local<Object> data_obj;
|
||||
if (!ObjectGetObject(context, params, "data").ToLocal(&data_obj)) {
|
||||
return;
|
||||
}
|
||||
if (!data_obj->IsUint8Array()) {
|
||||
return;
|
||||
}
|
||||
Local<Uint8Array> data = data_obj.As<Uint8Array>();
|
||||
auto data_bin = protocol::Binary::fromUint8Array(data);
|
||||
request_entry->second.request_data_blobs.push_back(data_bin);
|
||||
}
|
||||
|
||||
void NetworkAgent::dataReceived(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Object> params) {
|
||||
protocol::String request_id;
|
||||
if (!ObjectGetProtocolString(context, params, "requestId").To(&request_id)) {
|
||||
return;
|
||||
}
|
||||
double timestamp;
|
||||
if (!ObjectGetDouble(context, params, "timestamp").To(×tamp)) {
|
||||
return;
|
||||
|
@ -406,11 +535,17 @@ void NetworkAgent::dataReceived(v8::Local<v8::Context> context,
|
|||
Local<Uint8Array> data = data_obj.As<Uint8Array>();
|
||||
auto data_bin = protocol::Binary::fromUint8Array(data);
|
||||
|
||||
if (request_entry->second.is_streaming) {
|
||||
auto it = requests_.find(request_id);
|
||||
if (it == requests_.end()) {
|
||||
// No entry found. Ignore it.
|
||||
return;
|
||||
}
|
||||
auto& request_entry = it->second;
|
||||
if (request_entry.is_streaming) {
|
||||
frontend_->dataReceived(
|
||||
request_id, timestamp, data_length, encoded_data_length, data_bin);
|
||||
} else {
|
||||
requests_[request_id].response_data_blobs.push_back(data_bin);
|
||||
request_entry.response_data_blobs.push_back(data_bin);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,29 @@ namespace inspector {
|
|||
|
||||
class NetworkInspector;
|
||||
|
||||
// Supported charsets for devtools frontend on request/response data.
|
||||
// If the charset is kUTF8, the data is expected to be text and can be
|
||||
// formatted on the frontend.
|
||||
enum class Charset {
|
||||
kUTF8,
|
||||
kBinary,
|
||||
};
|
||||
|
||||
struct RequestEntry {
|
||||
double timestamp;
|
||||
bool is_finished;
|
||||
bool is_streaming;
|
||||
bool is_request_finished = false;
|
||||
bool is_response_finished = false;
|
||||
bool is_streaming = false;
|
||||
Charset request_charset;
|
||||
std::vector<protocol::Binary> request_data_blobs;
|
||||
Charset response_charset;
|
||||
std::vector<protocol::Binary> response_data_blobs;
|
||||
|
||||
RequestEntry(double timestamp, Charset request_charset, bool has_request_body)
|
||||
: timestamp(timestamp),
|
||||
is_request_finished(!has_request_body),
|
||||
request_charset(request_charset),
|
||||
response_charset(Charset::kBinary) {}
|
||||
};
|
||||
|
||||
class NetworkAgent : public protocol::Network::Backend {
|
||||
|
@ -29,6 +47,15 @@ class NetworkAgent : public protocol::Network::Backend {
|
|||
|
||||
protocol::DispatchResponse disable() override;
|
||||
|
||||
protocol::DispatchResponse getRequestPostData(
|
||||
const protocol::String& in_requestId,
|
||||
protocol::String* out_postData) override;
|
||||
|
||||
protocol::DispatchResponse getResponseBody(
|
||||
const protocol::String& in_requestId,
|
||||
protocol::String* out_body,
|
||||
bool* out_base64Encoded) override;
|
||||
|
||||
protocol::DispatchResponse streamResourceContent(
|
||||
const protocol::String& in_requestId,
|
||||
protocol::Binary* out_bufferedData) override;
|
||||
|
@ -49,6 +76,8 @@ class NetworkAgent : public protocol::Network::Backend {
|
|||
void loadingFinished(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Object> params);
|
||||
|
||||
void dataSent(v8::Local<v8::Context> context, v8::Local<v8::Object> params);
|
||||
|
||||
void dataReceived(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Object> params);
|
||||
|
||||
|
|
|
@ -165,6 +165,7 @@ experimental domain Network
|
|||
string url
|
||||
string method
|
||||
Headers headers
|
||||
boolean hasPostData
|
||||
|
||||
# HTTP response data.
|
||||
type Response extends object
|
||||
|
@ -185,6 +186,26 @@ experimental domain Network
|
|||
# Enables network tracking, network events will now be delivered to the client.
|
||||
command enable
|
||||
|
||||
# Returns post data sent with the request. Returns an error when no data was sent with the request.
|
||||
command getRequestPostData
|
||||
parameters
|
||||
# Identifier of the network request to get content for.
|
||||
RequestId requestId
|
||||
returns
|
||||
# Request body string, omitting files from multipart requests
|
||||
string postData
|
||||
|
||||
# Returns content served for the given request.
|
||||
command getResponseBody
|
||||
parameters
|
||||
# Identifier of the network request to get content for.
|
||||
RequestId requestId
|
||||
returns
|
||||
# Response body.
|
||||
string body
|
||||
# True, if content was sent as base64.
|
||||
boolean base64Encoded
|
||||
|
||||
# Enables streaming of the response for the given requestId.
|
||||
# If enabled, the dataReceived event contains the data that was received during streaming.
|
||||
experimental command streamResourceContent
|
||||
|
|
|
@ -63,7 +63,12 @@ const EXPECTED_EVENTS = {
|
|||
},
|
||||
{
|
||||
name: 'dataReceived',
|
||||
// Network.dataReceived is buffered until Network.streamResourceContent is invoked.
|
||||
// Network.dataReceived is buffered until Network.streamResourceContent/Network.getResponseBody is invoked.
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: 'dataSent',
|
||||
// Network.dataSent is buffered until Network.getRequestPostData is invoked.
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
|
|
|
@ -15,7 +15,7 @@ const session = new inspector.Session();
|
|||
session.connect();
|
||||
session.post('Network.enable');
|
||||
|
||||
async function triggerNetworkEvents(requestId) {
|
||||
async function triggerNetworkEvents(requestId, charset) {
|
||||
const url = 'https://example.com';
|
||||
Network.requestWillBeSent({
|
||||
requestId,
|
||||
|
@ -42,6 +42,7 @@ async function triggerNetworkEvents(requestId) {
|
|||
headers: {
|
||||
mKey: 'mValue',
|
||||
},
|
||||
charset,
|
||||
},
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
@ -72,18 +73,30 @@ async function triggerNetworkEvents(requestId) {
|
|||
});
|
||||
}
|
||||
|
||||
function assertNetworkEvents(session, requestId) {
|
||||
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
session.on('Network.responseReceived', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
const loadingFinishedFuture = waitUntil(session, 'Network.loadingFinished')
|
||||
.then(async ([{ params }]) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
});
|
||||
|
||||
return loadingFinishedFuture;
|
||||
}
|
||||
|
||||
test('should stream Network.dataReceived with data chunks', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-1';
|
||||
const chunks = [];
|
||||
let totalDataLength = 0;
|
||||
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
const responseReceivedFuture = waitUntil(session, 'Network.responseReceived')
|
||||
.then(async ([{ params }]) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
.then(async () => {
|
||||
const { bufferedData } = await session.post('Network.streamResourceContent', {
|
||||
requestId,
|
||||
});
|
||||
|
@ -91,10 +104,6 @@ test('should stream Network.dataReceived with data chunks', async () => {
|
|||
totalDataLength += data.byteLength;
|
||||
chunks.push(data);
|
||||
});
|
||||
const loadingFinishedFuture = waitUntil(session, 'Network.loadingFinished')
|
||||
.then(([{ params }]) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
});
|
||||
session.on('Network.dataReceived', ({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
totalDataLength += params.dataLength;
|
||||
|
@ -114,16 +123,7 @@ test('Network.streamResourceContent should send all buffered chunks', async () =
|
|||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-2';
|
||||
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
session.on('Network.responseReceived', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
const loadingFinishedFuture = waitUntil(session, 'Network.loadingFinished')
|
||||
.then(async ([{ params }]) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
});
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
session.on('Network.dataReceived', common.mustNotCall());
|
||||
|
||||
await triggerNetworkEvents(requestId);
|
||||
|
@ -144,3 +144,35 @@ test('Network.streamResourceContent should reject if request id not found', asyn
|
|||
code: 'ERR_INSPECTOR_COMMAND',
|
||||
});
|
||||
});
|
||||
|
||||
test('Network.getResponseBody should send all buffered binary data', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-3';
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
session.on('Network.dataReceived', common.mustNotCall());
|
||||
|
||||
await triggerNetworkEvents(requestId);
|
||||
await loadingFinishedFuture;
|
||||
const { body, base64Encoded } = await session.post('Network.getResponseBody', {
|
||||
requestId,
|
||||
});
|
||||
assert.strictEqual(base64Encoded, true);
|
||||
assert.strictEqual(body, Buffer.from('Hello, world').toString('base64'));
|
||||
});
|
||||
|
||||
test('Network.getResponseBody should send all buffered text data', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-4';
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
session.on('Network.dataReceived', common.mustNotCall());
|
||||
|
||||
await triggerNetworkEvents(requestId, 'utf-8');
|
||||
await loadingFinishedFuture;
|
||||
const { body, base64Encoded } = await session.post('Network.getResponseBody', {
|
||||
requestId,
|
||||
});
|
||||
assert.strictEqual(base64Encoded, false);
|
||||
assert.strictEqual(body, 'Hello, world');
|
||||
});
|
||||
|
|
136
test/parallel/test-inspector-network-data-sent.js
Normal file
136
test/parallel/test-inspector-network-data-sent.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Flags: --inspect=0 --experimental-network-inspection
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
|
||||
common.skipIfInspectorDisabled();
|
||||
|
||||
const inspector = require('node:inspector/promises');
|
||||
const { Network } = require('node:inspector');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { waitUntil } = require('../common/inspector-helper');
|
||||
const { setTimeout } = require('node:timers/promises');
|
||||
|
||||
const session = new inspector.Session();
|
||||
session.connect();
|
||||
session.post('Network.enable');
|
||||
|
||||
async function triggerNetworkEvents(requestId, requestCharset) {
|
||||
const url = 'https://example.com';
|
||||
Network.requestWillBeSent({
|
||||
requestId,
|
||||
timestamp: 1,
|
||||
wallTime: 1,
|
||||
request: {
|
||||
url,
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
mKey: 'mValue',
|
||||
},
|
||||
hasPostData: true,
|
||||
},
|
||||
charset: requestCharset,
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
||||
const chunk1 = Buffer.from('Hello, ');
|
||||
Network.dataSent({
|
||||
requestId,
|
||||
timestamp: 2,
|
||||
dataLength: chunk1.byteLength,
|
||||
data: chunk1,
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
||||
const chunk2 = Buffer.from('world');
|
||||
Network.dataSent({
|
||||
requestId,
|
||||
timestamp: 3,
|
||||
dataLength: chunk2.byteLength,
|
||||
data: chunk2,
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
||||
Network.dataSent({
|
||||
requestId,
|
||||
finished: true,
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
||||
Network.responseReceived({
|
||||
requestId,
|
||||
timestamp: 4,
|
||||
type: 'Fetch',
|
||||
response: {
|
||||
url,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
headers: {
|
||||
mKey: 'mValue',
|
||||
},
|
||||
},
|
||||
});
|
||||
await setTimeout(1);
|
||||
|
||||
Network.loadingFinished({
|
||||
requestId,
|
||||
timestamp: 5,
|
||||
});
|
||||
}
|
||||
|
||||
function assertNetworkEvents(session, requestId) {
|
||||
session.on('Network.requestWillBeSent', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
session.on('Network.responseReceived', common.mustCall(({ params }) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
}));
|
||||
const loadingFinishedFuture = waitUntil(session, 'Network.loadingFinished')
|
||||
.then(async ([{ params }]) => {
|
||||
assert.strictEqual(params.requestId, requestId);
|
||||
});
|
||||
|
||||
return loadingFinishedFuture;
|
||||
}
|
||||
|
||||
test('Network.getRequestPostData should send all buffered text data', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-1';
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
|
||||
await triggerNetworkEvents(requestId, 'utf-8');
|
||||
await loadingFinishedFuture;
|
||||
|
||||
const { postData } = await session.post('Network.getRequestPostData', {
|
||||
requestId,
|
||||
});
|
||||
assert.strictEqual(postData, 'Hello, world');
|
||||
});
|
||||
|
||||
test('Network.getRequestPostData does not support binary data', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'my-req-id-2';
|
||||
const loadingFinishedFuture = assertNetworkEvents(session, requestId);
|
||||
|
||||
await triggerNetworkEvents(requestId);
|
||||
await loadingFinishedFuture;
|
||||
|
||||
await assert.rejects(session.post('Network.getRequestPostData', {
|
||||
requestId,
|
||||
}), {
|
||||
code: 'ERR_INSPECTOR_COMMAND',
|
||||
});
|
||||
});
|
||||
|
||||
test('Network.getRequestPostData should reject if request id not found', async () => {
|
||||
session.removeAllListeners();
|
||||
|
||||
const requestId = 'unknown-request-id';
|
||||
await assert.rejects(session.post('Network.getRequestPostData', {
|
||||
requestId,
|
||||
}), {
|
||||
code: 'ERR_INSPECTOR_COMMAND',
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue