diff --git a/packages/insomnia-smoke-test/fixtures/response-download-collection.yaml b/packages/insomnia-smoke-test/fixtures/response-download-collection.yaml new file mode 100644 index 0000000000..53315dad79 --- /dev/null +++ b/packages/insomnia-smoke-test/fixtures/response-download-collection.yaml @@ -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 diff --git a/packages/insomnia-smoke-test/server/index.ts b/packages/insomnia-smoke-test/server/index.ts index cb681dce6e..99f643cc8d 100644 --- a/packages/insomnia-smoke-test/server/index.ts +++ b/packages/insomnia-smoke-test/server/index.ts @@ -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', diff --git a/packages/insomnia-smoke-test/tests/smoke/response-downloads.test.ts b/packages/insomnia-smoke-test/tests/smoke/response-downloads.test.ts new file mode 100644 index 0000000000..239733b297 --- /dev/null +++ b/packages/insomnia-smoke-test/tests/smoke/response-downloads.test.ts @@ -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/); + }); +}); diff --git a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx index 3fc14c8dc3..f5990e345c 100644 --- a/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx @@ -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 = ({ 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 = ({ 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 ( = ({ download, copyToClipboard }) => {shouldPrettifyOption && ( - + )} - + 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..77b71f0a23 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 @@ -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(); + }); + }); }); 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..3b5987e829 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,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) }); } diff --git a/packages/insomnia/src/ui/components/panes/response-pane.tsx b/packages/insomnia/src/ui/components/panes/response-pane.tsx index c8d3868ade..beac40d667 100644 --- a/packages/insomnia/src/ui/components/panes/response-pane.tsx +++ b/packages/insomnia/src/ui/components/panes/response-pane.tsx @@ -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 = ({ 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([]); diff --git a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx index 4c42bc46d2..d7340cef6a 100644 --- a/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-multipart-viewer.tsx @@ -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; diff --git a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx index 1c969f504e..a396f3d67b 100644 --- a/packages/insomnia/src/ui/components/viewers/response-viewer.tsx +++ b/packages/insomnia/src/ui/components/viewers/response-viewer.tsx @@ -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 ? (

Responses over {HUGE_RESPONSE_MB}MB cannot be shown

-
@@ -200,7 +200,7 @@ export const ResponseViewer = ({

Response over {LARGE_RESPONSE_MB}MB hidden for performance reasons

-