Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
type: collection.insomnia.rest/5.0

@arora-r arora-r Apr 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created the collection in the insomnia app and then exported them for this file

schema_version: "5.1"
name: Response Download Tests
meta:
id: wrk_response_download_tests_001
created: 1776178027509
modified: 1776178027509
collection:
- url: http://127.0.0.1:4010/pets/1
name: JSON Request
meta:
id: req_0d0ec3bafe584da3a3ecfc668bf2824a
created: 1776178029290
modified: 1776178033770
isPrivate: false
description: ""
sortKey: -1776178029290
method: GET
headers:
- name: Content-Type
value: application/json
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
- url: http://127.0.0.1:4010/large-json
name: Large JSON Request
meta:
id: req_10813c5f0ea14388a5141987645e2e94
created: 1776178064890
modified: 1776178068539
isPrivate: false
description: ""
sortKey: -1776178064890
method: GET
headers:
- name: Content-Type
value: application/json
settings:
renderRequestBody: true
encodeUrl: true
followRedirects: global
cookies:
send: true
store: true
rebuildPath: true
9 changes: 9 additions & 0 deletions packages/insomnia-smoke-test/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ app.get('/pets/:id', (req, res) => {
res.status(200).send({ id: req.params.id });
});

app.get('/large-json', (_req, res) => {
const items = Array.from({ length: 100_000 }, (_, i) => ({
id: i,
name: `item-${i}`,
value: 'x'.repeat(100),
}));
res.status(200).json({ items });
});
Comment on lines +57 to +64

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generates ~13MB response size


