Discord Gateway Proxy Support (OpenClaw)
Discord Gateway Proxy Support — Implementation Overview
This document explains how the Discord gateway proxy support works in OpenClaw, starting from zero context.
1. What Problem We Solve
In some networks, outbound internet access is blocked unless traffic goes through a local proxy (for example,
http://127.0.0.1:5780). The Discord gateway uses a WebSocket connection to wss://gateway.discord.gg/, so if
direct outbound TCP is blocked, the connection fails with timeouts.
The goal is to let OpenClaw connect the Discord gateway through a user-specified HTTP(S) proxy.
2. Where the Configuration Lives
OpenClaw stores channel configuration under channels.<channelName> in the main config.
We added a new optional field:
channels.discord.proxy: a string containing the proxy URL
This can also be set per Discord account:
channels.discord.accounts.<accountId>.proxy
3. Config Type Definitions
The Discord config types live in src/config/types.discord.ts. We added:
proxy?: string;inDiscordAccountConfig
This makes the proxy field a first-class config setting for Discord accounts.
4. Config Validation (Zod Schema)
All config is validated at load time through Zod schemas in src/config/zod-schema.providers-core.ts. We added:
proxy: z.string().optional()
This allows the new field to pass validation when present.
5. Config Schema Labels / Descriptions
OpenClaw also maintains a schema registry in src/config/schema.ts used for CLI help and docs. We added:
channels.discord.proxy: labeled as a Discord gateway proxy URL
This makes the field discoverable in CLI output and docs tooling.
6. How the Discord Gateway Connection Is Created
Discord connections are handled by the Carbon client and its GatewayPlugin in src/discord/monitor/provider.ts.
For background on Carbon’s gateway plugin, see the official docs: https://carbon.buape.com/plugins.
Originally, the client was created like this:
- A new
GatewayPlugininstance was created directly. - The plugin internally creates a WebSocket with
new WebSocket(url). - There was no way to inject a proxy.
We changed this by inserting a custom GatewayPlugin that overrides its WebSocket creation.
7. Proxy-Aware GatewayPlugin
We added a helper function in src/discord/monitor/provider.ts:
createDiscordGatewayPlugin({ discordConfig, runtime })
This does the following:
- Reads
discordConfig.proxy. - If no proxy is set, it returns a normal
GatewayPluginwith the usual options. - If a proxy is set:
- It constructs an
HttpsProxyAgentwith that proxy URL. - It defines a small subclass of
GatewayPluginthat overridescreateWebSocket. - The overridden method uses:
new WebSocket(url, { agent: proxyAgent })
- It constructs an
This causes all gateway WebSocket traffic to be tunneled through the proxy.
8. Dependency Used
We added the dependency https-proxy-agent to the main package.json.
This library provides the HttpsProxyAgent class, which is compatible with the ws WebSocket client used by Carbon.
9. End-to-End Flow Summary
- User sets
channels.discord.proxyin config. - Config loads and validates successfully.
- Discord monitor (
src/discord/monitor/provider.ts) resolves the Discord account config. - It calls
createDiscordGatewayPlugin(...). - The gateway plugin is created with a proxy-aware WebSocket.
- Discord gateway connects via the proxy instead of direct outbound TCP.
9.1 Visual Flow Diagram
flowchart TB cfg["Config<br/>file<br/>channels.discord.proxy"] --> validate["Config<br/>load<br/>Zod<br/>validation"] validate --> monitor["Discord<br/>monitor<br/>provider.ts"] monitor --> decision{Proxy<br/>set?} decision -- No --> gwDefault["GatewayPlugin<br/>new<br/>WebSocket(url)"] decision -- Yes --> gwCustom["Proxy<br/>GatewayPlugin<br/>createWebSocket<br/>override"] gwCustom --> agent["HttpsProxyAgent<br/>proxyUrl"] agent --> wsProxy["WebSocket<br/>agent=proxy"] gwDefault --> gateway["Discord<br/>Gateway<br/>wss://gateway.discord.gg"] wsProxy --> gateway
10. Example Config
channels:
discord:
token: "<your token>"
proxy: "http://127.0.0.1:5780"
Per-account override:
channels:
discord:
accounts:
primary:
token: "<your token>"
proxy: "http://127.0.0.1:5780"
Conclusion
The core idea is simple: Discord’s gateway is a WebSocket, not a REST call, so the proxy
must be applied at the WebSocket layer. We wrap Carbon’s GatewayPlugin with a small subclass
that overrides createWebSocket to attach an HttpsProxyAgent. This keeps the rest of the
Discord integration untouched, while ensuring gateway traffic is tunneled through your proxy
whenever channels.discord.proxy is set.