From eb5fb2aa69f4bd42014b7d7b0ac08ce13825f6eb Mon Sep 17 00:00:00 2001 From: pick-cat Date: Tue, 30 Jun 2026 01:50:38 +0800 Subject: [PATCH] fix(microsoft-foundry): bound connection test error reads (#97812) Co-authored-by: Pick-cat <266665499+Pick-cat@users.noreply.github.com> --- .../onboard.connection.test.ts | 73 +++++++++++++++++++ extensions/microsoft-foundry/onboard.ts | 13 +++- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 extensions/microsoft-foundry/onboard.connection.test.ts diff --git a/extensions/microsoft-foundry/onboard.connection.test.ts b/extensions/microsoft-foundry/onboard.connection.test.ts new file mode 100644 index 00000000000..836747d74d8 --- /dev/null +++ b/extensions/microsoft-foundry/onboard.connection.test.ts @@ -0,0 +1,73 @@ +// Microsoft Foundry tests cover bounded connection-test error reads. +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as cli from "./cli.js"; +import { testFoundryConnection } from "./onboard.js"; +import { DEFAULT_API } from "./shared.js"; + +const hoisted = vi.hoisted(() => ({ + fetchWithSsrFGuard: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ + fetchWithSsrFGuard: hoisted.fetchWithSsrFGuard, +})); + +function cancelTrackedResponse( + text: string, + init: ResponseInit, +): { + response: Response; + wasCanceled: () => boolean; +} { + let canceled = false; + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode(text)); + }, + cancel() { + canceled = true; + }, + }); + return { + response: new Response(stream, init), + wasCanceled: () => canceled, + }; +} + +describe("testFoundryConnection", () => { + beforeEach(() => { + vi.spyOn(cli, "getAccessTokenResult").mockReturnValue({ accessToken: "token" }); + }); + + afterEach(() => { + vi.restoreAllMocks(); + hoisted.fetchWithSsrFGuard.mockReset(); + }); + + it("bounds connection-test error bodies without using response.text()", async () => { + const note = vi.fn(); + const tracked = cancelTrackedResponse(`${"foundry failure ".repeat(1024)}tail`, { + status: 503, + headers: { "content-type": "text/plain" }, + }); + const textSpy = vi.spyOn(tracked.response, "text").mockRejectedValue(new Error("unbounded")); + hoisted.fetchWithSsrFGuard.mockResolvedValue({ + response: tracked.response, + release: async () => {}, + }); + + await testFoundryConnection({ + ctx: { prompter: { note } } as never, + endpoint: "https://example.openai.azure.com", + modelId: "gpt-4o", + api: DEFAULT_API, + }); + + expect(textSpy).not.toHaveBeenCalled(); + expect(tracked.wasCanceled()).toBe(true); + expect(note).toHaveBeenCalledWith( + expect.stringContaining("Warning: test request returned 503"), + "Connection Test", + ); + }); +}); diff --git a/extensions/microsoft-foundry/onboard.ts b/extensions/microsoft-foundry/onboard.ts index e9815ec876c..6f6e8fa1334 100644 --- a/extensions/microsoft-foundry/onboard.ts +++ b/extensions/microsoft-foundry/onboard.ts @@ -1,6 +1,7 @@ // Microsoft Foundry setup module handles plugin onboarding behavior. import type { ProviderAuthContext } from "openclaw/plugin-sdk/core"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { readResponseTextLimited } from "openclaw/plugin-sdk/provider-http"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeOptionalString, @@ -32,6 +33,8 @@ import { usesFoundryResponsesByDefault, } from "./shared.js"; +const FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES = 8 * 1024; + export { listSubscriptions } from "./cli.js"; function listFoundryResources(subscriptionId?: string): FoundryResourceOption[] { @@ -605,13 +608,19 @@ export async function testFoundryConnection(params: { }); try { if (res.status === 400) { - const body = await res.text().catch(() => ""); + const body = await readResponseTextLimited( + res, + FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES, + ).catch(() => ""); await params.ctx.prompter.note( `Endpoint is reachable but returned 400 Bad Request - check your deployment name and API version.\n${body.slice(0, 200)}`, "Connection Test", ); } else if (!res.ok) { - const body = await res.text().catch(() => ""); + const body = await readResponseTextLimited( + res, + FOUNDRY_CONNECTION_TEST_ERROR_BODY_LIMIT_BYTES, + ).catch(() => ""); await params.ctx.prompter.note( `Warning: test request returned ${res.status}. ${body.slice(0, 200)}\nProceeding anyway - you can fix the endpoint later.`, "Connection Test",