diff --git a/CHANGELOG.md b/CHANGELOG.md index 53a75079c2f..034eac5f316 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 76954986828..1f752ab1f85 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 958fd4e4f23..cb22f985a95 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 0ece4854b82..ba7b1e49659 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 acf328a6e47..bda04da0794 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()