diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a75079c2..034eac5f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai - Slack/app manifest: add the missing `groups:read` scope to the onboarding and example Slack app manifest so apps copied from the OpenClaw templates can resolve private group conversations reliably. - Mobile pairing/Android: stop generating Tailscale and public mobile setup codes that point at unusable cleartext remote gateways, keep private LAN pairing allowed, and make Android reject insecure remote endpoints with clearer guidance while mixed bootstrap approvals honor operator scopes correctly. (#60128) Thanks @obviyus. - Telegram/media: add `channels.telegram.network.dangerouslyAllowPrivateNetwork` for trusted fake-IP or transparent-proxy environments where Telegram media downloads resolve `api.telegram.org` to private/internal/special-use addresses. +- Discord/proxy: keep Carbon REST, monitor startup, and webhook sends on the configured Discord proxy while falling back cleanly when the proxy URL is invalid, so Discord replies and deploys do not hard-fail on malformed proxy config. (#57465) Thanks @geekhuashan. ## 2026.4.2 diff --git a/extensions/discord/src/client.proxy.test.ts b/extensions/discord/src/client.proxy.test.ts index 7695498682..1f752ab1f8 100644 --- a/extensions/discord/src/client.proxy.test.ts +++ b/extensions/discord/src/client.proxy.test.ts @@ -1,7 +1,23 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../src/config/config.js"; import { createDiscordRestClient } from "./client.js"; +const makeProxyFetchMock = vi.hoisted(() => vi.fn()); + +vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { + const actual = await importOriginal(); + makeProxyFetchMock.mockImplementation((proxyUrl: string) => { + if (proxyUrl === "bad-proxy") { + throw new Error("bad proxy"); + } + return actual.makeProxyFetch(proxyUrl); + }); + return { + ...actual, + makeProxyFetch: makeProxyFetchMock, + }; +}); + describe("createDiscordRestClient proxy support", () => { it("injects a custom fetch into RequestClient when a Discord proxy is configured", () => { const cfg = { @@ -39,4 +55,23 @@ describe("createDiscordRestClient proxy support", () => { expect(requestClient.options?.fetch).toBeUndefined(); }); + + it("falls back to direct fetch when the Discord proxy URL is invalid", () => { + const cfg = { + channels: { + discord: { + token: "Bot test-token", + proxy: "bad-proxy", + }, + }, + } as OpenClawConfig; + + const { rest } = createDiscordRestClient({}, cfg); + const requestClient = rest as unknown as { + options?: { fetch?: typeof fetch }; + }; + + expect(makeProxyFetchMock).toHaveBeenCalledWith("bad-proxy"); + expect(requestClient.options?.fetch).toBeUndefined(); + }); }); diff --git a/extensions/discord/src/client.ts b/extensions/discord/src/client.ts index 958fd4e4f2..cb22f985a9 100644 --- a/extensions/discord/src/client.ts +++ b/extensions/discord/src/client.ts @@ -3,6 +3,7 @@ import { loadConfig } from "openclaw/plugin-sdk/config-runtime"; import { makeProxyFetch } from "openclaw/plugin-sdk/infra-runtime"; import type { RetryConfig, RetryRunner } from "openclaw/plugin-sdk/retry-runtime"; import { normalizeAccountId } from "openclaw/plugin-sdk/routing"; +import { danger, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { mergeDiscordAccountConfig, resolveDiscordAccount, @@ -46,24 +47,41 @@ function resolveDiscordProxyUrl( return trimmed || undefined; } +function resolveDiscordProxyFetchByUrl( + proxyUrl: string | undefined, + runtime?: Pick, +): typeof fetch | undefined { + const proxy = proxyUrl?.trim(); + if (!proxy) { + return undefined; + } + try { + return makeProxyFetch(proxy); + } catch (err) { + runtime?.error?.(danger(`discord: invalid rest proxy: ${String(err)}`)); + return undefined; + } +} + export function resolveDiscordProxyFetchForAccount( account: Pick, cfg?: ReturnType, + runtime?: Pick, ): typeof fetch | undefined { - const proxy = resolveDiscordProxyUrl(account, cfg); - return proxy ? makeProxyFetch(proxy) : undefined; + return resolveDiscordProxyFetchByUrl(resolveDiscordProxyUrl(account, cfg), runtime); } export function resolveDiscordProxyFetch( opts: Pick, cfg?: ReturnType, + runtime?: Pick, ): typeof fetch | undefined { const resolvedCfg = opts.cfg ?? cfg ?? loadConfig(); const account = resolveAccountWithoutToken({ cfg: resolvedCfg, accountId: opts.accountId, }); - return resolveDiscordProxyFetchForAccount(account, resolvedCfg); + return resolveDiscordProxyFetchForAccount(account, resolvedCfg, runtime); } function resolveRest( diff --git a/extensions/discord/src/monitor/provider.ts b/extensions/discord/src/monitor/provider.ts index 0ece4854b8..ba7b1e4965 100644 --- a/extensions/discord/src/monitor/provider.ts +++ b/extensions/discord/src/monitor/provider.ts @@ -593,7 +593,7 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) { const discordAccountThreadBindings = cfg.channels?.discord?.accounts?.[account.accountId]?.threadBindings; const discordRestFetch = resolveDiscordRestFetch(rawDiscordCfg.proxy, runtime); - const discordProxyFetch = resolveDiscordProxyFetchForAccount(account, cfg); + const discordProxyFetch = resolveDiscordProxyFetchForAccount(account, cfg, runtime); const dmConfig = rawDiscordCfg.dm; let guildEntries = rawDiscordCfg.guilds; const defaultGroupPolicy = resolveDefaultGroupPolicy(cfg); diff --git a/extensions/discord/src/send.webhook.proxy.test.ts b/extensions/discord/src/send.webhook.proxy.test.ts index acf328a6e4..bda04da079 100644 --- a/extensions/discord/src/send.webhook.proxy.test.ts +++ b/extensions/discord/src/send.webhook.proxy.test.ts @@ -13,6 +13,36 @@ vi.mock("openclaw/plugin-sdk/infra-runtime", async (importOriginal) => { }); describe("sendWebhookMessageDiscord proxy support", () => { + it("falls back to global fetch when the Discord proxy URL is invalid", async () => { + makeProxyFetchMock.mockImplementation(() => { + throw new Error("bad proxy"); + }); + const globalFetchMock = vi + .spyOn(globalThis, "fetch") + .mockResolvedValue(new Response(JSON.stringify({ id: "msg-0" }), { status: 200 })); + + const cfg = { + channels: { + discord: { + token: "Bot test-token", + proxy: "bad-proxy", + }, + }, + } as OpenClawConfig; + + await sendWebhookMessageDiscord("hello", { + cfg, + accountId: "default", + webhookId: "123", + webhookToken: "abc", + wait: true, + }); + + expect(makeProxyFetchMock).toHaveBeenCalledWith("bad-proxy"); + expect(globalFetchMock).toHaveBeenCalledOnce(); + globalFetchMock.mockRestore(); + }); + it("uses proxy fetch when a Discord proxy is configured", async () => { const proxiedFetch = vi .fn()