Skip to content

Commit 3734609

Browse files
committed
inspector,http: support builtin http request bodies
Signed-off-by: GrinZero <[email protected]>
1 parent 5f92b6d commit 3734609

7 files changed

Lines changed: 323 additions & 32 deletions

File tree

lib/_http_client.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ const { Buffer } = require('buffer');
6060
const { defaultTriggerAsyncIdScope } = require('internal/async_hooks');
6161
const { URL, urlToHttpOptions, isURL } = require('internal/url');
6262
const {
63+
kIsClientRequest,
6364
kOutHeaders,
6465
kNeedDrain,
6566
isTraceHTTPEnabled,
@@ -192,6 +193,7 @@ function rewriteForProxiedHttp(req, reqOptions) {
192193

193194
function ClientRequest(input, options, cb) {
194195
OutgoingMessage.call(this);
196+
this[kIsClientRequest] = true;
195197

196198
if (typeof input === 'string') {
197199
const urlStr = input;

lib/_http_common.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
Uint8Array,
2828
} = primordials;
2929
const { setImmediate } = require('timers');
30+
const dc = require('diagnostics_channel');
3031

3132
const { methods, allMethods, HTTPParser } = internalBinding('http_parser');
3233
const { getOptionValue } = require('internal/options');
@@ -50,6 +51,9 @@ const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0;
5051
const kOnExecute = HTTPParser.kOnExecute | 0;
5152
const kOnTimeout = HTTPParser.kOnTimeout | 0;
5253

54+
const onClientResponseBodyChunkReceivedChannel =
55+
dc.channel('http.client.response.bodyChunkReceived');
56+
5357
const MAX_HEADER_PAIRS = 2000;
5458

5559
// Only called in the slow case where slow means
@@ -120,6 +124,7 @@ function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
120124
// client only
121125
incoming.statusCode = statusCode;
122126
incoming.statusMessage = statusMessage;
127+
incoming.req = socket?._httpMessage;
123128
}
124129

125130
return parser.onIncoming(incoming, shouldKeepAlive);
@@ -134,6 +139,13 @@ function parserOnBody(b) {
134139

135140
// Pretend this was the result of a stream._read call.
136141
if (!stream._dumped) {
142+
if (stream.req && onClientResponseBodyChunkReceivedChannel.hasSubscribers) {
143+
onClientResponseBodyChunkReceivedChannel.publish({
144+
request: stream.req,
145+
response: stream,
146+
chunk: b,
147+
});
148+
}
137149
const ret = stream.push(b);
138150
if (!ret)
139151
readStop(this.socket);

lib/_http_outgoing.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ const {
3636

3737
const { getDefaultHighWaterMark } = require('internal/streams/state');
3838
const assert = require('internal/assert');
39+
const dc = require('diagnostics_channel');
3940
const EE = require('events');
4041
const Stream = require('stream');
41-
const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
42+
const { kIsClientRequest, kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
4243
const { Buffer } = require('buffer');
4344
const {
4445
_checkIsHttpToken: checkIsHttpToken,
@@ -86,6 +87,12 @@ const kBytesWritten = Symbol('kBytesWritten');
8687
const kErrored = Symbol('errored');
8788
const kHighWaterMark = Symbol('kHighWaterMark');
8889
const kRejectNonStandardBodyWrites = Symbol('kRejectNonStandardBodyWrites');
90+
const kClientRequestBodyChunksWritten = Symbol('kClientRequestBodyChunksWritten');
91+
92+
const onClientRequestBodyChunkSentChannel =
93+
dc.channel('http.client.request.bodyChunkSent');
94+
const onClientRequestBodySentChannel =
95+
dc.channel('http.client.request.bodySent');
8996

9097
const nop = () => {};
9198

@@ -950,6 +957,17 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
950957
}
951958
}
952959

960+
if (msg[kIsClientRequest]) {
961+
msg[kClientRequestBodyChunksWritten] = true;
962+
if (onClientRequestBodyChunkSentChannel.hasSubscribers) {
963+
onClientRequestBodyChunkSentChannel.publish({
964+
request: msg,
965+
chunk,
966+
encoding,
967+
});
968+
}
969+
}
970+
953971
if (!fromEnd && msg.socket && !msg.socket.writableCorked) {
954972
msg.socket.cork();
955973
process.nextTick(connectionCorkNT, msg.socket);
@@ -1103,6 +1121,12 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11031121

11041122
this.finished = true;
11051123

1124+
if (this[kIsClientRequest] &&
1125+
this[kClientRequestBodyChunksWritten] &&
1126+
onClientRequestBodySentChannel.hasSubscribers) {
1127+
onClientRequestBodySentChannel.publish({ request: this });
1128+
}
1129+
11061130
// There is the first message on the outgoing queue, and we've sent
11071131
// everything to the socket.
11081132
debug('outgoing message end.');

lib/internal/http.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ function getGlobalAgent(proxyEnv, Agent) {
262262
}
263263

264264
module.exports = {
265+
kIsClientRequest: Symbol('kIsClientRequest'),
265266
kOutHeaders: Symbol('kOutHeaders'),
266267
kNeedDrain: Symbol('kNeedDrain'),
267268
kProxyConfig: Symbol('kProxyConfig'),

lib/internal/inspector/network_http.js

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const {
1717
sniffMimeType,
1818
} = require('internal/inspector/network');
1919
const { Network } = require('inspector');
20-
const EventEmitter = require('events');
20+
const { Buffer } = require('buffer');
2121

2222
const kRequestUrl = Symbol('kRequestUrl');
2323

@@ -95,6 +95,61 @@ function onClientRequestError({ request, error }) {
9595
});
9696
}
9797

98+
/**
99+
* When a chunk of the request body is being sent, cache it until
100+
* `getRequestPostData` request.
101+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getRequestPostData
102+
* @param {{ request: import('http').ClientRequest, chunk: Uint8Array | string, encoding?: string }} event
103+
*/
104+
function onClientRequestBodyChunkSent({ request, chunk, encoding }) {
105+
if (typeof request[kInspectorRequestId] !== 'string') {
106+
return;
107+
}
108+
109+
const buffer = typeof chunk === 'string' ? Buffer.from(chunk, encoding) : Buffer.from(chunk);
110+
Network.dataSent({
111+
requestId: request[kInspectorRequestId],
112+
timestamp: getMonotonicTime(),
113+
dataLength: buffer.byteLength,
114+
data: buffer,
115+
});
116+
}
117+
118+
/**
119+
* Mark a request body as fully sent.
120+
* @param {{ request: import('http').ClientRequest }} event
121+
*/
122+
function onClientRequestBodySent({ request }) {
123+
if (typeof request[kInspectorRequestId] !== 'string') {
124+
return;
125+
}
126+
127+
Network.dataSent({
128+
requestId: request[kInspectorRequestId],
129+
finished: true,
130+
});
131+
}
132+
133+
/**
134+
* When a chunk of the response body is received, cache the raw bytes until
135+
* `getResponseBody` request.
136+
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody
137+
* @param {{ request: import('http').ClientRequest, chunk: Uint8Array }} event
138+
*/
139+
function onClientResponseBodyChunkReceived({ request, chunk }) {
140+
if (typeof request[kInspectorRequestId] !== 'string') {
141+
return;
142+
}
143+
144+
Network.dataReceived({
145+
requestId: request[kInspectorRequestId],
146+
timestamp: getMonotonicTime(),
147+
dataLength: chunk.byteLength,
148+
encodedDataLength: chunk.byteLength,
149+
data: chunk,
150+
});
151+
}
152+
98153
/**
99154
* When response headers are received, emit Network.responseReceived event.
100155
* https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived
@@ -121,17 +176,6 @@ function onClientResponseFinish({ request, response }) {
121176
},
122177
});
123178

124-
// Unlike response.on('data', ...), this does not put the stream into flowing mode.
125-
EventEmitter.prototype.on.call(response, 'data', (chunk) => {
126-
Network.dataReceived({
127-
requestId: request[kInspectorRequestId],
128-
timestamp: getMonotonicTime(),
129-
dataLength: chunk.byteLength,
130-
encodedDataLength: chunk.byteLength,
131-
data: chunk,
132-
});
133-
});
134-
135179
// Wait until the response body is consumed by user code.
136180
response.once('end', () => {
137181
Network.loadingFinished({
@@ -143,6 +187,9 @@ function onClientResponseFinish({ request, response }) {
143187

144188
module.exports = registerDiagnosticChannels([
145189
['http.client.request.created', onClientRequestCreated],
190+
['http.client.request.bodyChunkSent', onClientRequestBodyChunkSent],
191+
['http.client.request.bodySent', onClientRequestBodySent],
146192
['http.client.request.error', onClientRequestError],
193+
['http.client.response.bodyChunkReceived', onClientResponseBodyChunkReceived],
147194
['http.client.response.finish', onClientResponseFinish],
148195
]);

test/parallel/test-diagnostics-channel-http.js

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,40 @@ const isError = (error) => error instanceof Error;
1414

1515
dc.subscribe('http.client.request.start', common.mustCall(({ request }) => {
1616
assert.strictEqual(isOutgoingMessage(request), true);
17+
}, 4));
18+
19+
dc.subscribe('http.client.request.bodyChunkSent', common.mustCall(({ request, chunk, encoding }) => {
20+
assert.strictEqual(isOutgoingMessage(request), true);
21+
assert.ok(typeof chunk === 'string' || chunk instanceof Uint8Array);
22+
assert.strictEqual(typeof encoding === 'string' || encoding == null, true);
23+
}, 3));
24+
25+
dc.subscribe('http.client.request.bodySent', common.mustCall(({ request }) => {
26+
assert.strictEqual(isOutgoingMessage(request), true);
1727
}, 2));
1828

1929
dc.subscribe('http.client.request.error', common.mustCall(({ request, error }) => {
2030
assert.strictEqual(isOutgoingMessage(request), true);
2131
assert.strictEqual(isError(error), true);
2232
}));
2333

34+
dc.subscribe('http.client.response.bodyChunkReceived', common.mustCall(({
35+
request,
36+
response,
37+
chunk,
38+
}) => {
39+
assert.strictEqual(isOutgoingMessage(request), true);
40+
assert.strictEqual(isIncomingMessage(response), true);
41+
assert.ok(chunk instanceof Uint8Array);
42+
}, 3));
43+
2444
dc.subscribe('http.client.response.finish', common.mustCall(({
2545
request,
2646
response
2747
}) => {
2848
assert.strictEqual(isOutgoingMessage(request), true);
2949
assert.strictEqual(isIncomingMessage(response), true);
30-
}));
50+
}, 3));
3151

3252
dc.subscribe('http.server.request.start', common.mustCall(({
3353
request,
@@ -39,7 +59,7 @@ dc.subscribe('http.server.request.start', common.mustCall(({
3959
assert.strictEqual(isOutgoingMessage(response), true);
4060
assert.strictEqual(isNetSocket(socket), true);
4161
assert.strictEqual(isHTTPServer(server), true);
42-
}));
62+
}, 3));
4363

4464
dc.subscribe('http.server.response.finish', common.mustCall(({
4565
request,
@@ -51,24 +71,37 @@ dc.subscribe('http.server.response.finish', common.mustCall(({
5171
assert.strictEqual(isOutgoingMessage(response), true);
5272
assert.strictEqual(isNetSocket(socket), true);
5373
assert.strictEqual(isHTTPServer(server), true);
54-
}));
74+
}, 3));
5575

5676
dc.subscribe('http.server.response.created', common.mustCall(({
5777
request,
5878
response,
5979
}) => {
6080
assert.strictEqual(isIncomingMessage(request), true);
6181
assert.strictEqual(isOutgoingMessage(response), true);
62-
}));
82+
}, 3));
6383

6484
dc.subscribe('http.client.request.created', common.mustCall(({ request }) => {
6585
assert.strictEqual(isOutgoingMessage(request), true);
6686
assert.strictEqual(isHTTPServer(server), true);
67-
}, 2));
87+
}, 4));
6888

6989
const server = http.createServer(common.mustCall((req, res) => {
70-
res.end('done');
71-
}));
90+
const chunks = [];
91+
req.on('data', (chunk) => chunks.push(chunk));
92+
req.on('end', common.mustCall(() => {
93+
if (req.method === 'POST' && req.url === '/string-body') {
94+
assert.strictEqual(Buffer.concat(chunks).toString(), 'foobar');
95+
} else if (req.method === 'POST' && req.url === '/binary-body') {
96+
assert.deepStrictEqual(Buffer.concat(chunks), Buffer.from([0, 1, 2, 3]));
97+
} else {
98+
assert.strictEqual(req.method, 'GET');
99+
assert.strictEqual(req.url, '/');
100+
assert.strictEqual(Buffer.concat(chunks).byteLength, 0);
101+
}
102+
res.end('done');
103+
}));
104+
}, 3));
72105

73106
server.listen(async () => {
74107
const { port } = server.address();
@@ -78,10 +111,33 @@ server.listen(async () => {
78111
await new Promise((resolve) => {
79112
invalidRequest.on('error', resolve);
80113
});
81-
http.get(`http://localhost:${port}`, (res) => {
82-
res.resume();
83-
res.on('end', () => {
84-
server.close();
114+
await new Promise((resolve, reject) => {
115+
http.get(`http://localhost:${port}`, (res) => {
116+
res.setEncoding('utf8');
117+
res.resume();
118+
res.on('end', resolve);
119+
}).on('error', reject);
120+
});
121+
await new Promise((resolve, reject) => {
122+
const req = http.request(`http://localhost:${port}/string-body`, {
123+
method: 'POST',
124+
}, (res) => {
125+
res.resume();
126+
res.on('end', resolve);
127+
});
128+
req.on('error', reject);
129+
req.write('foo');
130+
req.end('bar');
131+
});
132+
await new Promise((resolve, reject) => {
133+
const req = http.request(`http://localhost:${port}/binary-body`, {
134+
method: 'POST',
135+
}, (res) => {
136+
res.resume();
137+
res.on('end', resolve);
85138
});
139+
req.on('error', reject);
140+
req.end(Buffer.from([0, 1, 2, 3]));
86141
});
142+
server.close();
87143
});

0 commit comments

Comments
 (0)