From b4034b32c365c69db0d5ad7ff649bc9920842f40 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 8 Apr 2026 05:36:57 +0100 Subject: [PATCH] fix: honor Slack Socket Mode env proxies (#62878) (thanks @mjamiv) --- CHANGELOG.md | 4 +++ extensions/slack/src/client.test.ts | 29 +++++++++++++++++++-- extensions/slack/src/client.ts | 40 +++++++++++------------------ pnpm-lock.yaml | 3 +++ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632105a784a..84796d64f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Docs: https://docs.openclaw.ai ## Unreleased +### Fixes + +- Slack: honor ambient HTTP(S) proxy settings for Socket Mode WebSocket connections, including NO_PROXY exclusions, so proxy-only deployments can connect without a monkey patch. (#62878) Thanks @mjamiv. + ## 2026.4.7-1 ### Fixes diff --git a/extensions/slack/src/client.test.ts b/extensions/slack/src/client.test.ts index 3941254d30f..5d070d74856 100644 --- a/extensions/slack/src/client.test.ts +++ b/extensions/slack/src/client.test.ts @@ -85,7 +85,14 @@ describe("slack web client config", () => { describe("slack proxy agent", () => { const originalEnv = { ...process.env }; - const PROXY_KEYS = ["HTTPS_PROXY", "HTTP_PROXY", "https_proxy", "http_proxy", "NO_PROXY", "no_proxy"]; + const PROXY_KEYS = [ + "HTTPS_PROXY", + "HTTP_PROXY", + "https_proxy", + "http_proxy", + "NO_PROXY", + "no_proxy", + ]; beforeEach(() => { for (const key of PROXY_KEYS) { @@ -139,7 +146,17 @@ describe("slack proxy agent", () => { expect(options.agent).toBeDefined(); // HttpsProxyAgent stores the proxy URL — verify it picked the lower-case one - expect((options.agent as { proxy: { href: string } }).proxy.href).toContain("lower.example.com"); + expect((options.agent as unknown as { proxy: { href: string } }).proxy.href).toContain( + "lower.example.com", + ); + }); + + it("treats empty lowercase https_proxy as authoritative over uppercase", () => { + process.env.https_proxy = ""; + process.env.HTTPS_PROXY = "http://upper.example.com:3128"; + const options = resolveSlackWebClientOptions(); + + expect(options.agent).toBeUndefined(); }); it("also applies proxy agent to write client options", () => { @@ -166,6 +183,14 @@ describe("slack proxy agent", () => { expect(options.agent).toBeUndefined(); }); + it("respects space-separated no_proxy entries", () => { + process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; + process.env.no_proxy = "localhost *.slack.com"; + const options = resolveSlackWebClientOptions(); + + expect(options.agent).toBeUndefined(); + }); + it("respects NO_PROXY wildcard", () => { process.env.HTTPS_PROXY = "http://proxy.example.com:3128"; process.env.NO_PROXY = "*"; diff --git a/extensions/slack/src/client.ts b/extensions/slack/src/client.ts index 491fad83823..f84c6a5e94e 100644 --- a/extensions/slack/src/client.ts +++ b/extensions/slack/src/client.ts @@ -1,5 +1,6 @@ import { type RetryOptions, type WebClientOptions, WebClient } from "@slack/web-api"; import { HttpsProxyAgent } from "https-proxy-agent"; +import { resolveEnvHttpProxyUrl } from "openclaw/plugin-sdk/infra-runtime"; export const SLACK_DEFAULT_RETRY_OPTIONS: RetryOptions = { retries: 2, @@ -17,23 +18,27 @@ export const SLACK_WRITE_RETRY_OPTIONS: RetryOptions = { * Check whether a hostname is excluded from proxying by `NO_PROXY` / `no_proxy`. * Supports comma-separated entries with optional leading dots (e.g. `.slack.com`). */ -function isHostExcludedByNoProxy( - hostname: string, - env: NodeJS.ProcessEnv = process.env, -): boolean { +function isHostExcludedByNoProxy(hostname: string, env: NodeJS.ProcessEnv = process.env): boolean { const raw = env.no_proxy ?? env.NO_PROXY; if (!raw) { return false; } - const entries = raw.split(",").map((e) => e.trim().toLowerCase()).filter(Boolean); + const entries = raw + .split(/[,\s]+/) + .map((e) => e.trim().toLowerCase()) + .filter(Boolean); const lower = hostname.toLowerCase(); for (const entry of entries) { if (entry === "*") { return true; } - // Strip optional leading dot for comparison so `.slack.com` matches both - // `slack.com` (apex) and `wss-primary.slack.com` (subdomain). - const bare = entry.startsWith(".") ? entry.slice(1) : entry; + // Strip optional wildcard/leading dot so `*.slack.com` and `.slack.com` + // match both `slack.com` (apex) and Slack subdomains. + const bare = entry.startsWith("*.") + ? entry.slice(2) + : entry.startsWith(".") + ? entry.slice(1) + : entry; if (lower === bare || lower.endsWith(`.${bare}`)) { return true; } @@ -41,21 +46,6 @@ function isHostExcludedByNoProxy( return false; } -/** - * Resolve the proxy URL from env vars following undici EnvHttpProxyAgent - * semantics: lower-case takes precedence, HTTPS prefers https_proxy then - * falls back to http_proxy. Returns `undefined` when no proxy is configured. - */ -function resolveProxyUrlFromEnv(env: NodeJS.ProcessEnv = process.env): string | undefined { - return ( - env.https_proxy?.trim() || - env.HTTPS_PROXY?.trim() || - env.http_proxy?.trim() || - env.HTTP_PROXY?.trim() || - undefined - ); -} - /** * Build an HTTPS proxy agent from env vars (HTTPS_PROXY, HTTP_PROXY, etc.) * for use as the `agent` option in Slack WebClient and Socket Mode connections. @@ -72,7 +62,7 @@ function resolveProxyUrlFromEnv(env: NodeJS.ProcessEnv = process.env): string | * are excluded by `NO_PROXY`. */ function resolveSlackProxyAgent(): HttpsProxyAgent | undefined { - const proxyUrl = resolveProxyUrlFromEnv(); + const proxyUrl = resolveEnvHttpProxyUrl("https"); if (!proxyUrl) { return undefined; } @@ -81,7 +71,7 @@ function resolveSlackProxyAgent(): HttpsProxyAgent | undefined { return undefined; } try { - return new HttpsProxyAgent(proxyUrl); + return new HttpsProxyAgent(proxyUrl); } catch { // Malformed proxy URL — degrade gracefully to direct connection. return undefined; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ac1e6c4db99..81dfb23a088 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -941,6 +941,9 @@ importers: '@slack/web-api': specifier: ^7.15.0 version: 7.15.0 + https-proxy-agent: + specifier: ^9.0.0 + version: 9.0.0 devDependencies: '@openclaw/plugin-sdk': specifier: workspace:*