diff --git a/extensions/chutes/oauth.test.ts b/extensions/chutes/oauth.test.ts index 28aaf6daf942..29ec201c46b1 100644 --- a/extensions/chutes/oauth.test.ts +++ b/extensions/chutes/oauth.test.ts @@ -270,6 +270,47 @@ describe("chutes plugin OAuth", () => { expect(timeoutSpy).toHaveBeenCalledTimes(2); }); + it("cancels the userinfo error response body when profile lookup fails", async () => { + let canceled = false; + let bytesPulled = 0; + const userInfoResponse = new Response( + new ReadableStream({ + pull(controller) { + if (bytesPulled > 0) { + controller.close(); + return; + } + bytesPulled += 1; + controller.enqueue(new TextEncoder().encode("temporarily unavailable")); + }, + cancel() { + canceled = true; + }, + }), + { status: 503 }, + ); + const fetchFn = vi.fn(async (input: RequestInfo | URL) => { + const url = fetchInputUrl(input); + if (url === CHUTES_TOKEN_ENDPOINT) { + return new Response( + '{"access_token":"at_123","refresh_token":"rt_123","expires_in":3600}', + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + if (url === CHUTES_USERINFO_ENDPOINT) { + return userInfoResponse; + } + return new Response("not found", { status: 404 }); + }); + + const credentials = await loginWithFetch(fetchFn); + + expect(canceled).toBe(true); + expect(credentials.access).toBe("at_123"); + expect(credentials.email).toBeUndefined(); + expect(credentials.accountId).toBeUndefined(); + }); + it("cancels authentication when the caller aborts during userinfo", async () => { const controller = new AbortController(); const reason = new Error("cancelled by caller"); diff --git a/extensions/chutes/oauth.ts b/extensions/chutes/oauth.ts index b9b75696d27e..68ea85502117 100644 --- a/extensions/chutes/oauth.ts +++ b/extensions/chutes/oauth.ts @@ -129,6 +129,8 @@ async function fetchChutesUserInfo(params: { }), }); if (!response.ok) { + // Release the connection instead of leaving the error body to idle timeout. + await response.body?.cancel().catch(() => undefined); return null; } const data = await readProviderJsonResponse(response, "Chutes userinfo");