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:
Chengzhong Wu 2025-06-20 11:20:37 +01:00 committed by RafaelGSS
parent b5ff3f42b8
commit be93091694
No known key found for this signature in database
GPG key ID: 8BEAB4DFCF555EF4
11 changed files with 483 additions and 88 deletions

View file

@ -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

View file

@ -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),
};

View file

@ -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,
};

View file

@ -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,
},

View file

@ -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,
},

View file

@ -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(&timestamp)) {
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(&timestamp)) {
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);
}
}

View file

@ -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);

View file

@ -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

View file

@ -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,
},
{

View file

@ -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');
});

View 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',
});
});