Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
60 changes: 60 additions & 0 deletions nemoclaw-blueprint/scripts/http-proxy-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,64 @@
}
return origRequest.apply(http, arguments);
};

// ── fetch() custom-dispatcher fix ────────────────────────────────
// Problem:
// OpenClaw's Microsoft Teams adapter calls the Graph API via native
// fetch() with a custom undici dispatcher (`opts.dispatcher`). The
// custom dispatcher routes the request through its own connection
// pool — bypassing Node's default EnvHttpProxyAgent, which is the
// only thing routing HTTPS traffic through the OpenShell L7 proxy.
// Direct egress to e.g. graph.microsoft.com is blocked by the
// sandbox network namespace and surfaces as ECONNREFUSED. The bot
// then silently drops channel @mentions (DMs work because the
// webhook payload carries the message body and no Graph call is
// needed).
//
// Fix:
// When fetch() is called with a custom dispatcher and an HTTPS URL,
// strip the dispatcher and let the default EnvHttpProxyAgent handle
// the request through the proxy. Non-HTTPS URLs are left untouched
// — direct HTTP traffic inside the sandbox netns still works, and
// any non-proxy use of the dispatcher option remains honored. A
// one-shot console.warn makes the override discoverable without
// spamming on every call (the Teams adapter can call this many
// times per minute under load).
//
// fetch() input forms (handled in this order):
// - string — typeof === 'string'
// - URL object — has a string .href
// - Request object — has a string .url
// - anything else — passed through to origFetch unchanged
var origFetch = globalThis.fetch;
if (typeof origFetch === 'function') {
var dispatcherStripWarned = false;
globalThis.fetch = function (url, opts) {
if (opts && opts.dispatcher) {
var urlStr = '';
if (typeof url === 'string') {
urlStr = url;
} else if (url && typeof url.href === 'string') {
urlStr = url.href;
} else if (url && typeof url.url === 'string') {
urlStr = url.url;
}
if (urlStr.startsWith('https://')) {
if (!dispatcherStripWarned) {
dispatcherStripWarned = true;
console.warn(
'[nemoclaw http-proxy-fix] stripping custom fetch() dispatcher for HTTPS URL ' +
urlStr +
' so EnvHttpProxyAgent can route through the L7 proxy. ' +
'Subsequent strips suppressed.',
);
}
var newOpts = Object.assign({}, opts);
delete newOpts.dispatcher;
return origFetch.call(this, url, newOpts);
}
}
return origFetch.apply(this, arguments);
};
}
})();
60 changes: 60 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,66 @@ if [ "${NODE_USE_ENV_PROXY:-}" = "1" ]; then
}
return origRequest.apply(http, arguments);
};

// ── fetch() custom-dispatcher fix ────────────────────────────────
// Problem:
// OpenClaw's Microsoft Teams adapter calls the Graph API via native
// fetch() with a custom undici dispatcher (`opts.dispatcher`). The
// custom dispatcher routes the request through its own connection
// pool — bypassing Node's default EnvHttpProxyAgent, which is the
// only thing routing HTTPS traffic through the OpenShell L7 proxy.
// Direct egress to e.g. graph.microsoft.com is blocked by the
// sandbox network namespace and surfaces as ECONNREFUSED. The bot
// then silently drops channel @mentions (DMs work because the
// webhook payload carries the message body and no Graph call is
// needed).
//
// Fix:
// When fetch() is called with a custom dispatcher and an HTTPS URL,
// strip the dispatcher and let the default EnvHttpProxyAgent handle
// the request through the proxy. Non-HTTPS URLs are left untouched
// — direct HTTP traffic inside the sandbox netns still works, and
// any non-proxy use of the dispatcher option remains honored. A
// one-shot console.warn makes the override discoverable without
// spamming on every call (the Teams adapter can call this many
// times per minute under load).
//
// fetch() input forms (handled in this order):
// - string — typeof === 'string'
// - URL object — has a string .href
// - Request object — has a string .url
// - anything else — passed through to origFetch unchanged
var origFetch = globalThis.fetch;
if (typeof origFetch === 'function') {
var dispatcherStripWarned = false;
globalThis.fetch = function (url, opts) {
if (opts && opts.dispatcher) {
var urlStr = '';
if (typeof url === 'string') {
urlStr = url;
} else if (url && typeof url.href === 'string') {
urlStr = url.href;
} else if (url && typeof url.url === 'string') {
urlStr = url.url;
}
if (urlStr.startsWith('https://')) {
if (!dispatcherStripWarned) {
dispatcherStripWarned = true;
console.warn(
'[nemoclaw http-proxy-fix] stripping custom fetch() dispatcher for HTTPS URL ' +
urlStr +
' so EnvHttpProxyAgent can route through the L7 proxy. ' +
'Subsequent strips suppressed.',
);
}
var newOpts = Object.assign({}, opts);
delete newOpts.dispatcher;
return origFetch.call(this, url, newOpts);
}
}
return origFetch.apply(this, arguments);
};
}
})();
HTTP_PROXY_FIX_EOF
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require $_PROXY_FIX_SCRIPT"
Expand Down
6 changes: 6 additions & 0 deletions test/http-proxy-fix-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ describe("http-proxy-fix heredoc sync (#2109)", () => {
expect(content.length).toBeGreaterThan(0);
expect(content).toContain("(function () {");
expect(content).toContain("http.request = function");
// fetch() custom-dispatcher fix (the Teams Graph API path — see PR
// body). Asserted explicitly because the byte-equality test below
// only proves the two copies match — it cannot detect "both copies
// lost the wrapper".
expect(content).toContain("globalThis.fetch = function");
expect(content).toContain("delete newOpts.dispatcher");
});

it("nemoclaw-start.sh embeds the fix via a HTTP_PROXY_FIX_EOF heredoc", () => {
Expand Down
Loading