mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(github-copilot): cancel unread device-flow error bodies before throwing (#112268)
* fix(github-copilot): cancel unread device-flow error bodies before throwing
postGitHubDeviceFlowForm now drops the unread response stream before it
raises on a non-OK HTTP status. GitHub OAuth error responses ship a JSON
body (`{error, error_description}`) that this helper never consumes, so
the underlying fetch connection previously stayed open holding the
payload until the caller's release() ran, keeping the device-flow socket
half-drained on every 4xx/5xx.
Mirrors the recently-merged qqbot/kilocode/tlon fixes for the same
'cancel body before throwing' pattern (#110008, #109950, #112059).
* style(github-copilot): clarify response release ordering
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -130,6 +130,35 @@ describe("runGitHubCopilotDeviceFlow — HTTP error propagation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels the unread device code body before throwing on non-OK", async () => {
|
||||
let canceled = false;
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(
|
||||
JSON.stringify({ error: "server_error", error_description: "boom" }),
|
||||
),
|
||||
);
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
mocks.fetchWithSsrFGuard.mockImplementation(async () => ({
|
||||
response: new Response(body, {
|
||||
status: 502,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
finalUrl: DEVICE_CODE_URL,
|
||||
release: vi.fn(async () => {}),
|
||||
}));
|
||||
|
||||
await expect(runGitHubCopilotDeviceFlow({ showCode: vi.fn() })).rejects.toThrow(
|
||||
"GitHub device code failed: HTTP 502",
|
||||
);
|
||||
expect(canceled).toBe(true);
|
||||
});
|
||||
|
||||
it("throws with failureLabel on non-OK access token response", async () => {
|
||||
let callIdx = 0;
|
||||
mocks.fetchWithSsrFGuard.mockImplementation(async () => {
|
||||
@@ -145,6 +174,40 @@ describe("runGitHubCopilotDeviceFlow — HTTP error propagation", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("cancels the unread access token body before throwing on non-OK", async () => {
|
||||
let canceled = false;
|
||||
let callIdx = 0;
|
||||
mocks.fetchWithSsrFGuard.mockImplementation(async () => {
|
||||
callIdx += 1;
|
||||
if (callIdx === 1) {
|
||||
return guardResponse(VALID_DEVICE_CODE_BODY);
|
||||
}
|
||||
const body = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
new TextEncoder().encode(JSON.stringify({ error: "bad_verification_code" })),
|
||||
);
|
||||
},
|
||||
cancel() {
|
||||
canceled = true;
|
||||
},
|
||||
});
|
||||
return {
|
||||
response: new Response(body, {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
finalUrl: ACCESS_TOKEN_URL,
|
||||
release: vi.fn(async () => {}),
|
||||
};
|
||||
});
|
||||
|
||||
await expect(runDeviceFlowAfterFirstPoll({ showCode: vi.fn(async () => {}) })).rejects.toThrow(
|
||||
"GitHub device token failed: HTTP 401",
|
||||
);
|
||||
expect(canceled).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a malformed access token response", async () => {
|
||||
let callIdx = 0;
|
||||
mocks.fetchWithSsrFGuard.mockImplementation(async () => {
|
||||
|
||||
@@ -161,6 +161,8 @@ async function postGitHubDeviceFlowForm(params: {
|
||||
});
|
||||
try {
|
||||
if (!response.ok) {
|
||||
// Release closes the dispatcher, so cancel its unread response body first.
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
throw new Error(`${params.failureLabel}: HTTP ${response.status}`);
|
||||
}
|
||||
return parseJsonResponse(
|
||||
|
||||
Reference in New Issue
Block a user