mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(qqbot): reject open HTTP error bodies during debug capture (#119466)
* fix(qqbot): cancel rejected direct-upload body fire-and-forget under debug proxy * test(qqbot): prove capture tee cancellation stays non-blocking Punchcard-Session: clear-orchard-lantern-bc --------- Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -451,4 +451,72 @@ describe("MediaApi.uploadMedia direct URL uploads", () => {
|
||||
expect(tokenManager["getAccessToken"]).not.toHaveBeenCalled();
|
||||
expect(client["request"]).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects promptly when a capture clone keeps body cancellation pending", async () => {
|
||||
fetchWithSsrFGuardMock.mockReset();
|
||||
const response = new Response(
|
||||
new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode("server error"));
|
||||
},
|
||||
}),
|
||||
{ status: 500 },
|
||||
);
|
||||
const captureClone = response.clone();
|
||||
const release = vi.fn(async () => {});
|
||||
fetchWithSsrFGuardMock.mockResolvedValueOnce({ response, release });
|
||||
|
||||
const body = response.body!;
|
||||
const originalCancel = body.cancel.bind(body);
|
||||
let cancellation: Promise<void> | undefined;
|
||||
let cancellationSettled = false;
|
||||
const cancellationStarted = new Promise<void>((resolve) => {
|
||||
vi.spyOn(body, "cancel").mockImplementation((reason) => {
|
||||
cancellation = originalCancel(reason).finally(() => {
|
||||
cancellationSettled = true;
|
||||
});
|
||||
resolve();
|
||||
return cancellation;
|
||||
});
|
||||
});
|
||||
|
||||
const client = mockApiClient();
|
||||
const tokenManager = mockTokenManager();
|
||||
const api = new MediaApi(client, tokenManager);
|
||||
const upload = api.uploadMedia(
|
||||
"c2c",
|
||||
"user-openid",
|
||||
MediaFileType.IMAGE,
|
||||
{ appId: "app-id", clientSecret: "client-secret" },
|
||||
{ url: "https://cdn.example.com/server-error.png" },
|
||||
);
|
||||
const cancellationPending = Symbol("capture cancellation pending");
|
||||
|
||||
try {
|
||||
await cancellationStarted;
|
||||
expect(cancellationSettled).toBe(false);
|
||||
|
||||
const result = await Promise.race([
|
||||
upload.then(
|
||||
() => undefined,
|
||||
(error: unknown) => error,
|
||||
),
|
||||
new Promise<symbol>((resolve) => {
|
||||
setImmediate(() => resolve(cancellationPending));
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(result).not.toBe(cancellationPending);
|
||||
expect(result).toMatchObject({
|
||||
message: "Direct-upload media URL returned HTTP 500",
|
||||
});
|
||||
expect(release).toHaveBeenCalledOnce();
|
||||
expect(tokenManager["getAccessToken"]).not.toHaveBeenCalled();
|
||||
expect(client["request"]).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
void captureClone.body?.cancel().catch(() => undefined);
|
||||
await cancellation?.catch(() => undefined);
|
||||
await upload.catch(() => undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +161,10 @@ export async function downloadDirectUploadUrl(
|
||||
const { response, release } = await fetchDirectUploadDownload(parsed.toString());
|
||||
try {
|
||||
if (!response.ok) {
|
||||
await response.body?.cancel().catch(() => undefined);
|
||||
// A debug-capture clone can keep the tee open, so waiting for cancel would
|
||||
// hang before the error is returned. Fire-and-forget matches the timeout
|
||||
// path above and the pattern used across other plugins.
|
||||
void response.body?.cancel().catch(() => undefined);
|
||||
throw new Error(`Direct-upload media URL returned HTTP ${response.status}`);
|
||||
}
|
||||
return await readDirectUploadResponse(response, opts.maxBytes ?? MAX_UPLOAD_SIZE);
|
||||
|
||||
Reference in New Issue
Block a user