Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
});
});

Expand Down Expand Up @@ -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)', () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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();
});
});
});
33 changes: 30 additions & 3 deletions packages/insomnia/src/ui/components/panes/response-pane-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -24,12 +33,30 @@ export async function downloadResponseBody(
if (canceled) {
return;
}
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;
}

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(activeResponse.bodyBuffer)) || '',
content: jsonPrettify(bodyBufferToUtf8(bodyBuffer)) || '',
});
return;
}
await window.main.writeFile({ path: outputPath, content: activeResponse.bodyBuffer ?? new Uint8Array(0) });
await window.main.writeFile({ path: outputPath, content: bodyBuffer ?? new Uint8Array(0) });
}
13 changes: 12 additions & 1 deletion packages/insomnia/src/ui/components/panes/response-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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[]>([]);
Expand Down