GeezerVison is showing github.com — inspector: support inspecting HTTP/2 request and response bodies · nodejs/node@d2087ba
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/inspector/network_http2.js +83Lines changed: 83 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -28,6 +28,8 @@ const { | 28 | 28 | HTTP2_HEADER_STATUS, | 29 | 29 | NGHTTP2_NO_ERROR, | 30 | 30 | } = internalBinding('http2').constants; | | 31 | +const EventEmitter = require('events'); | | 32 | +const { Buffer } = require('buffer'); | 31 | 33 |
| 32 | 34 | const kRequestUrl = Symbol('kRequestUrl'); | 33 | 35 |
| @@ -99,6 +101,7 @@ function onClientStreamCreated({ stream, headers }) { | 99 | 101 | url, | 100 | 102 | method, | 101 | 103 | headers: convertedHeaderObject, | | 104 | + hasPostData: !stream.writableEnded, | 102 | 105 | }, | 103 | 106 | }); | 104 | 107 | } | @@ -121,6 +124,66 @@ function onClientStreamError({ stream, error }) { | 121 | 124 | }); | 122 | 125 | } | 123 | 126 |
| | 127 | +/** | | 128 | + * When a chunk of the request body is being sent, cache it until `getRequestPostData` request. | | 129 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getRequestPostData | | 130 | + * @param {{ | | 131 | + * stream: import('http2').ClientHttp2Stream, | | 132 | + * writev: boolean, | | 133 | + * data: Buffer | string | Array<Buffer | {chunk: Buffer|string, encoding: string}>, | | 134 | + * encoding: string, | | 135 | + * }} event | | 136 | + */ | | 137 | +function onClientStreamBodyChunkSent({ stream, writev, data, encoding }) { | | 138 | + if (typeof stream[kInspectorRequestId] !== 'string') { | | 139 | + return; | | 140 | + } | | 141 | + | | 142 | + let chunk; | | 143 | + | | 144 | + if (writev) { | | 145 | + if (data.allBuffers) { | | 146 | + chunk = Buffer.concat(data); | | 147 | + } else { | | 148 | + const buffers = []; | | 149 | + for (let i = 0; i < data.length; ++i) { | | 150 | + if (typeof data[i].chunk === 'string') { | | 151 | + buffers.push(Buffer.from(data[i].chunk, data[i].encoding)); | | 152 | + } else { | | 153 | + buffers.push(data[i].chunk); | | 154 | + } | | 155 | + } | | 156 | + chunk = Buffer.concat(buffers); | | 157 | + } | | 158 | + } else if (typeof data === 'string') { | | 159 | + chunk = Buffer.from(data, encoding); | | 160 | + } else { | | 161 | + chunk = data; | | 162 | + } | | 163 | + | | 164 | + Network.dataSent({ | | 165 | + requestId: stream[kInspectorRequestId], | | 166 | + timestamp: getMonotonicTime(), | | 167 | + dataLength: chunk.byteLength, | | 168 | + data: chunk, | | 169 | + }); | | 170 | +} | | 171 | + | | 172 | +/** | | 173 | + * Mark a request body as fully sent. | | 174 | + * @param {{ stream: import('http2').ClientHttp2Stream }} event | | 175 | + */ | | 176 | +function onClientStreamBodySent({ stream }) { | | 177 | + if (typeof stream[kInspectorRequestId] !== 'string') { | | 178 | + return; | | 179 | + } | | 180 | + | | 181 | + Network.dataSent({ | | 182 | + requestId: stream[kInspectorRequestId], | | 183 | + finished: true, | | 184 | + }); | | 185 | +} | | 186 | + | 124 | 187 | /** | 125 | 188 | * When response headers are received, emit Network.responseReceived event. | 126 | 189 | * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#event-responseReceived | @@ -146,6 +209,24 @@ function onClientStreamFinish({ stream, headers }) { | 146 | 209 | charset, | 147 | 210 | }, | 148 | 211 | }); | | 212 | + | | 213 | + // Unlike stream.on('data', ...), this does not put the stream into flowing mode. | | 214 | + EventEmitter.prototype.on.call(stream, 'data', (chunk) => { | | 215 | + /** | | 216 | + * When a chunk of the response body has been received, cache it until `getResponseBody` request | | 217 | + * https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-getResponseBody or | | 218 | + * stream it with `streamResourceContent` request. | | 219 | + * https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-streamResourceContent | | 220 | + */ | | 221 | + | | 222 | + Network.dataReceived({ | | 223 | + requestId: stream[kInspectorRequestId], | | 224 | + timestamp: getMonotonicTime(), | | 225 | + dataLength: chunk.byteLength, | | 226 | + encodedDataLength: chunk.byteLength, | | 227 | + data: chunk, | | 228 | + }); | | 229 | + }); | 149 | 230 | } | 150 | 231 |
| 151 | 232 | /** | @@ -175,4 +256,6 @@ module.exports = registerDiagnosticChannels([ | 175 | 256 | ['http2.client.stream.error', onClientStreamError], | 176 | 257 | ['http2.client.stream.finish', onClientStreamFinish], | 177 | 258 | ['http2.client.stream.close', onClientStreamClose], | | 259 | + ['http2.client.stream.bodyChunkSent', onClientStreamBodyChunkSent], | | 260 | + ['http2.client.stream.bodySent', onClientStreamBodySent], | 178 | 261 | ]); |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-inspector-network-http2.js +50-10Lines changed: 50 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -14,12 +14,15 @@ const inspector = require('node:inspector/promises'); | 14 | 14 | const session = new inspector.Session(); | 15 | 15 | session.connect(); | 16 | 16 |
| | 17 | +const requestBody = { 'hello': 'world' }; | | 18 | + | 17 | 19 | const requestHeaders = { | 18 | 20 | 'x-header1': ['value1', 'value2'], | 19 | 21 | [http2.constants.HTTP2_HEADER_ACCEPT_LANGUAGE]: 'en-US', | 20 | 22 | [http2.constants.HTTP2_HEADER_AGE]: 1000, | | 23 | + [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: 'application/json; charset=utf-8', | 21 | 24 | [http2.constants.HTTP2_HEADER_COOKIE]: ['k1=v1', 'k2=v2'], | 22 | | - [http2.constants.HTTP2_HEADER_METHOD]: 'GET', | | 25 | + [http2.constants.HTTP2_HEADER_METHOD]: 'POST', | 23 | 26 | [http2.constants.HTTP2_HEADER_PATH]: '/hello-world', | 24 | 27 | }; | 25 | 28 |
| @@ -54,23 +57,35 @@ const pushResponseHeaders = { | 54 | 57 | [http2.constants.HTTP2_HEADER_STATUS]: 200, | 55 | 58 | }; | 56 | 59 |
| | 60 | +const styleCss = 'body { color: red; }\n'; | | 61 | +const serverResponse = 'hello world\n'; | | 62 | + | 57 | 63 | const kTimeout = 1000; | 58 | 64 | const kDelta = 200; | 59 | 65 |
| 60 | 66 | const handleStream = (stream, headers) => { | 61 | 67 | const path = headers[http2.constants.HTTP2_HEADER_PATH]; | | 68 | + let body = ''; | 62 | 69 | switch (path) { | 63 | 70 | case '/hello-world': | 64 | | - stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => { | 65 | | - pushStream.respond(pushResponseHeaders); | 66 | | - pushStream.end('body { color: red; }\n'); | 67 | | - })); | | 71 | + stream.on('data', (chunk) => { | | 72 | + body += chunk; | | 73 | + }); | 68 | 74 |
| 69 | | - stream.respond(responseHeaders); | | 75 | + stream.on('end', () => { | | 76 | + assert.strictEqual(body, JSON.stringify(requestBody)); | 70 | 77 |
| 71 | | - setTimeout(() => { | 72 | | - stream.end('hello world\n'); | 73 | | - }, kTimeout); | | 78 | + stream.pushStream(pushRequestHeaders, common.mustSucceed((pushStream) => { | | 79 | + pushStream.respond(pushResponseHeaders); | | 80 | + pushStream.end(styleCss); | | 81 | + })); | | 82 | + | | 83 | + stream.respond(responseHeaders); | | 84 | + | | 85 | + setTimeout(() => { | | 86 | + stream.end(serverResponse); | | 87 | + }, kTimeout); | | 88 | + }); | 74 | 89 | break; | 75 | 90 | case '/trigger-error': | 76 | 91 | stream.close(http2.constants.NGHTTP2_STREAM_CLOSED); | @@ -114,7 +129,6 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) { | 114 | 129 |
| 115 | 130 | assert.ok(params.requestId.startsWith('node-network-event-')); | 116 | 131 | assert.strictEqual(params.request.url, expectedUrl); | 117 | | - assert.strictEqual(params.request.method, 'GET'); | 118 | 132 | assert.strictEqual(typeof params.request.headers, 'object'); | 119 | 133 |
| 120 | 134 | if (expectedUrl.endsWith('/hello-world')) { | @@ -123,10 +137,17 @@ function verifyRequestWillBeSent({ method, params }, expectedUrl) { | 123 | 137 | assert.strictEqual(params.request.headers.age, '1000'); | 124 | 138 | assert.strictEqual(params.request.headers['x-header1'], 'value1, value2'); | 125 | 139 | assert.ok(findFrameInInitiator(__filename, params.initiator)); | | 140 | + assert.strictEqual(params.request.hasPostData, true); | | 141 | + assert.strictEqual(params.request.method, 'POST'); | 126 | 142 | } else if (expectedUrl.endsWith('/style.css')) { | 127 | 143 | assert.strictEqual(params.request.headers['x-header3'], 'value1, value2'); | 128 | 144 | assert.strictEqual(params.request.headers['x-push'], 'true'); | 129 | 145 | assert.ok(!findFrameInInitiator(__filename, params.initiator)); | | 146 | + assert.strictEqual(params.request.hasPostData, true); | | 147 | + assert.strictEqual(params.request.method, 'GET'); | | 148 | + } else { | | 149 | + assert.strictEqual(params.request.hasPostData, false); | | 150 | + assert.strictEqual(params.request.method, 'GET'); | 130 | 151 | } | 131 | 152 |
| 132 | 153 | assert.strictEqual(typeof params.timestamp, 'number'); | @@ -198,6 +219,8 @@ async function testHttp2(secure = false) { | 198 | 219 | rejectUnauthorized: false, | 199 | 220 | }); | 200 | 221 | const request = client.request(requestHeaders); | | 222 | + request.write(JSON.stringify(requestBody)); | | 223 | + request.end(); | 201 | 224 |
| 202 | 225 | // Dump the responses. | 203 | 226 | request.on('data', () => {}); | @@ -216,6 +239,11 @@ async function testHttp2(secure = false) { | 216 | 239 | verifyRequestWillBeSent(mainRequest, url); | 217 | 240 | verifyRequestWillBeSent(pushRequest, pushedUrl); | 218 | 241 |
| | 242 | + const { postData } = await session.post('Network.getRequestPostData', { | | 243 | + requestId: mainRequest.params.requestId | | 244 | + }); | | 245 | + assert.strictEqual(postData, JSON.stringify(requestBody)); | | 246 | + | 219 | 247 | const [ | 220 | 248 | { value: [ mainResponse ] }, | 221 | 249 | { value: [ pushResponse ] }, | @@ -230,6 +258,18 @@ async function testHttp2(secure = false) { | 230 | 258 | verifyLoadingFinished(event1); | 231 | 259 | verifyLoadingFinished(event2); | 232 | 260 |
| | 261 | + const responseBody = await session.post('Network.getResponseBody', { | | 262 | + requestId: mainRequest.params.requestId, | | 263 | + }); | | 264 | + assert.strictEqual(responseBody.base64Encoded, false); | | 265 | + assert.strictEqual(responseBody.body, serverResponse); | | 266 | + | | 267 | + const pushResponseBody = await session.post('Network.getResponseBody', { | | 268 | + requestId: pushRequest.params.requestId, | | 269 | + }); | | 270 | + assert.strictEqual(pushResponseBody.base64Encoded, true); | | 271 | + assert.strictEqual(Buffer.from(pushResponseBody.body, 'base64').toString(), styleCss); | | 272 | + | 233 | 273 | const mainFinished = [event1, event2] | 234 | 274 | .find((event) => event.params.requestId === mainResponse.params.requestId); | 235 | 275 | const pushFinished = [event1, event2] | |
|