From 6fefc7ff601ee98b0cb119d622fdb72f56c67fa1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 25 Jun 2026 03:02:56 +0000 Subject: [PATCH] fix: large response save to file --- .../__tests__/response-pane-utils.test.ts | 64 ++++++++++++++++++- .../components/panes/response-pane-utils.ts | 39 +++++++++-- .../src/ui/components/panes/response-pane.tsx | 13 +++- 3 files changed, 108 insertions(+), 8 deletions(-) diff --git a/packages/insomnia/src/ui/components/panes/__tests__/response-pane-utils.test.ts b/packages/insomnia/src/ui/components/panes/__tests__/response-pane-utils.test.ts index fc95411c7e..619fd07b91 100644 --- a/packages/insomnia/src/ui/components/panes/__tests__/response-pane-utils.test.ts +++ b/packages/insomnia/src/ui/components/panes/__tests__/response-pane-utils.test.ts @@ -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(); }); }); }); diff --git a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts index 73050ad52e..834c148733 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane-utils.ts +++ b/packages/insomnia/src/ui/components/panes/response-pane-utils.ts @@ -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, ) { 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) }); } diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index 32f5ed6437..f320295236 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -68,7 +68,18 @@ export const ResponsePane: FC = ({ 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([]);