mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## Description `headroom wrap opencode` broke third-party MCP servers in pip/wheel installs. The wrap transport plugin appended `NODE_OPTIONS=--import=<plugin dir>/../hook-shim/handler.js` to its own env (and injected it into every child it spawns), but that path only resolves in a repo checkout. Wheel installs load the standalone bundle from `headroom/providers/opencode/_dist/`, which has no `hook-shim/` sibling — the shim lives under `plugins/` and maturin only ships files under `headroom/` (pyproject.toml `python-source`/package-dir behavior). Every Node child then aborted with `ERR_MODULE_NOT_FOUND` before executing a line, including OpenCode's stdio MCP servers. OpenCode reports that as `<server> MCP error -32000: Connection closed`. Headroom's own MCP server is a Python process, so it stayed connected — which is why the breakage looked selective, and why nothing appeared in the proxy logs (the failure is entirely inside OpenCode's child process). Docker and `--no-proxy` are incidental: the plugin installs the transport on load in every wrap mode. Fix: resolve the shim only when it exists on disk, and skip the `NODE_OPTIONS` mutation otherwise. Children go direct instead of dying. Checkout builds still get child-process transport hooking, unchanged. Closes #2798 ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) - [ ] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update - [ ] Performance improvement - [ ] Code refactoring (no functional changes) ## Changes Made - `plugins/opencode/src/transport.ts`: `shimImportSpecifier()` returns `string | undefined`, gated on `fs.existsSync`; `installProcessEnv()` and `withShimEnv()` leave `NODE_OPTIONS` untouched when the shim is absent. - `plugins/opencode/src/transport.test.ts`: new regression test — with the shim missing, the parent's `NODE_OPTIONS` is unmodified and a spawned `npx -y firecrawl-mcp` receives no `--import`. - `headroom/providers/opencode/_dist/entry.opencode.js`: regenerated via `npm run build:standalone` (the bundle that wheel installs actually load). ## Testing - [ ] Unit tests pass (`pytest`) - [ ] Linting passes (`ruff check .`) - [ ] Type checking passes (`mypy headroom`) - [x] New tests added for new functionality - [x] Manual testing performed No Python source changed, so `pytest` / `ruff` / `mypy` are N/A here; the TypeScript equivalents were run instead. ### Test Output ```text $ npm run typecheck > tsc --noEmit (no output) $ npm test RUN v4.1.9 /private/tmp/hr-pr-2798/plugins/opencode Test Files 2 passed (2) Tests 14 passed (14) Duration 416ms # The new test is not vacuous — reverting the guard to `return shim.href` reddens it: $ npx vitest run -t "#2798" Test Files 1 failed | 1 skipped (2) Tests 1 failed | 13 skipped (14) ``` ## Real Behavior Proof - Environment: macOS (darwin 25.4.0), Node v24, Bun present; both bundles loaded directly from disk. - Exact command / steps: load each built bundle, invoke the default plugin export, print `process.env.NODE_OPTIONS`, then `spawnSync(process.execPath, ["-e", "console.log('mcp server handshake ok')"])` — the same way OpenCode launches a stdio MCP server. ```text ### BEFORE (wheel layout, shim missing) ### NODE_OPTIONS: "--import=file:///…/headroom/providers/opencode/hook-shim/handler.js" child: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…/headroom/providers/opencode/hook-shim/handler.js' <-- becomes MCP -32000 ### AFTER — wheel layout (headroom/providers/opencode/_dist/) ### NODE_OPTIONS after plugin load: undefined child status: 0 | stdout: mcp server handshake ok ### AFTER — checkout layout (plugins/opencode/dist/, shim present) ### NODE_OPTIONS after plugin load: "--import=file:///…/plugins/opencode/hook-shim/handler.js" child status: 0 | stdout: mcp server handshake ok ``` - Observed result: wheel installs no longer poison child env, so Node MCP servers start; checkout builds keep the preload and still start children cleanly. - Not tested: no reproduction against a live `opencode` + codegraph/firecrawl session on Ubuntu (no OpenCode install on this machine); the child-process failure was reproduced directly instead, which is the exact mechanism behind the reported `-32000`. Docker proxy path not re-tested — it is unrelated to the fix. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] I did **not** edit `CHANGELOG.md` ## Additional Notes Docs unchanged: this is an internal packaging/runtime bug with no documented behavior attached. Follow-up (deliberately not in this PR): wheel installs now lose child-process transport hooking rather than crashing — the same coverage they effectively had, since the preload never once loaded from a wheel. Restoring it means a standalone shim build emitted into `_dist/` plus exporting `installHeadroomTransport` from that bundle; `hook-shim/handler.js` also imports `../dist/index.js`, which does not exist in the wheel layout, so copying the file alone would not be enough. Worth doing only if something needs a subprocess's LLM traffic proxied.
493 lines
16 KiB
TypeScript
493 lines
16 KiB
TypeScript
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
|
|
const nodeRequire = createRequire(import.meta.url);
|
|
const http = nodeRequire("node:http") as typeof import("node:http");
|
|
const https = nodeRequire("node:https") as typeof import("node:https");
|
|
const http2 = nodeRequire("node:http2") as typeof import("node:http2");
|
|
const childProcess = nodeRequire("node:child_process") as typeof import("node:child_process");
|
|
const fs = nodeRequire("node:fs") as typeof import("node:fs");
|
|
|
|
const BASE_URL_HEADER = "x-headroom-base-url";
|
|
const ORIGINAL_PATH_HEADER = "x-headroom-original-path";
|
|
const PROXY_ENV = "HEADROOM_OPENCODE_TRANSPORT_PROXY_URL";
|
|
const STATE_KEY = Symbol.for("headroom.opencode.transport");
|
|
|
|
type FetchArgs = Parameters<typeof fetch>;
|
|
type HttpRequest = typeof http.request;
|
|
type HttpGet = typeof http.get;
|
|
type HttpsRequest = typeof https.request;
|
|
type HttpsGet = typeof https.get;
|
|
type Http2Connect = typeof http2.connect;
|
|
type ChildSpawn = typeof childProcess.spawn;
|
|
type ChildExec = typeof childProcess.exec;
|
|
type ChildExecFile = typeof childProcess.execFile;
|
|
type ChildFork = typeof childProcess.fork;
|
|
|
|
interface InstallOptions {
|
|
proxyUrl: string;
|
|
debug?: boolean;
|
|
}
|
|
|
|
interface TransportState {
|
|
refs: number;
|
|
proxyUrl: string;
|
|
debug: boolean;
|
|
originalFetch: typeof fetch;
|
|
originalHttpRequest: HttpRequest;
|
|
originalHttpGet: HttpGet;
|
|
originalHttpsRequest: HttpsRequest;
|
|
originalHttpsGet: HttpsGet;
|
|
originalHttp2Connect: Http2Connect;
|
|
originalChildSpawn: ChildSpawn;
|
|
originalChildExec: ChildExec;
|
|
originalChildExecFile: ChildExecFile;
|
|
originalChildFork: ChildFork;
|
|
}
|
|
|
|
interface GlobalWithHeadroomTransport {
|
|
[STATE_KEY]?: TransportState;
|
|
}
|
|
|
|
interface NodeRequestParts {
|
|
url?: URL;
|
|
options: Record<string, unknown>;
|
|
callback?: (...args: unknown[]) => unknown;
|
|
}
|
|
|
|
function getState(): TransportState | undefined {
|
|
return (globalThis as GlobalWithHeadroomTransport)[STATE_KEY];
|
|
}
|
|
|
|
function setState(state: TransportState | undefined): void {
|
|
(globalThis as GlobalWithHeadroomTransport)[STATE_KEY] = state;
|
|
}
|
|
|
|
// ponytail: the shim only exists next to the checkout build
|
|
// (plugins/opencode/dist/). The wheel ships entry.opencode.js alone, so
|
|
// `--import=<missing file>` killed every Node child at startup — including
|
|
// OpenCode's stdio MCP servers (issue #2798). No shim on disk, no injection:
|
|
// children go direct instead of dying. Upgrade path is bundling the shim into
|
|
// _dist/ so wheel installs get child-process routing back.
|
|
function shimImportSpecifier(): string | undefined {
|
|
const shim = new URL("../hook-shim/handler.js", import.meta.url);
|
|
return fs.existsSync(shim) ? shim.href : undefined;
|
|
}
|
|
|
|
function withNodeImportOption(existing: string | undefined, shim: string): string {
|
|
const parts = existing?.trim() ? existing.trim().split(/\s+/) : [];
|
|
const alreadyPresent = parts.some((part, index) => {
|
|
return part === `--import=${shim}` || (part === "--import" && parts[index + 1] === shim);
|
|
});
|
|
if (!alreadyPresent) {
|
|
parts.push(`--import=${shim}`);
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
|
|
function withShimEnv(env: NodeJS.ProcessEnv | Record<string, unknown> | undefined, proxyUrl: string): NodeJS.ProcessEnv {
|
|
const nextEnv = { ...(env ?? process.env) } as NodeJS.ProcessEnv;
|
|
nextEnv[PROXY_ENV] = proxyUrl;
|
|
const shim = shimImportSpecifier();
|
|
if (shim) {
|
|
nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shim);
|
|
}
|
|
return nextEnv;
|
|
}
|
|
|
|
function installProcessEnv(proxyUrl: string): void {
|
|
process.env[PROXY_ENV] = proxyUrl;
|
|
const shim = shimImportSpecifier();
|
|
if (shim) {
|
|
process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shim);
|
|
}
|
|
}
|
|
|
|
function isOptions(value: unknown): value is Record<string, unknown> {
|
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !(value instanceof URL);
|
|
}
|
|
|
|
function injectOptionsEnv(args: unknown[], optionIndex: number, proxyUrl: string): unknown[] {
|
|
const nextArgs = [...args];
|
|
const callback = typeof nextArgs.at(-1) === "function" ? nextArgs.pop() : undefined;
|
|
const existing = isOptions(nextArgs[optionIndex]) ? { ...(nextArgs[optionIndex] as Record<string, unknown>) } : {};
|
|
existing.env = withShimEnv(existing.env as NodeJS.ProcessEnv | undefined, proxyUrl);
|
|
|
|
if (isOptions(nextArgs[optionIndex])) {
|
|
nextArgs[optionIndex] = existing;
|
|
} else {
|
|
nextArgs.splice(optionIndex, 0, existing);
|
|
}
|
|
|
|
if (callback) {
|
|
nextArgs.push(callback);
|
|
}
|
|
return nextArgs;
|
|
}
|
|
|
|
function wrapSpawn(originalSpawn: ChildSpawn): ChildSpawn {
|
|
return function headroomSpawn(this: unknown, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (!state) {
|
|
return Reflect.apply(originalSpawn, this, args);
|
|
}
|
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
return Reflect.apply(originalSpawn, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
|
|
} as ChildSpawn;
|
|
}
|
|
|
|
function wrapExec(originalExec: ChildExec): ChildExec {
|
|
return function headroomExec(this: unknown, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (!state) {
|
|
return Reflect.apply(originalExec, this, args);
|
|
}
|
|
return Reflect.apply(originalExec, this, injectOptionsEnv(args, 1, state.proxyUrl));
|
|
} as ChildExec;
|
|
}
|
|
|
|
function wrapExecFile(originalExecFile: ChildExecFile): ChildExecFile {
|
|
return function headroomExecFile(this: unknown, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (!state) {
|
|
return Reflect.apply(originalExecFile, this, args);
|
|
}
|
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
return Reflect.apply(originalExecFile, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
|
|
} as ChildExecFile;
|
|
}
|
|
|
|
function wrapFork(originalFork: ChildFork): ChildFork {
|
|
return function headroomFork(this: unknown, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (!state) {
|
|
return Reflect.apply(originalFork, this, args);
|
|
}
|
|
const optionIndex = Array.isArray(args[1]) ? 2 : 1;
|
|
return Reflect.apply(originalFork, this, injectOptionsEnv(args, optionIndex, state.proxyUrl));
|
|
} as ChildFork;
|
|
}
|
|
|
|
function normalizeProxyUrl(proxyUrl: string): URL {
|
|
return new URL(proxyUrl);
|
|
}
|
|
|
|
function isLoopback(hostname: string): boolean {
|
|
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
}
|
|
|
|
function shouldRoute(url: URL, proxy: URL): boolean {
|
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
return false;
|
|
}
|
|
if (isLoopback(url.hostname)) {
|
|
return false;
|
|
}
|
|
if (url.origin === proxy.origin) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function routedUrl(upstream: URL, proxy: URL): URL {
|
|
return new URL(`${upstream.pathname}${upstream.search}`, proxy.origin);
|
|
}
|
|
|
|
function normalizedOpenAiProxyPath(pathname: string): string | undefined {
|
|
if (pathname.endsWith("/chat/completions")) {
|
|
return "/v1/chat/completions";
|
|
}
|
|
if (pathname.endsWith("/responses")) {
|
|
return "/v1/responses";
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function routedUrlForOpenCode(upstream: URL, proxy: URL): { url: URL; originalPath: string | undefined } {
|
|
const normalizedPath = normalizedOpenAiProxyPath(upstream.pathname);
|
|
if (!normalizedPath) {
|
|
return {
|
|
url: routedUrl(upstream, proxy),
|
|
originalPath: undefined,
|
|
};
|
|
}
|
|
|
|
return {
|
|
url: new URL(`${normalizedPath}${upstream.search}`, proxy.origin),
|
|
originalPath: upstream.pathname,
|
|
};
|
|
}
|
|
|
|
function requestUrl(input: RequestInfo | URL): URL {
|
|
if (input instanceof Request) {
|
|
return new URL(input.url);
|
|
}
|
|
if (input instanceof URL) {
|
|
return input;
|
|
}
|
|
return new URL(String(input));
|
|
}
|
|
|
|
function mergeFetchHeaders(
|
|
input: RequestInfo | URL,
|
|
init: RequestInit | undefined,
|
|
upstream: URL | undefined,
|
|
originalPath: string | undefined = undefined,
|
|
): Headers {
|
|
const headers = new Headers(input instanceof Request ? input.headers : undefined);
|
|
if (init?.headers) {
|
|
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
}
|
|
if (upstream) {
|
|
headers.set(BASE_URL_HEADER, upstream.origin);
|
|
headers.delete("host");
|
|
}
|
|
if (originalPath) {
|
|
headers.set(ORIGINAL_PATH_HEADER, originalPath);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
function withRoutedFetchInput(input: RequestInfo | URL, init: RequestInit | undefined, proxy: URL): FetchArgs {
|
|
const upstream = requestUrl(input);
|
|
if (!shouldRoute(upstream, proxy)) {
|
|
return [input, init];
|
|
}
|
|
|
|
const { url: nextUrl, originalPath } = routedUrlForOpenCode(upstream, proxy);
|
|
const nextInit = {
|
|
...init,
|
|
headers: mergeFetchHeaders(input, init, upstream, originalPath),
|
|
};
|
|
|
|
if (input instanceof Request) {
|
|
return [new Request(nextUrl, input), nextInit];
|
|
}
|
|
return [nextUrl, nextInit];
|
|
}
|
|
|
|
function splitNodeArgs(args: unknown[]): NodeRequestParts {
|
|
const callback = typeof args.at(-1) === "function" ? (args.at(-1) as (...args: unknown[]) => unknown) : undefined;
|
|
const withoutCallback = callback ? args.slice(0, -1) : args;
|
|
const [first, second] = withoutCallback;
|
|
const options = typeof second === "object" && second !== null ? { ...(second as Record<string, unknown>) } : {};
|
|
|
|
if (first instanceof URL) {
|
|
return { url: first, options, callback };
|
|
}
|
|
if (typeof first === "string") {
|
|
try {
|
|
return { url: new URL(first), options, callback };
|
|
} catch {
|
|
return { options, callback };
|
|
}
|
|
}
|
|
if (typeof first === "object" && first !== null) {
|
|
const requestOptions = { ...(first as Record<string, unknown>), ...options };
|
|
return { url: urlFromRequestOptions(requestOptions), options: requestOptions, callback };
|
|
}
|
|
return { options, callback };
|
|
}
|
|
|
|
function urlFromRequestOptions(options: Record<string, unknown>): URL | undefined {
|
|
const protocol = String(options.protocol ?? "http:");
|
|
if (protocol !== "http:" && protocol !== "https:") {
|
|
return undefined;
|
|
}
|
|
|
|
const hostValue = options.hostname ?? options.host;
|
|
if (!hostValue) {
|
|
return undefined;
|
|
}
|
|
|
|
const hostname = String(hostValue).replace(/:\d+$/, "");
|
|
const port = options.port ? `:${String(options.port)}` : "";
|
|
const path = String(options.path ?? "/");
|
|
try {
|
|
return new URL(`${protocol}//${hostname}${port}${path}`);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function headersForNodeRequest(
|
|
options: Record<string, unknown>,
|
|
upstream: URL,
|
|
originalPath: string | undefined,
|
|
): Record<string, string> {
|
|
const headers = new Headers(options.headers as HeadersInit | undefined);
|
|
headers.set(BASE_URL_HEADER, upstream.origin);
|
|
if (originalPath) {
|
|
headers.set(ORIGINAL_PATH_HEADER, originalPath);
|
|
}
|
|
headers.delete("host");
|
|
|
|
const result: Record<string, string> = {};
|
|
headers.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function routedNodeOptions(parts: NodeRequestParts, proxy: URL): Record<string, unknown> | undefined {
|
|
if (!parts.url || !shouldRoute(parts.url, proxy)) {
|
|
return undefined;
|
|
}
|
|
|
|
const { url: nextUrl, originalPath } = routedUrlForOpenCode(parts.url, proxy);
|
|
const {
|
|
agent: _agent,
|
|
auth: _auth,
|
|
createConnection: _createConnection,
|
|
defaultPort: _defaultPort,
|
|
family: _family,
|
|
headers: _headers,
|
|
host: _host,
|
|
hostname: _hostname,
|
|
href: _href,
|
|
lookup: _lookup,
|
|
path: _path,
|
|
pathname: _pathname,
|
|
port: _port,
|
|
protocol: _protocol,
|
|
search: _search,
|
|
servername: _servername,
|
|
setHost: _setHost,
|
|
...rest
|
|
} = parts.options;
|
|
|
|
return {
|
|
...rest,
|
|
protocol: nextUrl.protocol,
|
|
hostname: nextUrl.hostname,
|
|
port: nextUrl.port || undefined,
|
|
path: `${nextUrl.pathname}${nextUrl.search}`,
|
|
headers: headersForNodeRequest(parts.options, parts.url, originalPath),
|
|
};
|
|
}
|
|
|
|
function wrapRequest(
|
|
originalHttpRequest: HttpRequest,
|
|
originalHttpsRequest: HttpsRequest,
|
|
originalRequest: HttpRequest | HttpsRequest,
|
|
): HttpRequest | HttpsRequest {
|
|
return function headroomRequest(this: unknown, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (!state) {
|
|
return Reflect.apply(originalRequest, this, args);
|
|
}
|
|
|
|
const proxy = normalizeProxyUrl(state.proxyUrl);
|
|
const parts = splitNodeArgs(args);
|
|
const nextOptions = routedNodeOptions(parts, proxy);
|
|
if (!nextOptions) {
|
|
return Reflect.apply(originalRequest, this, args);
|
|
}
|
|
|
|
const targetRequest = proxy.protocol === "https:" ? originalHttpsRequest : originalHttpRequest;
|
|
const nextArgs = parts.callback ? [nextOptions, parts.callback] : [nextOptions];
|
|
return Reflect.apply(targetRequest, this, nextArgs);
|
|
} as HttpRequest | HttpsRequest;
|
|
}
|
|
|
|
function wrapGet(request: HttpRequest | HttpsRequest): HttpGet | HttpsGet {
|
|
return function headroomGet(this: unknown, ...args: unknown[]) {
|
|
const req = Reflect.apply(request, this, args);
|
|
req.end();
|
|
return req;
|
|
} as HttpGet | HttpsGet;
|
|
}
|
|
|
|
function wrapHttp2Connect(originalConnect: Http2Connect): Http2Connect {
|
|
return function headroomHttp2Connect(this: unknown, authority: string | URL, ...args: unknown[]) {
|
|
const state = getState();
|
|
if (state) {
|
|
const proxy = normalizeProxyUrl(state.proxyUrl);
|
|
const upstream = authority instanceof URL ? authority : new URL(String(authority));
|
|
if (shouldRoute(upstream, proxy)) {
|
|
throw new Error(
|
|
`Headroom OpenCode wrap blocked direct HTTP/2 connection to ${upstream.origin}. ` +
|
|
"Use fetch, http, or https so traffic can be routed through Headroom.",
|
|
);
|
|
}
|
|
}
|
|
return Reflect.apply(originalConnect, this, [authority, ...args]);
|
|
} as Http2Connect;
|
|
}
|
|
|
|
export function installHeadroomTransport(options: InstallOptions): () => void {
|
|
const existing = getState();
|
|
if (existing) {
|
|
existing.refs += 1;
|
|
existing.proxyUrl = options.proxyUrl;
|
|
existing.debug = Boolean(options.debug);
|
|
installProcessEnv(options.proxyUrl);
|
|
return () => uninstallHeadroomTransport();
|
|
}
|
|
|
|
const state: TransportState = {
|
|
refs: 1,
|
|
proxyUrl: options.proxyUrl,
|
|
debug: Boolean(options.debug),
|
|
originalFetch: globalThis.fetch,
|
|
originalHttpRequest: http.request,
|
|
originalHttpGet: http.get,
|
|
originalHttpsRequest: https.request,
|
|
originalHttpsGet: https.get,
|
|
originalHttp2Connect: http2.connect,
|
|
originalChildSpawn: childProcess.spawn,
|
|
originalChildExec: childProcess.exec,
|
|
originalChildExecFile: childProcess.execFile,
|
|
originalChildFork: childProcess.fork,
|
|
};
|
|
|
|
setState(state);
|
|
installProcessEnv(options.proxyUrl);
|
|
globalThis.fetch = async (...args: FetchArgs) => {
|
|
const current = getState();
|
|
if (!current) {
|
|
return state.originalFetch(...args);
|
|
}
|
|
const proxy = normalizeProxyUrl(current.proxyUrl);
|
|
const [nextInput, nextInit] = withRoutedFetchInput(args[0], args[1], proxy);
|
|
return state.originalFetch(nextInput, nextInit);
|
|
};
|
|
|
|
http.request = wrapRequest(state.originalHttpRequest, state.originalHttpsRequest, state.originalHttpRequest) as HttpRequest;
|
|
https.request = wrapRequest(state.originalHttpRequest, state.originalHttpsRequest, state.originalHttpsRequest) as HttpsRequest;
|
|
http.get = wrapGet(http.request) as HttpGet;
|
|
https.get = wrapGet(https.request) as HttpsGet;
|
|
http2.connect = wrapHttp2Connect(state.originalHttp2Connect);
|
|
childProcess.spawn = wrapSpawn(state.originalChildSpawn);
|
|
childProcess.exec = wrapExec(state.originalChildExec);
|
|
childProcess.execFile = wrapExecFile(state.originalChildExecFile);
|
|
childProcess.fork = wrapFork(state.originalChildFork);
|
|
syncBuiltinESMExports();
|
|
|
|
return () => uninstallHeadroomTransport();
|
|
}
|
|
|
|
export function uninstallHeadroomTransport(): void {
|
|
const state = getState();
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
state.refs -= 1;
|
|
if (state.refs > 0) {
|
|
return;
|
|
}
|
|
|
|
globalThis.fetch = state.originalFetch;
|
|
http.request = state.originalHttpRequest;
|
|
http.get = state.originalHttpGet;
|
|
https.request = state.originalHttpsRequest;
|
|
https.get = state.originalHttpsGet;
|
|
http2.connect = state.originalHttp2Connect;
|
|
childProcess.spawn = state.originalChildSpawn;
|
|
childProcess.exec = state.originalChildExec;
|
|
childProcess.execFile = state.originalChildExecFile;
|
|
childProcess.fork = state.originalChildFork;
|
|
syncBuiltinESMExports();
|
|
setState(undefined);
|
|
}
|