fix: large responses now download correctly [INS-2365] (#9807)

* fix: large responses now download correctly

* fix: linting

* chore: add e2e testing to prevent future regressions

* fix: cloud sync test flaky (#10238)

* fix: cloud sync test flaky

* fix

* fix args to be more concrete

* Feat/basic components m0 (#10230)

* refactor: components infra

* fix

* fix: data-* class

* rename cn to cls

* chore: emit and upload junit test results per shard (#10253)

* chore: emit junit reports per e2e tests shard

* chore: include retries in junit reporter

* chore: test some stuff

* chore: test again

* chore: revert test

* fix(import): always show collection picker (#10262)

* feat(undo): broaden undo-history persistence via a stable historyKey (#10233)

* feat: add a tooltip for show/hide secret on auth tab (#10263)

* feat(templating): vetted npm libraries in the sandbox — uuid + ajv (M3) (#10244)

* fix: stop reusing blank-row ids in key-value editors [INS-2598] (#10260)

* fix: ensure unique blank IDs for environment key-value pairs to prevent stale state

* test: add validation for blank row behavior after deleting committed rows in environment table editor

* fix: restore visible keyboard focus indicators across the app (#10268)

* feat: enhance focus-visible styles for input elements

* feat: simplify focus-visible styles for form elements

* update test

* fix: route v3 spaces SDK through proxy-aware fetch (#10265)

* feat: show error when fails download

---------

Co-authored-by: Curry Yang <163384738+CurryYangxx@users.noreply.github.com>
Co-authored-by: Fares Osman <43153226+fiosman@users.noreply.github.com>
Co-authored-by: Ryan Willis <ryan.willis@konghq.com>
Co-authored-by: Jack Kavanagh <jackkav@gmail.com>
Co-authored-by: Shelby <13246465+shelby-moore@users.noreply.github.com>
Co-authored-by: Pavlos Koutoglou <pkoutoglou@gmail.com>
Co-authored-by: Alison Sabuwala <alison.sabuwala1024@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-08-10 10:49:40 -04:00
1 parent 640e0ab037
commit d0116ebe73
9 files changed
+465 -45

No files matched your search

@@ -0,0 +1,50 @@
type: collection.insomnia.rest/5.0
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
@@ -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 });
});
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',
@@ -0,0 +1,152 @@
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';
// Reads only the head of a file, so asserting on multi-MB exports stays cheap.
function readFirstBytes(filePath: string, length: number): string {
const fd = fs.openSync(filePath, 'r');
try {
const buffer = Buffer.alloc(length);
const bytesRead = fs.readSync(fd, buffer, 0, length, 0);
return buffer.subarray(0, bytesRead).toString('utf8');
} finally {
fs.closeSync(fd);
}
}
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 insomnia.navigationSidebar.clickRequestOrFolder('JSON Request');
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 insomnia.navigationSidebar.clickRequestOrFolder('JSON Request');
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 insomnia.navigationSidebar.clickRequestOrFolder('JSON Request');
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 insomnia.navigationSidebar.clickRequestOrFolder('Large JSON Request');
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);
}
});
// The "Save To File" button only ever renders for responses over LARGE_RESPONSE_MB, which are
// exactly the ones with no bodyBuffer, so it has to take the streaming raw path.
test('Can save a large response via the Save To File button', async ({ insomnia, app, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await insomnia.navigationSidebar.clickRequestOrFolder('Large JSON Request');
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-save-to-file.json');
try {
await mockSaveDialogForFile(app, exportPath);
await page.getByRole('button', { name: 'Save To File' }).click();
await waitForExportFiles(tempDir, 1, 30_000);
const { size } = fs.statSync(exportPath);
expect.soft(size).toBeGreaterThan(5 * 1024 * 1024);
// Raw bytes, not prettified: a prettified body would start with '{\n\t"items"'.
expect.soft(readFirstBytes(exportPath, 16)).toContain('{"items"');
} finally {
cleanupExportDir(tempDir);
}
});
test('Prettified export is disabled for large JSON response', async ({ insomnia, page }) => {
await insomnia.projectPage.importFixture(FIXTURE);
await insomnia.navigationSidebar.clickRequestOrFolder('Large JSON Request');
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');
// The label alone would still pass if the item stayed clickable. This class is driven by the
// same isDisabled that MenuItem uses to drop onAction, so it proves the guard is wired up.
await expect.soft(prettifyItem).toHaveClass(/cursor-not-allowed/);
});
});
@@ -3,6 +3,7 @@ import { getPreviewModeName, PREVIEW_MODE_SOURCE, PREVIEW_MODES } from 'insomnia
import React, { type FC, useCallback } from 'react';
import { Button } from 'react-aria-components';
import { LARGE_RESPONSE_MB } from '~/common/constants';
import { bodyBufferToUtf8 } from '~/common/utils/utf8-bytes';
import {
@@ -11,6 +12,7 @@ import {
} 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 { showError } from '../modals';
interface Props {
download: (pretty: boolean) => any;
@@ -36,26 +38,33 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
return;
}
const data = await window.main.exportHarCurrentRequest({
requestId: activeRequest._id,
responseId: activeResponse._id,
});
const har = JSON.stringify(data, null, '\t');
try {
const data = await window.main.exportHarCurrentRequest({
requestId: activeRequest._id,
responseId: activeResponse._id,
});
const har = JSON.stringify(data, null, '\t');
const { filePath } = await window.dialog.showSaveDialog({
title: 'Export As HAR',
buttonLabel: 'Save',
defaultPath: `${activeRequest.name.replace(/ +/g, '_')}-${Date.now()}.har`,
});
const { filePath } = await window.dialog.showSaveDialog({
title: 'Export As HAR',
buttonLabel: 'Save',
defaultPath: `${activeRequest.name.replace(/ +/g, '_')}-${Date.now()}.har`,
});
if (!filePath) {
return;
if (!filePath) {
return;
}
await window.main.writeFile({
path: filePath,
content: har,
});
} catch (err) {
showError({
title: 'Failed to export as HAR',
error: err instanceof Error ? err : new Error(String(err)),
});
}
await window.main.writeFile({
path: filePath,
content: har,
});
}, [activeRequest, activeResponse]);
const exportDebugFile = useCallback(async () => {
@@ -64,30 +73,40 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
return;
}
const timeline = await services.helpers.getResponseTimeline(activeResponse);
const headers = timeline
.filter(v => v.name === 'HeaderIn')
.map(v => v.value)
.join('');
try {
const timeline = await services.helpers.getResponseTimeline(activeResponse);
const headers = timeline
.filter(v => v.name === 'HeaderIn')
.map(v => v.value)
.join('');
const { canceled, filePath } = await window.dialog.showSaveDialog({
title: 'Save Full Response',
buttonLabel: 'Save',
defaultPath: `${activeRequest.name.replace(/ +/g, '_')}-${Date.now()}.txt`,
});
const { canceled, filePath } = await window.dialog.showSaveDialog({
title: 'Save Full Response',
buttonLabel: 'Save',
defaultPath: `${activeRequest.name.replace(/ +/g, '_')}-${Date.now()}.txt`,
});
if (canceled) {
return;
}
if (canceled || !filePath || !activeResponse.bodyBuffer) {
return;
}
if (filePath && activeResponse.bodyBuffer) {
await window.main.writeFile({
path: filePath,
content: headers + '\n' + bodyBufferToUtf8(activeResponse.bodyBuffer) || '',
});
} catch (err) {
showError({
title: 'Failed to export HTTP debug',
error: err instanceof Error ? err : new Error(String(err)),
});
}
}, [activeRequest, activeResponse]);
const shouldPrettifyOption = activeResponse?.contentType.includes('json');
// Prettifying and the debug export both need the whole body as a string in renderer memory, so
// they stay bounded. The raw export has no such limit: it streams from disk in the main process.
const isLargeResponse = activeResponse
? Math.max(activeResponse.bytesContent ?? 0, activeResponse.bytesRead ?? 0) > LARGE_RESPONSE_MB * 1024 * 1024
: false;
return (
<Dropdown
@@ -119,11 +138,25 @@ export const PreviewModeDropdown: FC<Props> = ({ download, copyToClipboard }) =>
</DropdownItem>
<DropdownItem aria-label="Export prettified response">
{shouldPrettifyOption && (
<ItemContent icon="save" label="Export prettified response" onClick={handleDownloadPrettify} />
<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">
<ItemContent icon="bug" label="Export HTTP debug" onClick={exportDebugFile} />
<ItemContent
icon="bug"
label={isLargeResponse ? `Export HTTP debug (must be <${LARGE_RESPONSE_MB}MB)` : 'Export HTTP debug'}
isDisabled={isLargeResponse}
className={isLargeResponse ? 'opacity-50' : ''}
onClick={exportDebugFile}
/>
</DropdownItem>
<DropdownItem aria-label="Export as HAR">
<ItemContent icon="save" label="Export as HAR" onClick={exportAsHAR} />
@@ -4,11 +4,12 @@ import { downloadResponseBody } from '../response-pane-utils';
const mockWriteFile = vi.fn();
const mockShowSaveDialog = vi.fn();
const mockWriteResponseBodyToFile = vi.fn();
beforeEach(() => {
vi.stubGlobal('window', {
dialog: { showSaveDialog: mockShowSaveDialog },
main: { writeFile: mockWriteFile },
main: { writeFile: mockWriteFile, writeResponseBodyToFile: mockWriteResponseBodyToFile },
});
});
@@ -122,4 +123,126 @@ describe('downloadResponseBody', () => {
expect(content.length).toBe(0);
});
});
describe('large responses (bodyBuffer absent, body on disk)', () => {
it('copies the body from disk instead of writing an empty file', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/large.json' });
// The loader leaves bodyBuffer undefined above LARGE_RESPONSE_MB; before this fix that
// produced a 0-byte export.
await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyPath: '/responses/abc.response' },
false,
);
expect(mockWriteResponseBodyToFile).toHaveBeenCalledWith({
sourcePath: '/responses/abc.response',
destinationPath: '/tmp/large.json',
bodyCompression: null,
});
expect(mockWriteFile).not.toHaveBeenCalled();
});
it('forwards zip compression so the copy is gunzipped', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/large.json' });
await downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyPath: '/responses/abc.response', bodyCompression: 'zip' },
false,
);
expect(mockWriteResponseBodyToFile).toHaveBeenCalledWith(
expect.objectContaining({ bodyCompression: 'zip' }),
);
});
it('treats __NEEDS_MIGRATION__ compression as uncompressed', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/large.json' });
await downloadResponseBody(
{
name: 'My Request',
},
{
contentType: 'application/json',
bodyPath: '/responses/abc.response',
bodyCompression: '__NEEDS_MIGRATION__',
},
false,
);
expect(mockWriteResponseBodyToFile).toHaveBeenCalledWith(expect.objectContaining({ bodyCompression: null }));
});
it('prefers the streaming copy over an in-memory buffer for the raw export', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.json' });
await downloadResponseBody(
{ name: 'My Request' },
{
contentType: 'application/json',
bodyBuffer: new TextEncoder().encode('{}'),
bodyPath: '/responses/abc.response',
},
false,
);
expect(mockWriteResponseBodyToFile).toHaveBeenCalledOnce();
expect(mockWriteFile).not.toHaveBeenCalled();
});
});
describe('prettify falls back to reading the body from disk', () => {
it('prettifies a disk-backed body via getBodyBuffer', 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', bodyPath: '/responses/abc.response' },
true,
getBodyBuffer,
);
expect(getBodyBuffer).toHaveBeenCalledOnce();
expect(mockWriteResponseBodyToFile).not.toHaveBeenCalled();
const { content } = mockWriteFile.mock.calls[0][0];
expect(typeof content).toBe('string');
expect(content).toContain('"b": 2');
});
it('throws the read-failure message when getBodyBuffer returns a string', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.json' });
const getBodyBuffer = vi.fn().mockResolvedValue('Failed to read response body');
await expect(
downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyPath: '/responses/abc.response' },
true,
getBodyBuffer,
),
).rejects.toThrow('Failed to read response body');
expect(mockWriteFile).not.toHaveBeenCalled();
});
it('throws rather than writing an empty file when the disk read yields nothing', async () => {
mockShowSaveDialog.mockResolvedValue({ canceled: false, filePath: '/tmp/out.json' });
const getBodyBuffer = vi.fn().mockResolvedValue(null);
await expect(
downloadResponseBody(
{ name: 'My Request' },
{ contentType: 'application/json', bodyPath: '/responses/abc.response' },
true,
getBodyBuffer,
),
).rejects.toThrow('Failed to read response body from filesystem');
expect(mockWriteFile).not.toHaveBeenCalled();
});
});
});
@@ -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 | string | null>,
) {
if (!activeResponse || !activeRequest) {
console.warn('Nothing to download');
@@ -24,12 +33,41 @@ export async function downloadResponseBody(
if (canceled) {
return;
}
if (prettify && contentType.includes('json')) {
await window.main.writeFile({
path: outputPath,
content: jsonPrettify(bodyBufferToUtf8(activeResponse.bodyBuffer)) || '',
// The loader only populates bodyBuffer for responses under LARGE_RESPONSE_MB, so reading it
// here would write an empty file for exactly the large responses this export needs to handle.
// Copy from disk in the main process instead, which also keeps the body out of renderer memory.
if (!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) });
let bodyBuffer = activeResponse.bodyBuffer ?? null;
if (!bodyBuffer && getBodyBuffer) {
const diskBodyBuffer = await getBodyBuffer();
if (typeof diskBodyBuffer === 'string') {
throw new TypeError(diskBodyBuffer);
}
if (diskBodyBuffer) {
bodyBuffer = diskBodyBuffer;
}
}
if (!bodyBuffer && activeResponse.bodyPath) {
throw new Error('Failed to read response body from filesystem');
}
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) });
}
@@ -21,6 +21,7 @@ import { PreviewModeDropdown } from '../dropdowns/preview-mode-dropdown';
import { ResponseHistoryDropdown } from '../dropdowns/response-history-dropdown';
import { MockResponseExtractor } from '../editors/mock-response-extractor';
import { ErrorBoundary } from '../error-boundary';
import { showError } from '../modals';
import { ResponseTimer } from '../response-timer';
import { SizeTag } from '../tags/size-tag';
import { StatusTag } from '../tags/status-tag';
@@ -73,7 +74,21 @@ export const ResponsePane: FC<Props> = ({ activeRequestId }) => {
const { isExecuting, steps } = useExecutionState({ requestId: activeRequest._id });
const handleDownloadResponseBody = useCallback(
(prettify: boolean) => downloadResponseBody(activeRequest, activeResponse, prettify),
async (prettify: boolean) => {
try {
await downloadResponseBody(
activeRequest,
activeResponse,
prettify,
activeResponse ? () => services.helpers.getResponseBodyBuffer(activeResponse) : undefined,
);
} catch (err) {
showError({
title: 'Failed to save response body',
error: err instanceof Error ? err : new Error(String(err)),
});
}
},
[activeRequest, activeResponse],
);
const [timeline, setTimeline] = useState<ResponseTimelineEntry[]>([]);
@@ -15,7 +15,7 @@ import { ResponseHeadersViewer } from './response-headers-viewer';
import { ResponseViewer } from './response-viewer';
interface Props {
download: (...args: any[]) => any;
download: (prettify: boolean) => void;
responseId: string;
bodyBuffer: Uint8Array | null;
contentType: string;
@@ -56,7 +56,7 @@ export interface ResponseViewerProps {
contentType: string;
disableHtmlPreviewJs: boolean;
disablePreviewLinks: boolean;
download: (...args: any[]) => any;
download: (prettify: boolean) => void;
editorFontSize: number;
filter: string;
filterHistory: string[];
@@ -192,7 +192,7 @@ export const ResponseViewer = ({
{hugeResponse ? (
<Fragment>
<p className="pad faint">Responses over {HUGE_RESPONSE_MB}MB cannot be shown</p>
<button onClick={download} className="btn btn--clicky inline-block">
<button onClick={() => download(false)} className="btn btn--clicky inline-block">
Save Response To File
</button>
</Fragment>
@@ -200,7 +200,7 @@ export const ResponseViewer = ({
<Fragment>
<p className="pad faint">Response over {LARGE_RESPONSE_MB}MB hidden for performance reasons</p>
<div>
<button onClick={download} className="btn btn--clicky margin-xs inline-block">
<button onClick={() => download(false)} className="btn btn--clicky margin-xs inline-block">
Save To File
</button>
<button