fix(gateway/discord): respect env proxy vars and prevent ECONNRESET on proxy tunnels
Gateway startup now bootstraps the global undici EnvHttpProxyAgent so Node's fetch() honors https_proxy/HTTP_PROXY. The Discord gateway plugin also falls back to env proxy vars for its WebSocket connection when no explicit channels.discord.proxy is configured. All proxy ProxyAgent instances disable keepAlive to avoid ECONNRESET from local proxies (Clash, Surge, etc.) that close CONNECT tunnels after one request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b31b681088
commit
dfce4052dc
@ -6,6 +6,7 @@ import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { ProxyAgent, fetch as undiciFetch } from "undici";
|
||||
import WebSocket from "ws";
|
||||
import { resolveEnvHttpProxyUrl } from "../../../../src/infra/net/proxy-env.js";
|
||||
|
||||
const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot";
|
||||
const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/";
|
||||
@ -270,7 +271,7 @@ export function createDiscordGatewayPlugin(params: {
|
||||
runtime: RuntimeEnv;
|
||||
}): GatewayPlugin {
|
||||
const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents);
|
||||
const proxy = params.discordConfig?.proxy?.trim();
|
||||
const proxy = params.discordConfig?.proxy?.trim() || resolveEnvHttpProxyUrl("https");
|
||||
const options = {
|
||||
reconnect: { maxAttempts: 50 },
|
||||
intents,
|
||||
@ -287,7 +288,7 @@ export function createDiscordGatewayPlugin(params: {
|
||||
|
||||
try {
|
||||
const wsAgent = new HttpsProxyAgent<string>(proxy);
|
||||
const fetchAgent = new ProxyAgent(proxy);
|
||||
const fetchAgent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } });
|
||||
|
||||
params.runtime.log?.("discord: gateway proxy enabled");
|
||||
|
||||
|
||||
@ -89,7 +89,8 @@ vi.mock("https-proxy-agent", () => ({
|
||||
vi.mock("undici", () => ({
|
||||
ProxyAgent: class {
|
||||
proxyUrl: string;
|
||||
constructor(proxyUrl: string) {
|
||||
constructor(opts: string | { uri: string }) {
|
||||
const proxyUrl = typeof opts === "string" ? opts : opts.uri;
|
||||
this.proxyUrl = proxyUrl;
|
||||
undiciProxyAgentSpy(proxyUrl);
|
||||
restProxyAgentSpy(proxyUrl);
|
||||
@ -98,6 +99,10 @@ vi.mock("undici", () => ({
|
||||
fetch: undiciFetchMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../../../src/infra/net/proxy-env.js", () => ({
|
||||
resolveEnvHttpProxyUrl: () => undefined,
|
||||
}));
|
||||
|
||||
vi.mock("ws", () => ({
|
||||
default: class MockWebSocket {
|
||||
constructor(url: string, options?: { agent?: unknown }) {
|
||||
|
||||
@ -9,7 +9,8 @@ const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({
|
||||
vi.mock("undici", () => {
|
||||
class ProxyAgent {
|
||||
proxyUrl: string;
|
||||
constructor(proxyUrl: string) {
|
||||
constructor(opts: string | { uri: string }) {
|
||||
const proxyUrl = typeof opts === "string" ? opts : opts.uri;
|
||||
if (proxyUrl === "bad-proxy") {
|
||||
throw new Error("bad proxy");
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export function resolveDiscordRestFetch(
|
||||
return fetch;
|
||||
}
|
||||
try {
|
||||
const agent = new ProxyAgent(proxy);
|
||||
const agent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } });
|
||||
const fetcher = ((input: RequestInfo | URL, init?: RequestInit) =>
|
||||
undiciFetch(input as string | URL, {
|
||||
...(init as Record<string, unknown>),
|
||||
|
||||
@ -17,6 +17,10 @@ import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js";
|
||||
import { setVerbose } from "../../globals.js";
|
||||
import { GatewayLockError } from "../../infra/gateway-lock.js";
|
||||
import {
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
ensureGlobalUndiciStreamTimeouts,
|
||||
} from "../../infra/net/undici-global-dispatcher.js";
|
||||
import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js";
|
||||
import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js";
|
||||
import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js";
|
||||
@ -418,6 +422,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Proxy bootstrap must happen before timeout tuning so the timeouts wrap the
|
||||
// active EnvHttpProxyAgent instead of being replaced by a bare proxy dispatcher.
|
||||
ensureGlobalUndiciEnvProxyDispatcher();
|
||||
ensureGlobalUndiciStreamTimeouts();
|
||||
|
||||
try {
|
||||
await runGatewayLoop({
|
||||
runtime: defaultRuntime,
|
||||
|
||||
@ -111,6 +111,7 @@ describe("ensureGlobalUndiciStreamTimeouts", () => {
|
||||
expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS);
|
||||
expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS);
|
||||
expect(next.options?.connect).toEqual({
|
||||
keepAlive: false,
|
||||
autoSelectFamily: false,
|
||||
autoSelectFamilyAttemptTimeout: 300,
|
||||
});
|
||||
|
||||
@ -93,7 +93,10 @@ export function ensureGlobalUndiciEnvProxyDispatcher(): void {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent());
|
||||
// Many local proxies (Clash, Surge, etc.) close HTTP CONNECT tunnels after
|
||||
// the first request, causing ECONNRESET on reuse. Disable keep-alive so
|
||||
// undici opens a fresh tunnel per request instead of pooling.
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent({ connect: { keepAlive: false } }));
|
||||
lastAppliedProxyBootstrap = true;
|
||||
} catch {
|
||||
// Best-effort bootstrap only.
|
||||
@ -120,10 +123,11 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }):
|
||||
const connect = resolveConnectOptions(autoSelectFamily);
|
||||
try {
|
||||
if (kind === "env-proxy") {
|
||||
const proxyConnect = { keepAlive: false, ...connect };
|
||||
const proxyOptions = {
|
||||
bodyTimeout: timeoutMs,
|
||||
headersTimeout: timeoutMs,
|
||||
...(connect ? { connect } : {}),
|
||||
connect: proxyConnect,
|
||||
} as ConstructorParameters<typeof EnvHttpProxyAgent>[0];
|
||||
setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions));
|
||||
} else {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user