fix: keep Discord proxy fallback local (#57465) (thanks @geekhuashan)

This commit is contained in:
Peter Steinberger 2026-04-03 18:34:27 +09:00
parent 3a4fd62135
commit 86ff57518f
5 changed files with 89 additions and 5 deletions

View File

@ -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

View File

@ -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<typeof import("openclaw/plugin-sdk/infra-runtime")>();
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();
});
});

View File

@ -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<RuntimeEnv, "error">,
): 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<ResolvedDiscordAccount, "config">,
cfg?: ReturnType<typeof loadConfig>,
runtime?: Pick<RuntimeEnv, "error">,
): typeof fetch | undefined {
const proxy = resolveDiscordProxyUrl(account, cfg);
return proxy ? makeProxyFetch(proxy) : undefined;
return resolveDiscordProxyFetchByUrl(resolveDiscordProxyUrl(account, cfg), runtime);
}
export function resolveDiscordProxyFetch(
opts: Pick<DiscordClientOpts, "cfg" | "accountId">,
cfg?: ReturnType<typeof loadConfig>,
runtime?: Pick<RuntimeEnv, "error">,
): 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(

View File

@ -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);

View File

@ -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()