Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
fdbcc23
node inspect: match either path separator inside a probe suffix on Wi…
cirospaciari Jul 24, 2026
c46c9ab
ci: allow the binary-size increase for the node inspect subsystem [al…
cirospaciari Jul 24, 2026
a88b213
Merge remote-tracking branch 'origin/claude/node-v26-combined-34719' …
cirospaciari Jul 25, 2026
804bdf9
http2: fix wire behavior of stream.close(code) and pushStream header …
cirospaciari Jul 25, 2026
531c2e6
node:inspector: emit Network events for http/http2/fetch clients
cirospaciari Jul 25, 2026
8771468
process: implement _debugProcess on POSIX
cirospaciari Jul 25, 2026
701a20b
cluster: allocate consecutive inspector ports for forked workers
cirospaciari Jul 25, 2026
25622f8
node:inspector: deliver deferred in-process replies before loop exit;…
cirospaciari Jul 25, 2026
2470186
Merge remote-tracking branch 'origin/claude/node-v26-combined-34719' …
cirospaciari Jul 25, 2026
be49ac7
cluster: build the full ERR_SOCKET_BAD_PORT message for inspectPort: …
cirospaciari Jul 25, 2026
986a45d
Merge remote-tracking branch 'origin/claude/node-v26-combined-34719' …
cirospaciari Jul 25, 2026
6290ca2
Revert "process: implement _debugProcess on POSIX"
cirospaciari Jul 25, 2026
592c075
http2: treat a peer RST_STREAM(NO_ERROR) as a clean close; flush defe…
cirospaciari Jul 25, 2026
1b4df4b
[autofix.ci] apply automated fixes
autofix-ci[bot] Jul 25, 2026
d3fe524
trim comments to <=3 lines, cite spec/node source
robobun Aug 3, 2026
f8b8f84
fix CI: oxlint errors, h2/inspector test snapshots, skip JSC exceptio…
robobun Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/js/internal/cluster/primary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const RoundRobinHandle = require("internal/cluster/RoundRobinHandle");
const SharedHandle = require("internal/cluster/SharedHandle");
const path = require("node:path");
const { throwNotImplemented, kHandle } = require("internal/shared");
const { getInspectPort, isUsingInspector } = require("internal/util/inspector");

const sendHelper = $newRustFunction("node_cluster_binding.rs", "sendHelperPrimary", 4);
const onInternalMessage = $newRustFunction("node_cluster_binding.rs", "onInternalMessagePrimary", 3);
Expand Down Expand Up @@ -81,12 +82,12 @@ function createWorkerProcess(id, env) {
const workerEnv = { ...process.env, ...env, NODE_UNIQUE_ID: `${id}` };
const execArgv = [...cluster.settings.execArgv];

// if (cluster.settings.inspectPort === null) {
// throw new ERR_SOCKET_BAD_PORT("Port", null, true);
// }
// if (isUsingInspector(cluster.settings.execArgv)) {
// ArrayPrototypePush(execArgv, `--inspect-port=${getInspectPort(cluster.settings.inspectPort)}`);
// }
if (cluster.settings.inspectPort === null) {
throw $ERR_SOCKET_BAD_PORT("Port should be >= 0 and < 65536. Received null.");
}
if (isUsingInspector(cluster.settings.execArgv)) {
execArgv.push(`--inspect-port=${getInspectPort(cluster.settings.inspectPort)}`);
}

child_process ??= require("node:child_process");
return child_process.fork(cluster.settings.exec, cluster.settings.args, {
Expand Down
8 changes: 7 additions & 1 deletion src/js/internal/debugger/inspect_probe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -958,8 +958,14 @@ class ProbeInspectorSession {
? SideEffectFreeRegExpPrototypeSymbolReplace(/\\/g, target.suffix, "/")
: target.suffix;
const escapedPath = SideEffectFreeRegExpPrototypeSymbolReplace(/([/\\.?*()^${}|[\]])/g, normalizedFile, "\\$1");
// Separators *inside* a multi-segment suffix were pinned to "/", so "dir/file.js" never
// matched a Windows script URL spelled "...\dir\file.js". POSIX keeps "\" literal.
const pathPattern =
process.platform === "win32"
? SideEffectFreeRegExpPrototypeSymbolReplace(/\\\//g, escapedPath, "[\\/\\\\]")
: escapedPath;
const params = {
urlRegex: `^(.*[\\/\\\\])?${escapedPath}$`,
urlRegex: `^(.*[\\/\\\\])?${pathPattern}$`,
// CDP locations are 0-based, the probe target from CLI is 1-based.
lineNumber: target.line - 1,
};
Expand Down
22 changes: 7 additions & 15 deletions src/js/internal/inspector/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,10 +725,11 @@ class InspectorCDPAdapter {
// An error VALUE with a preview: JSC caps preview properties at five, and
// an error's five JSC location properties crowd `stack` out entirely, so
// recover it from the object itself (V8 lists it first).
if (remote?.subtype === "error" && remote.preview && remote.objectId) {
const previewObjectId = remote?.objectId;
if (remote?.subtype === "error" && remote.preview && previewObjectId) {
this.#sendToBackend(
"Runtime.getProperties",
{ objectId: remote.objectId, ownProperties: true },
{ objectId: previewObjectId, ownProperties: true },
null,
method,
(props, error) => {
Expand Down Expand Up @@ -1075,7 +1076,7 @@ class InspectorCDPAdapter {
const pending = this.#pending.$get(id);
if (!pending) return;
this.#pending.$delete(id);
const { clientId, onResult } = pending;
const { clientId, onResult, method: pendingMethod } = pending;
if (onResult) {
onResult(parsed.result || {}, error);
return;
Expand All @@ -1085,11 +1086,11 @@ class InspectorCDPAdapter {
this.#replyErrorToClient(clientId, error.code ?? -32000, toCdpErrorMessage(error.message));
return;
}
if (EVALUATE_LIKE_METHODS.$has(pending.method)) {
this.#replyEvaluateLike(clientId, pending.method, parsed.result || {});
if (EVALUATE_LIKE_METHODS.$has(pendingMethod)) {
this.#replyEvaluateLike(clientId, pendingMethod, parsed.result || {});
return;
}
this.#replyToClient(clientId, this.#translateResult(pending.method, parsed.result || {}));
this.#replyToClient(clientId, this.#translateResult(pendingMethod, parsed.result || {}));
return;
}
if (typeof method === "string") {
Expand Down Expand Up @@ -1630,15 +1631,6 @@ class InspectorCDPAdapter {
case "Debugger.getPossibleBreakpoints":
return { locations: this.#toOriginalLocations(result.locations) };

case "Debugger.setBreakpointByUrl":
return { breakpointId: result.breakpointId, locations: this.#toOriginalLocations(result.locations) };

case "Debugger.setBreakpoint":
return {
breakpointId: result.breakpointId,
actualLocation: this.#toOriginalLocation(result.actualLocation ?? result.location),
};

default:
return result;
}
Expand Down
92 changes: 92 additions & 0 deletions src/js/internal/inspector/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Port of Node v26.3.0 lib/internal/inspector/network.js: shared helpers for
// the Network-domain instrumentation of the http/http2/fetch clients.
const { MIMEType } = require("internal/util/mime");

const kInspectorRequestId = Symbol("kInspectorRequestId");

// https://chromedevtools.github.io/devtools-protocol/1-3/Network/#type-ResourceType
const kResourceType = {
__proto__: null,
Document: "Document",
Stylesheet: "Stylesheet",
Image: "Image",
Media: "Media",
Font: "Font",
Script: "Script",
TextTrack: "TextTrack",
XHR: "XHR",
Fetch: "Fetch",
Prefetch: "Prefetch",
EventSource: "EventSource",
WebSocket: "WebSocket",
Manifest: "Manifest",
SignedExchange: "SignedExchange",
Ping: "Ping",
CSPViolationReport: "CSPViolationReport",
Preflight: "Preflight",
Other: "Other",
};

// Monotonic seconds since an arbitrary origin, the timestamp unit CDP uses.
function getMonotonicTime() {
return performance.now() / 1000;
}

const kMaxSafeInteger = Number.MAX_SAFE_INTEGER;
let requestId = 0;
function getNextRequestId() {
if (requestId === kMaxSafeInteger) {
requestId = 0;
}
return `node-network-event-${++requestId}`;
}

function sniffMimeType(contentType: string) {
let mimeType: string;
let charset: string;
try {
const mimeTypeObj = new MIMEType(contentType);
mimeType = (mimeTypeObj.essence || "").toLowerCase();
charset = (mimeTypeObj.params.get("charset") || "").toLowerCase();
} catch {
mimeType = "";
charset = "";
}

return {
__proto__: null,
mimeType,
charset,
};
}

type ListenerPair = [string, (message: unknown) => void];

function registerDiagnosticChannels(listenerPairs: ListenerPair[]) {
const dc = require("node:diagnostics_channel");
function enable() {
for (const { 0: channel, 1: listener } of listenerPairs) {
dc.subscribe(channel, listener);
}
}

function disable() {
for (const { 0: channel, 1: listener } of listenerPairs) {
dc.unsubscribe(channel, listener);
}
}

return {
enable,
disable,
};
}

export default {
kInspectorRequestId,
kResourceType,
getMonotonicTime,
getNextRequestId,
registerDiagnosticChannels,
sniffMimeType,
};
205 changes: 205 additions & 0 deletions src/js/internal/inspector/network_fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// Network-domain instrumentation for fetch(). Bun's native fetch has no diagnostics_channel,
// so the global is swapped for a wrapper while inspection is enabled. Events mirror
// https://github.com/nodejs/node/blob/main/lib/internal/inspector/network_undici.js
const { kResourceType, getMonotonicTime, getNextRequestId, sniffMimeType } = require("internal/inspector/network");
const { Network } = require("node:inspector");

// Captured at module load: instrumentation must keep working (and stay
// tamper-proof) if user code later replaces these globals.
const NativeRequest = globalThis.Request;
const NativeHeaders = globalThis.Headers;

let originalFetch: typeof fetch | undefined;
let instrumentedFetch: typeof fetch | undefined;

function headersToDictionary(headers: Headers) {
const dict: Record<string, string> = {};
let charset = "";
let mimeType = "";
for (const { 0: key, 1: value } of headers) {
if (key === "set-cookie") continue;
if (key === "content-type") {
const result = sniffMimeType(value);
charset = result.charset;
mimeType = result.mimeType;
}
dict[key] = value;
}
// ChromeDevTools frontend treats 'set-cookie' as a special case
// https://github.com/ChromeDevTools/devtools-frontend/blob/4275917f84266ef40613db3c1784a25f902ea74e/front_end/core/sdk/NetworkRequest.ts#L1368
const setCookie = headers.getSetCookie();
if (setCookie.length > 0) dict["set-cookie"] = setCookie.join("\n");
return [dict, charset, mimeType] as const;
}

function emitRequestWillBeSent(requestId: string, input: unknown, init: any) {
let url: string;
let method: string | undefined;
let headersInit: unknown;
let hasPostData = false;

if ($isObject(input) && input instanceof NativeRequest) {
url = (input as Request).url;
method = init?.method ?? (input as Request).method;
headersInit = init?.headers ?? (input as Request).headers;
hasPostData = init?.body != null || (input as Request).body != null;
} else {
url = `${input}`;
method = init?.method;
headersInit = init?.headers;
hasPostData = init?.body != null;
}
try {
url = new URL(url).href;
} catch {}

let headers: Record<string, string> = {};
let charset = "";
try {
const { 0: dict, 1: requestCharset } = headersToDictionary(new NativeHeaders(headersInit as HeadersInit));
headers = dict;
charset = requestCharset;
} catch {}

Network.requestWillBeSent({
requestId,
timestamp: getMonotonicTime(),
wallTime: Date.now(),
charset,
request: {
url,
method: typeof method === "string" && method.length > 0 ? method.toUpperCase() : "GET",
headers,
hasPostData,
},
});
return url;
}

function emitLoadingFailed(requestId: string, error: unknown) {
let errorText: string;
try {
errorText = `${(error as Error)?.message ?? error}`;
} catch {
errorText = "fetch failed";
}
Network.loadingFailed({
requestId,
timestamp: getMonotonicTime(),
type: kResourceType.Fetch,
errorText,
});
}

function emitLoadingFinished(requestId: string) {
Network.loadingFinished({
requestId,
timestamp: getMonotonicTime(),
});
}

// Reads the response clone so the response body reaches the session's buffer
// (Network.getResponseBody) and loadingFinished fires once the body is
// complete, whether or not user code consumes its branch of the tee.
async function pumpResponseClone(requestId: string, body: ReadableStream<Uint8Array>) {
const reader = body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
Network.dataReceived({
requestId,
timestamp: getMonotonicTime(),
dataLength: value.byteLength,
encodedDataLength: value.byteLength,
data: value,
});
}
}

function emitResponseReceived(requestId: string, requestUrl: string, response: Response) {
const { 0: headers, 1: charset, 2: mimeType } = headersToDictionary(response.headers);
Network.responseReceived({
requestId,
timestamp: getMonotonicTime(),
type: kResourceType.Fetch,
response: {
url: response.url || requestUrl,
status: response.status,
statusText: response.statusText,
headers,
mimeType,
charset,
},
});

let clonedBody: ReadableStream<Uint8Array> | null = null;
try {
if (response.body !== null && !response.bodyUsed) {
clonedBody = response.clone().body;
}
} catch {}
if (clonedBody === null) {
emitLoadingFinished(requestId);
return;
}
pumpResponseClone(requestId, clonedBody).then(
() => emitLoadingFinished(requestId),
(error: unknown) => emitLoadingFailed(requestId, error),
);
}

function makeInstrumentedFetch(original: typeof fetch): typeof fetch {
const wrapped = function fetch(input: unknown, init?: unknown) {
const requestId = getNextRequestId();
let requestUrl = "";
// Instrumentation must never turn a working fetch into a throwing one.
try {
requestUrl = emitRequestWillBeSent(requestId, input, init);
} catch {}
let result: Promise<Response>;
try {
result = original.$call(globalThis, input, init);
} catch (error) {
emitLoadingFailed(requestId, error);
throw error;
}
return result.then(
(response: Response) => {
try {
emitResponseReceived(requestId, requestUrl, response);
} catch {}
return response;
},
(error: unknown) => {
emitLoadingFailed(requestId, error);
throw error;
},
);
} as typeof fetch;
// Bun's fetch carries additional properties (e.g. fetch.preconnect).
try {
Object.setPrototypeOf(wrapped, original);
} catch {}
return wrapped;
}

function enable() {
if (instrumentedFetch !== undefined) return;
const current = globalThis.fetch;
if (typeof current !== "function") return;
originalFetch = current;
instrumentedFetch = makeInstrumentedFetch(current);
globalThis.fetch = instrumentedFetch;
}

function disable() {
if (instrumentedFetch === undefined) return;
// Only restore a slot that still holds our wrapper.
if (globalThis.fetch === instrumentedFetch) {
globalThis.fetch = originalFetch!;
}
originalFetch = undefined;
instrumentedFetch = undefined;
}

export default { enable, disable };
Loading
Loading