app.get('/builds/check/*', (_req, res) => {
res.status(200).send({
url: 'https://github.com/Kong/insomnia/releases/download/core@2023.5.6/Insomnia.Core-2023.5.6.zip',
Expand Down
114 changes: 114 additions & 0 deletions packages/insomnia-smoke-test/tests/smoke/response-downloads.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import fs from 'node:fs';
import path from 'node:path';

import { expect } from '@playwright/test';

import { test } from '../../playwright/test';
import {
cleanupExportDir,
createTempExportDir,
mockSaveDialogForFile,
readExportedFile,
waitForExportFiles,
} from '../../playwright/utils';

test.describe('Response Downloads', () => {
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');

const FIXTURE = 'response-download-collection.yaml';

test('Can export raw response body', async ({ insomnia, app, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await page.getByLabel('Request Collection').getByTestId('JSON Request').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');

const tempDir = createTempExportDir();
const exportPath = path.join(tempDir, 'response-raw.json');
try {
await mockSaveDialogForFile(app, exportPath);
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Export raw response' }).click();
await waitForExportFiles(tempDir, 1);
const content = readExportedFile(exportPath);
expect.soft(content).toContain('"id"');
expect.soft(content).toContain('"1"');
} finally {
cleanupExportDir(tempDir);
}
});

test('Can export prettified JSON response', async ({ insomnia, app, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await page.getByLabel('Request Collection').getByTestId('JSON Request').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');

const tempDir = createTempExportDir();
const exportPath = path.join(tempDir, 'response-pretty.json');
try {
await mockSaveDialogForFile(app, exportPath);
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Export prettified response' }).click();
await waitForExportFiles(tempDir, 1);
const content = readExportedFile(exportPath);
const parsed = JSON.parse(content);
expect.soft(parsed.id).toBe('1');
expect.soft(content.length).toBeGreaterThan('{"id":"1"}'.length);
} finally {
cleanupExportDir(tempDir);
}
});

test('Can export HTTP debug file', async ({ insomnia, app, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await page.getByLabel('Request Collection').getByTestId('JSON Request').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');

const tempDir = createTempExportDir();
const exportPath = path.join(tempDir, 'response-debug.txt');
try {
await mockSaveDialogForFile(app, exportPath);
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Export HTTP debug' }).click();
await waitForExportFiles(tempDir, 1);
const content = readExportedFile(exportPath);
expect.soft(content).toContain('Content-Type');
expect.soft(content).toContain('"id"');
} finally {
cleanupExportDir(tempDir);
}
});

test('Can export raw large response body', async ({ insomnia, app, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await page.getByLabel('Request Collection').getByTestId('Large JSON Request').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');

const tempDir = createTempExportDir();
const exportPath = path.join(tempDir, 'response-large.json');
try {
await mockSaveDialogForFile(app, exportPath);
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Export raw response' }).click();
await waitForExportFiles(tempDir, 1, 30_000);
const { size } = fs.statSync(exportPath);
expect.soft(size).toBeGreaterThan(5 * 1024 * 1024);
} finally {
cleanupExportDir(tempDir);
}
});

test('Prettified export is disabled for large JSON response', async ({ insomnia, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await page.getByLabel('Request Collection').getByTestId('Large JSON Request').press('Enter');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect.soft(page.locator('[data-testid="response-status-tag"]:visible')).toContainText('200 OK');

await page.getByRole('button', { name: 'Preview' }).click();
const prettifyItem = page.getByRole('menuitem', { name: 'Export prettified response' });
await expect.soft(prettifyItem).toContainText('must be <5MB');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import React, { type FC, useCallback } from 'react';
import { Button } from 'react-aria-components';

import { models, services } from '~/insomnia-data';
import { getBodyBuffer } from '~/models/helpers/response-operations';

import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from '../../../common/constants';
import { getPreviewModeName, LARGE_RESPONSE_MB, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from '../../../common/constants';
import { exportHarCurrentRequest } from '../../../common/har';
import {
type RequestLoaderData,
useRequestLoaderData,
} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId';
import { useRequestMetaPatcher } from '../../hooks/use-request';
import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown';
import { showToast } from '../toast-notification';

interface Props {
download: (pretty: boolean) => any;
Expand Down Expand Up @@ -77,14 +79,36 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
return;
}

if (filePath && activeResponse.bodyBuffer) {
await window.main.writeFile({
path: filePath,
content: headers + '\n' + activeResponse.bodyBuffer.toString('utf8') || '',
});
if (filePath) {
try {
let body: Buffer;
if (activeResponse.bodyBuffer) {
body = activeResponse.bodyBuffer;
} else if (activeResponse.bodyPath) {
const raw = await getBodyBuffer(activeResponse);
body = typeof raw === 'string' ? Buffer.from(raw) : raw;
} else {
body = Buffer.alloc(0);
}
await window.main.writeFile({
path: filePath,
content: headers + '\n' + body.toString('utf8'),
});
} catch (error) {
console.error('Failed to read response body for debug export', error);
showToast({
icon: 'circle-exclamation',
title: 'Export failed',
description: 'Could not read the response body from disk.',
status: 'error',
});
}
}
}, [activeRequest, activeResponse]);
const shouldPrettifyOption = activeResponse?.contentType.includes('json');
const isJsonResponse = activeResponse?.contentType.includes('json');
const isLargeResponse = activeResponse
? Math.max(activeResponse.bytesContent ?? 0, activeResponse.bytesRead ?? 0) > LARGE_RESPONSE_MB * 1024 * 1024
: false;

return (
<Dropdown
Expand Down Expand Up @@ -115,8 +139,18 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
<ItemContent icon="save" label="Export raw response" onClick={handleDownloadNormal} />
</DropdownItem>
<DropdownItem aria-label="Export prettified response">
{shouldPrettifyOption && (
<ItemContent icon="save" label="Export prettified response" onClick={handleDownloadPrettify} />
{isJsonResponse && (
<ItemContent
icon="save"
label={
isLargeResponse
? `Export prettified response (must be <${LARGE_RESPONSE_MB}MB)`
: 'Export prettified response'
}
isDisabled={isLargeResponse}
className={isLargeResponse ? 'opacity-50' : ''}
onClick={handleDownloadPrettify}
/>
)}
</DropdownItem>
<DropdownItem aria-label="Export HTTP debug">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { getBodyBuffer } from '../../../../models/helpers/response-operations';
import { showToast } from '../../../../ui/components/toast-notification';
import { downloadResponseBody } from '../response-pane-utils';

vi.mock('../../../../models/helpers/response-operations', () => ({
getBodyBuffer: vi.fn(),
}));

vi.mock('../../../../ui/components/toast-notification', () => ({
showToast: vi.fn(),
}));

const mockGetBodyBuffer = vi.mocked(getBodyBuffer);
const mockShowToast = vi.mocked(showToast);

const mockWriteFile = vi.fn();
const mockShowSaveDialog = vi.fn();

Expand Down Expand Up @@ -32,11 +45,7 @@ describe('downloadResponseBody', () => {
it('warns and skips the dialog when activeRequest is null', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

await downloadResponseBody(
null,
{ contentType: 'application/json', bodyBuffer: Buffer.from('{}') },
false,
);
await downloadResponseBody(null, { contentType: 'application/json', bodyBuffer: Buffer.from('{}') }, false);

expect(warnSpy).toHaveBeenCalledWith('Nothing to download');
expect(mockShowSaveDialog).not.toHaveBeenCalled();
Expand Down Expand Up @@ -86,11 +95,7 @@ describe('downloadResponseBody', () => {
// PNG magic bytes — would be corrupted by a UTF-8 round-trip
const binaryData = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'image/png', bodyBuffer: binaryData },
false,
);
await downloadResponseBody({ name: 'My Request' }, { contentType: 'image/png', bodyBuffer: binaryData }, false);

expect(mockWriteFile).toHaveBeenCalledOnce();
const { path, content } = mockWriteFile.mock.calls[0][0];
Expand All @@ -103,11 +108,7 @@ describe('downloadResponseBody', () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.txt' });
const textData = Buffer.from('Hello, World!');

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'text/plain', bodyBuffer: textData },
true,
);
await downloadResponseBody({ name: 'My Request' }, { contentType: 'text/plain', bodyBuffer: textData }, true);

expect(mockWriteFile).toHaveBeenCalledOnce();
const { content } = mockWriteFile.mock.calls[0][0];
Expand All @@ -130,4 +131,72 @@ describe('downloadResponseBody', () => {
expect(content.length).toBe(0);
});
});

describe('large response fallback (bodyBuffer undefined, reads from disk)', () => {
it('reads from disk via getBodyBuffer when bodyBuffer is undefined and writes raw Buffer', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.json' });
const diskBody = Buffer.from('{"large": true}');
mockGetBodyBuffer.mockResolvedValue(diskBody);

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyPath: '/fake/body.response', bodyCompression: null },
false,
);

expect(mockGetBodyBuffer).toHaveBeenCalledOnce();
expect(mockWriteFile).toHaveBeenCalledOnce();
const { content } = mockWriteFile.mock.calls[0][0];
expect(Buffer.isBuffer(content)).toBe(true);
expect(content).toEqual(diskBody);
});

it('writes empty Buffer when getBodyBuffer returns an empty buffer', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.bin' });
mockGetBodyBuffer.mockResolvedValue(Buffer.alloc(0));

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/octet-stream', bodyPath: '/fake/body.response', bodyCompression: null },
false,
);

expect(mockWriteFile).toHaveBeenCalledOnce();
const { content } = mockWriteFile.mock.calls[0][0];
expect(Buffer.isBuffer(content)).toBe(true);
expect(content.length).toBe(0);
});

it('does not crash and does not write when getBodyBuffer rejects, and shows an error toast', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.bin' });
mockGetBodyBuffer.mockRejectedValue(new Error('disk read failed'));
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/octet-stream', bodyPath: '/fake/body.response', bodyCompression: null },
false,
);

expect(errorSpy).toHaveBeenCalledWith('Failed to read response body for export', expect.any(Error));
expect(mockShowToast).toHaveBeenCalledOnce();
expect(mockWriteFile).not.toHaveBeenCalled();
});

it('handles getBodyBuffer returning a string by converting to Buffer', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.txt' });
mockGetBodyBuffer.mockResolvedValue('string response body');

await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'text/plain', bodyPath: '/fake/body.response', bodyCompression: null },
false,
);

expect(mockWriteFile).toHaveBeenCalledOnce();
const { content } = mockWriteFile.mock.calls[0][0];
expect(Buffer.isBuffer(content)).toBe(true);
expect(content.toString('utf8')).toBe('string response body');
});
});
});
Loading
Loading