From 0088b1f7b356bd8806875e7fd3f86852b086ef09 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Thu, 12 Mar 2026 14:45:03 -0700 Subject: [PATCH] fix: harden Discord gateway metadata wrapping Address PR review feedback by preserving Authorization when fetch init fields are merged into the Discord gateway metadata request and by treating response body read failures as transient wrapped fetch failures. Add a regression test for body stream errors during metadata fetch. Regeneration-Prompt: | Follow up on a Discord gateway startup hardening PR after bot review comments. The code already normalizes plain-text Discord /gateway/bot failures, but two edge cases remained: spreading fetchInit after headers could let a future caller clobber the Bot Authorization header, and response.text() could throw while streaming the body and bypass the wrapper that marks startup failures as transient. Keep the fix narrow to the Discord gateway metadata helper and its tests. Preserve existing behavior for successful fetches and existing proxy support, but make sure both request construction and body-read failures stay inside the same wrapped error path so launchd-managed gateways do not exit on these edge cases. --- src/discord/monitor/gateway-plugin.ts | 23 ++++++++++++++----- src/discord/monitor/provider.proxy.test.ts | 26 ++++++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/discord/monitor/gateway-plugin.ts b/src/discord/monitor/gateway-plugin.ts index 3e6e70944d2..b4030bcb386 100644 --- a/src/discord/monitor/gateway-plugin.ts +++ b/src/discord/monitor/gateway-plugin.ts @@ -11,9 +11,12 @@ const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot"; const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/"; type DiscordGatewayMetadataResponse = Pick; +type DiscordGatewayFetchInit = Record & { + headers?: Record; +}; type DiscordGatewayFetch = ( input: string, - init?: Record, + init?: DiscordGatewayFetchInit, ) => Promise; export function resolveDiscordGatewayIntents( @@ -74,15 +77,16 @@ function createGatewayMetadataError(params: { async function fetchDiscordGatewayInfo(params: { token: string; fetchImpl: DiscordGatewayFetch; - fetchInit?: Record; + fetchInit?: DiscordGatewayFetchInit; }): Promise { let response: DiscordGatewayMetadataResponse; try { response = await params.fetchImpl(DISCORD_GATEWAY_BOT_URL, { + ...params.fetchInit, headers: { + ...params.fetchInit?.headers, Authorization: `Bot ${params.token}`, }, - ...params.fetchInit, }); } catch (error) { throw createGatewayMetadataError({ @@ -92,7 +96,16 @@ async function fetchDiscordGatewayInfo(params: { }); } - const body = await response.text(); + let body: string; + try { + body = await response.text(); + } catch (error) { + throw createGatewayMetadataError({ + detail: error instanceof Error ? error.message : String(error), + transient: true, + cause: error, + }); + } const summary = summarizeGatewayResponseBody(body); const transient = isTransientDiscordGatewayResponse(response.status, body); @@ -128,7 +141,7 @@ function createGatewayPlugin(params: { autoInteractions: boolean; }; fetchImpl: DiscordGatewayFetch; - fetchInit?: Record; + fetchInit?: DiscordGatewayFetchInit; wsAgent?: HttpsProxyAgent; }): GatewayPlugin { class SafeGatewayPlugin extends GatewayPlugin { diff --git a/src/discord/monitor/provider.proxy.test.ts b/src/discord/monitor/provider.proxy.test.ts index 50caec4a2c8..0b45fd2a2e7 100644 --- a/src/discord/monitor/provider.proxy.test.ts +++ b/src/discord/monitor/provider.proxy.test.ts @@ -255,4 +255,30 @@ describe("createDiscordGatewayPlugin", () => { ); expect(baseRegisterClientSpy).toHaveBeenCalledTimes(1); }); + + it("maps body read failures to fetch failed", async () => { + const runtime = createRuntime(); + globalFetchMock.mockResolvedValue({ + ok: true, + status: 200, + text: async () => { + throw new Error("body stream closed"); + }, + } as unknown as Response); + const plugin = createDiscordGatewayPlugin({ + discordConfig: {}, + runtime, + }); + + await expect( + ( + plugin as unknown as { + registerClient: (client: { options: { token: string } }) => Promise; + } + ).registerClient({ + options: { token: "token-123" }, + }), + ).rejects.toThrow("Failed to get gateway information from Discord: fetch failed"); + expect(baseRegisterClientSpy).not.toHaveBeenCalled(); + }); });