fix(lmstudio): cancel model discovery response body on non-ok (#109718)

* fix(lmstudio): cancel model discovery response body on non-ok

Wire the direct-fetch release hook to cancel unread bodies so failed
model discovery releases the TCP connection instead of leaving it open.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(lmstudio): unify response cleanup

Co-authored-by: Zeng Wen <27948732+ZengWen-DT@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Zeng Wen <27948732+ZengWen-DT@users.noreply.github.com>
This commit is contained in:
Wynne668
2026-07-17 01:12:23 -07:00
committed by GitHub
co-authored by Cursor Peter Steinberger Zeng Wen
parent 4ecd85e0b7
commit 1573c78f5b
2 changed files with 55 additions and 7 deletions
+23 -7
View File
@@ -48,6 +48,12 @@ type DiscoverLmstudioModelsParams = {
fetchImpl?: typeof fetch;
};
async function cancelUnreadResponseBody(response: Response): Promise<void> {
if (!response.bodyUsed) {
await response.body?.cancel().catch(() => undefined);
}
}
async function fetchLmstudioEndpoint(params: {
url: string;
init?: RequestInit;
@@ -57,8 +63,10 @@ async function fetchLmstudioEndpoint(params: {
auditContext: string;
}): Promise<{ response: Response; release: () => Promise<void> }> {
const timeoutMs = resolveTimerTimeoutMs(params.timeoutMs, 1);
let response: Response;
let release: () => Promise<void>;
if (params.ssrfPolicy) {
return await fetchWithSsrFGuard({
const guarded = await fetchWithSsrFGuard({
url: params.url,
init: params.init,
timeoutMs,
@@ -66,14 +74,22 @@ async function fetchLmstudioEndpoint(params: {
policy: params.ssrfPolicy,
auditContext: params.auditContext,
});
}
const fetchFn = params.fetchImpl ?? fetch;
return {
response: await fetchFn(params.url, {
response = guarded.response;
release = guarded.release;
} else {
const fetchFn = params.fetchImpl ?? fetch;
response = await fetchFn(params.url, {
...params.init,
signal: AbortSignal.timeout(timeoutMs),
}),
release: async () => {},
});
release = async () => undefined;
}
return {
response,
release: async () => {
await cancelUnreadResponseBody(response);
await release();
},
};
}
+32
View File
@@ -392,6 +392,38 @@ describe("lmstudio-models", () => {
});
});
it("cancels the response body after a non-ok model discovery response", async () => {
const tracked = cancelTrackedResponse("unavailable", { status: 503 });
const fetchMock = vi.fn(async () => tracked.response);
const result = await fetchLmstudioModels({
baseUrl: "http://localhost:1234/v1",
fetchImpl: asFetch(fetchMock),
});
expect(result).toEqual({
reachable: true,
status: 503,
models: [],
});
expect(tracked.wasCanceled()).toBe(true);
});
it("cancels guarded non-ok discovery bodies before releasing the dispatcher", async () => {
const tracked = cancelTrackedResponse("unavailable", { status: 503 });
const release = vi.fn(async () => undefined);
fetchWithSsrFGuardMock.mockResolvedValue({ response: tracked.response, release });
const result = await fetchLmstudioModels({
baseUrl: "http://localhost:1234/v1",
ssrfPolicy: {},
});
expect(result).toMatchObject({ reachable: true, status: 503, models: [] });
expect(tracked.wasCanceled()).toBe(true);
expect(release).toHaveBeenCalledOnce();
});
it("reports malformed model list JSON with an owned error", async () => {
const fetchMock = vi.fn(async () => malformedJsonResponse());