From eb7da0a2e558456bdb00ec3cf6cfb00ad3eaf40a Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 19 Jun 2026 12:03:31 +0200 Subject: [PATCH] fix(plugins): cancel self-hosted probe error bodies --- .../provider-self-hosted-setup.test.ts | 44 ++++++++++++++++++- src/plugins/provider-self-hosted-setup.ts | 8 ++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/plugins/provider-self-hosted-setup.test.ts b/src/plugins/provider-self-hosted-setup.test.ts index 9f2f72ed2ac..d6429992438 100644 --- a/src/plugins/provider-self-hosted-setup.test.ts +++ b/src/plugins/provider-self-hosted-setup.test.ts @@ -86,10 +86,30 @@ async function configureSelfHostedTestProvider(params: { }); } +function cancelTrackedResponse(init?: ResponseInit): { + response: Response; + wasCanceled: () => boolean; +} { + let canceled = false; + const stream = new ReadableStream({ + start(controller) { + controller.enqueue(new TextEncoder().encode("ignored")); + }, + cancel() { + canceled = true; + }, + }); + return { + response: new Response(stream, init), + wasCanceled: () => canceled, + }; +} + describe("discoverOpenAICompatibleLocalModels", () => { it("uses guarded fetch pinned to the configured self-hosted provider", async () => { const release = vi.fn(async () => undefined); const propsRelease = vi.fn(async () => undefined); + const propsResponse = cancelTrackedResponse({ status: 404 }); fetchWithSsrFGuardMock.mockResolvedValueOnce({ response: new Response(JSON.stringify({ data: [{ id: "Qwen/Qwen3-32B" }] }), { status: 200, @@ -98,7 +118,7 @@ describe("discoverOpenAICompatibleLocalModels", () => { release, }); fetchWithSsrFGuardMock.mockResolvedValueOnce({ - response: new Response("{}", { status: 404 }), + response: propsResponse.response, finalUrl: "http://127.0.0.1:8000/props", release: propsRelease, }); @@ -141,6 +161,28 @@ describe("discoverOpenAICompatibleLocalModels", () => { }); expect(release).toHaveBeenCalledOnce(); expect(propsRelease).toHaveBeenCalledOnce(); + expect(propsResponse.wasCanceled()).toBe(true); + }); + + it("cancels model discovery error bodies before falling back", async () => { + const release = vi.fn(async () => undefined); + const response = cancelTrackedResponse({ status: 503, statusText: "Service Unavailable" }); + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: response.response, + finalUrl: "http://127.0.0.1:8000/v1/models", + release, + }); + + const models = await discoverOpenAICompatibleLocalModels({ + baseUrl: "http://127.0.0.1:8000/v1/", + apiKey: "self-hosted-test-key", + label: "vLLM", + env: {}, + }); + + expect(models).toEqual([]); + expect(release).toHaveBeenCalledOnce(); + expect(response.wasCanceled()).toBe(true); }); it("uses llama.cpp nested /props n_ctx as the runtime context cap", async () => { diff --git a/src/plugins/provider-self-hosted-setup.ts b/src/plugins/provider-self-hosted-setup.ts index 662c4c3ca43..f66f8ef9fea 100644 --- a/src/plugins/provider-self-hosted-setup.ts +++ b/src/plugins/provider-self-hosted-setup.ts @@ -86,6 +86,12 @@ function readPositiveInteger(value: unknown): number | undefined { return Math.trunc(value); } +async function cancelUnreadResponseBody(response: Response): Promise { + if (!response.bodyUsed) { + await response.body?.cancel().catch(() => undefined); + } +} + function resolveLlamaCppPropsUrl(baseUrl: string, modelId?: string): string { const parsed = new URL(baseUrl); const pathname = parsed.pathname.replace(/\/+$/, ""); @@ -124,6 +130,7 @@ async function discoverLlamaCppRuntimeContextTokens(params: { }); try { if (!response.ok) { + await cancelUnreadResponseBody(response); return undefined; } const data = (await response.json()) as LlamaCppPropsResponse; @@ -167,6 +174,7 @@ export async function discoverOpenAICompatibleLocalModels(params: { }); try { if (!response.ok) { + await cancelUnreadResponseBody(response); log.warn(`Failed to discover ${params.label} models: ${response.status}`); return []; }