Files
openclaw/extensions/discord/src/proxy-request-client.ts
T
Alex Knight e4ff7c1620 fix: Discord read/search timeout, session-key fallback, and gateway execution mode (#73521)
* fix: Discord read/search timeout, session-key fallback, and gateway execution mode

- Add 15s timeout to readMessagesDiscord and searchMessagesDiscord so they
  fail fast instead of hanging indefinitely (#73431)
- Fall back to CommandTargetSessionKey in dispatchReplyFromConfig when
  SessionKey is empty, so Discord inbound message:received hooks fire
  reliably (#73431, refs #33038)
- Add resolveExecutionMode to Discord channel actions routing read/search
  through gateway timeout path, matching Telegram's pattern (#73431)

* fix: move timeout to fetch layer, drop send.messages wrapper

Inject AbortSignal.timeout into the Discord proxy-request-client fetch
wrapper so every Discord REST call gets a 15s timeout at the HTTP level.
This replaces the Promise.race wrapper in send.messages.ts — cleaner,
covers all calls, and actually aborts the TCP connection.

* fix: remove unused callerController variable in proxy-request-client test

* fix: remove unnecessary mergeAbortSignal helper
2026-04-28 21:46:05 +10:00

55 lines
1.7 KiB
TypeScript

import { RequestClient, type RequestClientOptions } from "@buape/carbon";
import { FormData as UndiciFormData } from "undici";
export type ProxyRequestClientOptions = RequestClientOptions;
export const DISCORD_REST_TIMEOUT_MS = 15_000;
function toUndiciFormData(body: FormData): UndiciFormData {
const converted = new UndiciFormData();
for (const [key, value] of body.entries()) {
if (typeof value === "string") {
converted.append(key, value);
continue;
}
const filename = (value as Blob & { name?: unknown }).name;
if (typeof filename === "string" && filename.length > 0) {
converted.append(key, value, filename);
continue;
}
converted.append(key, value);
}
return converted;
}
function wrapDiscordFetch(fetchImpl: NonNullable<RequestClientOptions["fetch"]>) {
return (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const signal = AbortSignal.timeout(DISCORD_REST_TIMEOUT_MS);
if (init?.body instanceof FormData) {
// Carbon builds global FormData; undici-backed proxy fetch needs undici's
// FormData class to preserve multipart boundaries.
return fetchImpl(input, {
...init,
signal,
body: toUndiciFormData(init.body) as unknown as BodyInit,
});
}
return fetchImpl(input, { ...init, signal });
};
}
export function createDiscordRequestClient(
token: string,
options?: ProxyRequestClientOptions,
): RequestClient {
if (!options?.fetch) {
return new RequestClient(token, options);
}
return new RequestClient(token, {
runtimeProfile: "persistent",
maxQueueSize: 1000,
...options,
fetch: wrapDiscordFetch(options.fetch),
});
}