fix: large response save to file

This commit is contained in:
copilot-swe-agent[bot]
2026-06-25 03:02:56 +00:00
committed by Jay Wu
parent 4f5a72dc16
commit 6fefc7ff60
3 changed files with 108 additions and 8 deletions

View File

@@ -3,12 +3,13 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { downloadResponseBody } from '../response-pane-utils';
const mockWriteFile = vi.fn();
const mockWriteResponseBodyToFile = vi.fn();
const mockShowSaveDialog = vi.fn();
beforeEach(() => {
vi.stubGlobal('window', {
dialog: { showSaveDialog: mockShowSaveDialog },
main: { writeFile: mockWriteFile },
main: { writeFile: mockWriteFile, writeResponseBodyToFile: mockWriteResponseBodyToFile },
});
});
@@ -78,6 +79,23 @@ describe('downloadResponseBody', () => {
expect(content).toContain('"b": 2');
expect(content).toContain('"a": 1');
});
it('reads the response body before prettifying when it is not stored in memory', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.json' });
const getBodyBuffer = vi.fn().mockResolvedValue(new TextEncoder().encode('{"b":2,"a":1}'));
await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyBuffer: null },
true,
getBodyBuffer,
);
expect(getBodyBuffer).toHaveBeenCalledOnce();
expect(mockWriteFile).toHaveBeenCalledOnce();
expect(mockWriteFile.mock.calls[0][0].content).toContain('"b": 2');
expect(mockWriteFile.mock.calls[0][0].content).toContain('"a": 1');
});
});
describe('raw-bytes branch (default)', () => {
@@ -93,6 +111,7 @@ describe('downloadResponseBody', () => {
expect(path).toBe('/tmp/out.png');
expect(content).toBeInstanceOf(Uint8Array);
expect(content).toEqual(binaryData);
expect(mockWriteResponseBodyToFile).not.toHaveBeenCalled();
});
it('writes raw bytes when prettify is true but the content-type is not JSON', async () => {
@@ -105,6 +124,7 @@ describe('downloadResponseBody', () => {
const { content } = mockWriteFile.mock.calls[0][0];
expect(content).toBeInstanceOf(Uint8Array);
expect(content).toEqual(textData);
expect(mockWriteResponseBodyToFile).not.toHaveBeenCalled();
});
it('writes empty bytes when bodyBuffer is null', async () => {
@@ -120,6 +140,48 @@ describe('downloadResponseBody', () => {
const { content } = mockWriteFile.mock.calls[0][0];
expect(content).toBeInstanceOf(Uint8Array);
expect(content.length).toBe(0);
expect(mockWriteResponseBodyToFile).not.toHaveBeenCalled();
});
it('copies the response body file when bodyBuffer is null and bodyPath is available', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.bin' });
await downloadResponseBody(
{ name: 'My Request' },
{
contentType: 'application/octet-stream',
bodyBuffer: null,
bodyPath: '/tmp/responses/abc.response',
bodyCompression: 'zip',
},
false,
);
expect(mockWriteResponseBodyToFile).toHaveBeenCalledOnce();
expect(mockWriteResponseBodyToFile).toHaveBeenCalledWith({
sourcePath: '/tmp/responses/abc.response',
destinationPath: '/tmp/out.bin',
bodyCompression: 'zip',
});
expect(mockWriteFile).not.toHaveBeenCalled();
});
it('reads the response body when it is not stored in memory', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.bin' });
const fileBody = new TextEncoder().encode('body from disk');
const getBodyBuffer = vi.fn().mockResolvedValue(fileBody);
await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/octet-stream', bodyBuffer: null },
false,
getBodyBuffer,
);
expect(getBodyBuffer).toHaveBeenCalledOnce();
expect(mockWriteFile).toHaveBeenCalledOnce();
expect(mockWriteFile.mock.calls[0][0].content).toEqual(fileBody);
expect(mockWriteResponseBodyToFile).not.toHaveBeenCalled();
});
});
});

View File

@@ -5,8 +5,17 @@ import { jsonPrettify } from '~/ui/utils/prettify/json';
export async function downloadResponseBody(
activeRequest: { name: string } | null | undefined,
activeResponse: { contentType: string; bodyBuffer?: Uint8Array | null } | null | undefined,
activeResponse:
| {
contentType: string;
bodyBuffer?: Uint8Array | null;
bodyPath?: string;
bodyCompression?: 'zip' | null | '__NEEDS_MIGRATION__';
}
| null
| undefined,
prettify: boolean,
getBodyBuffer?: () => Promise<Uint8Array | null>,
) {
if (!activeResponse || !activeRequest) {
console.warn('Nothing to download');
@@ -24,12 +33,30 @@ export async function downloadResponseBody(
if (canceled) {
return;
}
if (prettify && contentType.includes('json')) {
await window.main.writeFile({
path: outputPath,
content: jsonPrettify(bodyBufferToUtf8(activeResponse.bodyBuffer)) || '',
let bodyBuffer = activeResponse.bodyBuffer ?? null;
if (!bodyBuffer && !prettify && activeResponse.bodyPath) {
await window.main.writeResponseBodyToFile({
sourcePath: activeResponse.bodyPath,
destinationPath: outputPath,
bodyCompression: activeResponse.bodyCompression === 'zip' ? 'zip' : null,
});
return;
}
await window.main.writeFile({ path: outputPath, content: activeResponse.bodyBuffer ?? new Uint8Array(0) });
if (!bodyBuffer && getBodyBuffer) {
const diskBodyBuffer = await getBodyBuffer();
if (diskBodyBuffer) {
bodyBuffer = diskBodyBuffer;
}
}
if (prettify && contentType.includes('json')) {
await window.main.writeFile({
path: outputPath,
content: jsonPrettify(bodyBufferToUtf8(bodyBuffer)) || '',
});
return;
}
await window.main.writeFile({ path: outputPath, content: bodyBuffer ?? new Uint8Array(0) });
}

View File

@@ -68,7 +68,18 @@ export const ResponsePane: FC<Props> = ({ activeRequestId }) => {
const { isExecuting, steps } = useExecutionState({ requestId: activeRequest._id });
const handleDownloadResponseBody = useCallback(
(prettify: boolean) => downloadResponseBody(activeRequest, activeResponse, prettify),
(prettify: boolean) =>
downloadResponseBody(
activeRequest,
activeResponse,
prettify,
activeResponse
? async () => {
const bodyBuffer = await services.helpers.getResponseBodyBuffer(activeResponse);
return typeof bodyBuffer === 'string' ? null : bodyBuffer;
}
: undefined,
),
[activeRequest, activeResponse],
);
const [timeline, setTimeline] = useState<ResponseTimelineEntry[]>([]);