Skip to content

Commit 280a349

Browse files
authored
feat(ext/node): make Network.* CDP events fire under plain --inspect (#34270)
Network.* CDP events for fetch and WebSocket only fired when user code imported node:inspector — the bridge that ext/fetch and ext/websocket look up on internals.__inspectorNetwork was installed inside that polyfill's module body. Attaching Chrome DevTools to a script that didn't import node:inspector showed nothing in the Network tab. Moves the bridge install into a small always-loaded script that runs at runtime startup via the node:process bootstrap. Also wires the missing WebSocket frame events (frameSent / frameReceived / frameError) and instruments Deno.upgradeWebSocket so server-side sockets accepted via Deno.serve show up alongside their clients in the inspector.
1 parent ddc9fac commit 280a349

8 files changed

Lines changed: 331 additions & 39 deletions

File tree

ext/http/02_websocket.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ function upgradeWebSocket(request, options = { __proto__: null }) {
9595
_role,
9696
_serverHandleIdleTimeout,
9797
createWebSocketBranded,
98+
installServerInspector,
9899
SERVER,
99100
WebSocket,
100101
} = loadWebSocket();
@@ -116,6 +117,7 @@ function upgradeWebSocket(request, options = { __proto__: null }) {
116117

117118
socket[_rid] = wsRid;
118119
socket[_readyState] = WebSocket.OPEN;
120+
installServerInspector(socket, request.url);
119121
const event = new Event("open");
120122
socket.dispatchEvent(event);
121123

@@ -199,6 +201,7 @@ function upgradeWebSocket(request, options = { __proto__: null }) {
199201

200202
socket[_rid] = wsRid;
201203
socket[_readyState] = WebSocket.OPEN;
204+
installServerInspector(socket, request.url);
202205
socket.dispatchEvent(new Event("open"));
203206

204207
socket[_eventLoop]();

ext/node/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ deno_core::extension!(deno_node,
555555
"http2.ts",
556556
"https.ts",
557557
"inspector.js",
558+
"inspector_network_bridge.js",
558559
"inspector/promises.js",
559560
"internal/streams/duplex.js",
560561
"internal/streams/passthrough.js",

ext/node/polyfills/inspector.js

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// deno-lint-ignore-file prefer-primordials
55

66
(function () {
7-
const { core, internals, primordials } = __bootstrap;
7+
const { core, primordials } = __bootstrap;
88
const {
99
op_base64_encode_from_buffer,
1010
op_get_extras_binding_object,
@@ -305,32 +305,6 @@ const Network = {
305305
broadcastToFrontend("Network.webSocketClosed", params),
306306
};
307307

308-
// Bridge for other extensions (fetch, websocket, http) to emit Network.*
309-
// inspector events without depending on ext/node directly. Populated when
310-
// node:inspector is loaded; ext/fetch and friends look it up lazily on
311-
// `internals.__inspectorNetwork`.
312-
let networkRequestIdCounter = 0;
313-
internals.__inspectorNetwork = {
314-
isEnabled: () => op_inspector_enabled(),
315-
nextRequestId: () => `node-network-event-${++networkRequestIdCounter}`,
316-
requestWillBeSent: Network.requestWillBeSent,
317-
responseReceived: Network.responseReceived,
318-
loadingFinished: Network.loadingFinished,
319-
loadingFailed: Network.loadingFailed,
320-
dataReceived: Network.dataReceived,
321-
dataSent: Network.dataSent,
322-
webSocketCreated: Network.webSocketCreated,
323-
// Not exposed on `inspector.Network` (Node doesn't expose it either - see
324-
// node_compat test-inspector-emit-protocol-event), but DevTools still
325-
// needs the event to populate the request-side Headers panel, so we
326-
// route it directly through `broadcastToFrontend`.
327-
webSocketWillSendHandshakeRequest: (params) =>
328-
broadcastToFrontend("Network.webSocketWillSendHandshakeRequest", params),
329-
webSocketHandshakeResponseReceived:
330-
Network.webSocketHandshakeResponseReceived,
331-
webSocketClosed: Network.webSocketClosed,
332-
};
333-
334308
const DOMStorage = {
335309
domStorageItemAdded: (params) =>
336310
broadcastToFrontend("DOMStorage.domStorageItemAdded", params),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
3+
// deno-lint-ignore-file prefer-primordials
4+
5+
// Installs `internals.__inspectorNetwork`, the bridge that other
6+
// extensions (ext/fetch, http, websocket) use to emit `Network.*` CDP
7+
// events without depending on ext/node directly. It needs to be
8+
// installed eagerly so that `deno run --inspect` alone is enough to
9+
// see fetch traffic in DevTools - previously the bridge lived in the
10+
// `node:inspector` polyfill and only appeared if user code imported
11+
// that module.
12+
13+
(function () {
14+
const { core, internals, primordials } = __bootstrap;
15+
const {
16+
op_base64_encode_from_buffer,
17+
op_inspector_emit_protocol_event,
18+
op_inspector_enabled,
19+
} = core.ops;
20+
const {
21+
JSONStringify,
22+
ObjectAssign,
23+
TypedArrayPrototypeGetByteLength,
24+
TypedArrayPrototypeGetSymbolToStringTag,
25+
Uint8Array,
26+
} = primordials;
27+
28+
function encodeNetworkData(data) {
29+
if (data == null) return undefined;
30+
if (typeof data === "string") {
31+
const buf = core.encode(data);
32+
return op_base64_encode_from_buffer(buf, 0, buf.byteLength);
33+
}
34+
if (TypedArrayPrototypeGetSymbolToStringTag(data) === "Uint8Array") {
35+
return op_base64_encode_from_buffer(
36+
data,
37+
0,
38+
TypedArrayPrototypeGetByteLength(data),
39+
);
40+
}
41+
if (data instanceof ArrayBuffer) {
42+
const view = new Uint8Array(data);
43+
return op_base64_encode_from_buffer(view, 0, view.byteLength);
44+
}
45+
throw new TypeError(
46+
"Expected data to be a string, Buffer, Uint8Array, or ArrayBuffer",
47+
);
48+
}
49+
50+
function emit(eventName, params) {
51+
op_inspector_emit_protocol_event(eventName, JSONStringify(params ?? {}));
52+
}
53+
54+
function emitWithData(eventName, params) {
55+
if (params && params.data !== undefined) {
56+
const encoded = encodeNetworkData(params.data);
57+
if (encoded !== params.data) {
58+
params = ObjectAssign({ __proto__: null }, params, { data: encoded });
59+
}
60+
}
61+
emit(eventName, params);
62+
}
63+
64+
// CDP's `webSocketFrameSent`/`webSocketFrameReceived` carry the payload
65+
// inside `params.response.payloadData`. Text frames go through as-is, binary
66+
// frames must be base64-encoded (DevTools uses `opcode` to decide how to
67+
// render the panel - 1=text, 2=binary).
68+
function emitFrame(eventName, params) {
69+
const payload = params?.response?.payloadData;
70+
if (payload != null && typeof payload !== "string") {
71+
let view = null;
72+
if (TypedArrayPrototypeGetSymbolToStringTag(payload) === "Uint8Array") {
73+
view = payload;
74+
} else if (payload instanceof ArrayBuffer) {
75+
view = new Uint8Array(payload);
76+
}
77+
if (view !== null) {
78+
const encoded = op_base64_encode_from_buffer(
79+
view,
80+
0,
81+
TypedArrayPrototypeGetByteLength(view),
82+
);
83+
params = ObjectAssign({ __proto__: null }, params, {
84+
response: ObjectAssign({ __proto__: null }, params.response, {
85+
payloadData: encoded,
86+
}),
87+
});
88+
}
89+
}
90+
emit(eventName, params);
91+
}
92+
93+
let networkRequestIdCounter = 0;
94+
internals.__inspectorNetwork = {
95+
isEnabled: () => op_inspector_enabled(),
96+
nextRequestId: () => `node-network-event-${++networkRequestIdCounter}`,
97+
requestWillBeSent: (p) => emit("Network.requestWillBeSent", p),
98+
responseReceived: (p) => emit("Network.responseReceived", p),
99+
loadingFinished: (p) => emit("Network.loadingFinished", p),
100+
loadingFailed: (p) => emit("Network.loadingFailed", p),
101+
dataReceived: (p) => emitWithData("Network.dataReceived", p),
102+
dataSent: (p) => emitWithData("Network.dataSent", p),
103+
webSocketCreated: (p) => emit("Network.webSocketCreated", p),
104+
// Not part of the public `inspector.Network` surface (Node doesn't
105+
// expose it either - see node_compat test-inspector-emit-protocol-event)
106+
// but DevTools still needs the event to populate the request-side
107+
// Headers panel for websocket connections.
108+
webSocketWillSendHandshakeRequest: (p) =>
109+
emit("Network.webSocketWillSendHandshakeRequest", p),
110+
webSocketHandshakeResponseReceived: (p) =>
111+
emit("Network.webSocketHandshakeResponseReceived", p),
112+
webSocketClosed: (p) => emit("Network.webSocketClosed", p),
113+
webSocketFrameSent: (p) => emitFrame("Network.webSocketFrameSent", p),
114+
webSocketFrameReceived: (p) => emitFrame("Network.webSocketFrameReceived", p),
115+
webSocketFrameError: (p) => emit("Network.webSocketFrameError", p),
116+
};
117+
})();

ext/node/polyfills/process.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
// deno-lint-ignore-file prefer-primordials
66

77
import { core, internals, primordials } from "ext:core/mod.js";
8+
// Installs `internals.__inspectorNetwork` so ext/fetch (and other
9+
// extensions) can emit Network.* CDP events without requiring user code
10+
// to import `node:inspector`. Side-effect import; no exports.
11+
core.loadExtScript("ext:deno_node/inspector_network_bridge.js");
812
const { initializeDebugEnv } = core.loadExtScript(
913
"ext:deno_node/internal/util/debuglog.ts",
1014
);

ext/websocket/01_websocket.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,62 @@ function emitWebSocketClosed(ws) {
191191
ws[_inspectorRequestId] = undefined;
192192
}
193193

194+
// Opcodes per RFC 6455 / CDP `WebSocketFrame.opcode`:
195+
// 1 = text, 2 = binary. Per RFC 6455 client-to-server frames are masked,
196+
// server-to-client are not - so the `mask` flag depends on direction and
197+
// role: a CLIENT socket masks what it sends, a SERVER socket sees masked
198+
// frames coming in.
199+
function emitWebSocketFrame(ws, eventName, opcode, payloadData) {
200+
const ins = getInspectorNetwork();
201+
const requestId = ws[_inspectorRequestId];
202+
if (ins === null || requestId === undefined) return;
203+
const sent = eventName === "frameSent";
204+
const mask = ws[_role] === SERVER ? !sent : sent;
205+
try {
206+
const params = {
207+
requestId,
208+
timestamp: DateNow() / 1000,
209+
response: { opcode, mask, payloadData },
210+
};
211+
if (sent) ins.webSocketFrameSent(params);
212+
else ins.webSocketFrameReceived(params);
213+
} catch {
214+
// never let inspector instrumentation break a real WebSocket
215+
}
216+
}
217+
218+
// Called by ext/http's `upgradeWebSocket` after a successful handshake, so
219+
// server-accepted sockets show up in the inspector's Network panel and
220+
// the frame emitters in `_eventLoop` / `send` start firing for them.
221+
// Handshake events (`willSendHandshakeRequest`, `handshakeResponseReceived`)
222+
// are intentionally skipped here - those are client-side concepts.
223+
function installServerInspector(ws, url) {
224+
const ins = getInspectorNetwork();
225+
if (ins === null) return;
226+
const requestId = ins.nextRequestId();
227+
ws[_inspectorRequestId] = requestId;
228+
try {
229+
ins.webSocketCreated({ requestId, url });
230+
} catch {
231+
// never let inspector instrumentation break a real WebSocket
232+
}
233+
}
234+
235+
function emitWebSocketFrameError(ws, errorMessage) {
236+
const ins = getInspectorNetwork();
237+
const requestId = ws[_inspectorRequestId];
238+
if (ins === null || requestId === undefined) return;
239+
try {
240+
ins.webSocketFrameError({
241+
requestId,
242+
timestamp: DateNow() / 1000,
243+
errorMessage,
244+
});
245+
} catch {
246+
// never let inspector instrumentation break a real WebSocket
247+
}
248+
}
249+
194250
class WebSocket extends EventTarget {
195251
constructor(url, initOrProtocols) {
196252
super();
@@ -573,8 +629,10 @@ class WebSocket extends EventTarget {
573629
// data is being sent.
574630
if (ArrayBufferIsView(data)) {
575631
op_ws_send_binary(this[_rid], data);
632+
emitWebSocketFrame(this, "frameSent", 2, data);
576633
} else if (isArrayBuffer(data)) {
577634
op_ws_send_binary_ab(this[_rid], data);
635+
emitWebSocketFrame(this, "frameSent", 2, data);
578636
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
579637
this[_queueSend](data);
580638
} else {
@@ -583,6 +641,7 @@ class WebSocket extends EventTarget {
583641
this[_rid],
584642
string,
585643
);
644+
emitWebSocketFrame(this, "frameSent", 1, string);
586645
}
587646
} else {
588647
// Slower path if the send queue is not empty, for example when sending
@@ -690,6 +749,7 @@ class WebSocket extends EventTarget {
690749
}
691750

692751
this[_serverHandleIdleTimeout]();
752+
emitWebSocketFrame(this, "frameReceived", 1, data);
693753
const event = new MessageEvent("message", {
694754
data,
695755
origin: this[_url],
@@ -708,6 +768,7 @@ class WebSocket extends EventTarget {
708768
this[_serverHandleIdleTimeout]();
709769
// deno-lint-ignore prefer-primordials
710770
const buffer = d.buffer;
771+
emitWebSocketFrame(this, "frameReceived", 2, buffer);
711772
let data;
712773
if (this.binaryType === "blob") {
713774
data = new Blob([buffer]);
@@ -733,6 +794,7 @@ class WebSocket extends EventTarget {
733794
this[_readyState] = CLOSED;
734795

735796
const message = op_ws_get_error(rid);
797+
emitWebSocketFrameError(this, message);
736798
const error = new Error(message);
737799
const errorEv = new ErrorEvent("error", {
738800
error,
@@ -797,18 +859,22 @@ class WebSocket extends EventTarget {
797859
const data = queue[0];
798860
if (ArrayBufferIsView(data)) {
799861
op_ws_send_binary(this[_rid], data);
862+
emitWebSocketFrame(this, "frameSent", 2, data);
800863
} else if (isArrayBuffer(data)) {
801864
op_ws_send_binary_ab(this[_rid], data);
865+
emitWebSocketFrame(this, "frameSent", 2, data);
802866
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, data)) {
803867
// deno-lint-ignore prefer-primordials
804868
const ab = await data.slice().arrayBuffer();
805869
op_ws_send_binary_ab(this[_rid], ab);
870+
emitWebSocketFrame(this, "frameSent", 2, ab);
806871
} else {
807872
const string = String(data);
808873
op_ws_send_text(
809874
this[_rid],
810875
string,
811876
);
877+
emitWebSocketFrame(this, "frameSent", 1, string);
812878
}
813879
ArrayPrototypeShift(queue);
814880
}
@@ -947,6 +1013,8 @@ export {
9471013
_serverHandleIdleTimeout,
9481014
CLIENT,
9491015
createWebSocketBranded,
1016+
emitWebSocketClosed,
1017+
installServerInspector,
9501018
SERVER,
9511019
WebSocket,
9521020
};

0 commit comments

Comments
 (0)