diff --git a/scripts/github/real-behavior-proof-policy.mjs b/scripts/github/real-behavior-proof-policy.mjs index 14436d70bec9..2b5d6ffd52ef 100644 --- a/scripts/github/real-behavior-proof-policy.mjs +++ b/scripts/github/real-behavior-proof-policy.mjs @@ -134,6 +134,10 @@ export async function readBoundedGitHubApiJson( return JSON.parse(text); } +async function cancelGitHubApiResponseBody(response) { + await response.body?.cancel?.().catch(() => undefined); +} + function normalizeLineEndings(text = "") { return text.replace(/\r\n?/g, "\n"); } @@ -193,21 +197,30 @@ export async function isMaintainerTeamMember({ signal, }), ); - if (response.status === 404) { - return false; + try { + if (response.status === 404) { + return false; + } + if (!response.ok) { + throw new Error(`Team membership lookup failed: ${response.status}`); + } + const body = await withGitHubApiTimeout( + `maintainer membership response for ${login}`, + timeoutMs, + (signal) => + readBoundedGitHubApiJson( + response, + `maintainer membership response for ${login}`, + undefined, + { + signal, + }, + ), + ); + return body?.state === "active"; + } finally { + await cancelGitHubApiResponseBody(response); } - if (!response.ok) { - throw new Error(`Team membership lookup failed: ${response.status}`); - } - const body = await withGitHubApiTimeout( - `maintainer membership response for ${login}`, - timeoutMs, - (signal) => - readBoundedGitHubApiJson(response, `maintainer membership response for ${login}`, undefined, { - signal, - }), - ); - return body?.state === "active"; } function nextFenceMarker(line, fenceMarker) { diff --git a/test/scripts/real-behavior-proof-policy.test.ts b/test/scripts/real-behavior-proof-policy.test.ts index 35305bcca2db..5c7e7baffb93 100644 --- a/test/scripts/real-behavior-proof-policy.test.ts +++ b/test/scripts/real-behavior-proof-policy.test.ts @@ -560,6 +560,22 @@ describe("isMaintainerTeamMember", () => { expect(await isMaintainerTeamMember({ token: "t", org: "o", login: "u", fetch })).toBe(false); }); + it("cancels 404 membership response bodies", async () => { + let canceled = false; + const response = new Response( + new ReadableStream({ + cancel() { + canceled = true; + }, + }), + { status: 404 }, + ); + const fetch = vi.fn().mockResolvedValue(response); + + expect(await isMaintainerTeamMember({ token: "t", org: "o", login: "u", fetch })).toBe(false); + expect(canceled).toBe(true); + }); + it("returns false when the token, org, or login is missing", async () => { const fetch = vi.fn(); expect(await isMaintainerTeamMember({ org: "o", login: "u", fetch })).toBe(false); @@ -575,6 +591,24 @@ describe("isMaintainerTeamMember", () => { ).rejects.toThrow(/500/); }); + it("cancels unexpected HTTP error response bodies", async () => { + let canceled = false; + const response = new Response( + new ReadableStream({ + cancel() { + canceled = true; + }, + }), + { status: 500 }, + ); + const fetch = vi.fn().mockResolvedValue(response); + + await expect( + isMaintainerTeamMember({ token: "t", org: "o", login: "u", fetch }), + ).rejects.toThrow(/500/); + expect(canceled).toBe(true); + }); + it("aborts stalled membership fetches", async () => { const fetch = vi.fn((_url: string, init: RequestInit) => { return new Promise((_resolve, reject) => {