mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-08 14:35:03 +00:00
fix(bru-1447): json response formatting using fast-json-format library (#5932)
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
"dompurify": "^3.2.4",
|
||||
"escape-html": "^1.0.3",
|
||||
"fast-fuzzy": "^1.12.0",
|
||||
"fast-json-format": "~0.2.0",
|
||||
"file": "^0.2.2",
|
||||
"file-dialog": "^0.0.8",
|
||||
"file-saver": "^2.0.5",
|
||||
@@ -38,7 +39,6 @@
|
||||
"graphql-request": "^3.7.0",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"idb": "^7.0.0",
|
||||
"immer": "^9.0.15",
|
||||
"jsesc": "^3.0.2",
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { debounce } from 'lodash';
|
||||
import QueryResultFilter from './QueryResultFilter';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import iconv from 'iconv-lite';
|
||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||
import { getContentType, formatResponse } from 'utils/common';
|
||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||
import QueryResultPreview from './QueryResultPreview';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -13,64 +11,6 @@ import { useTheme } from 'providers/Theme/index';
|
||||
import { getEncoding, uuid } from 'utils/common/index';
|
||||
import LargeResponseWarning from '../LargeResponseWarning';
|
||||
|
||||
// Memory threshold to prevent crashes when decoding large buffers
|
||||
const LARGE_BUFFER_THRESHOLD = 50 * 1024 * 1024; // 50 MB
|
||||
|
||||
const applyJSONPathFilter = (data, filter) => {
|
||||
try {
|
||||
return JSONPath({ path: filter, json: data });
|
||||
} catch (e) {
|
||||
console.warn('Could not apply JSONPath filter:', e.message);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
const formatResponse = (data, dataBuffer, encoding, mode, filter) => {
|
||||
if (data === undefined || !dataBuffer || !mode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let bufferSize = 0;
|
||||
try {
|
||||
bufferSize = Buffer.from(dataBuffer, 'base64').length;
|
||||
} catch (error) {
|
||||
console.warn('Failed to calculate buffer size:', error);
|
||||
}
|
||||
|
||||
const isVeryLargeResponse = bufferSize > LARGE_BUFFER_THRESHOLD;
|
||||
|
||||
if (mode.includes('json')) {
|
||||
try {
|
||||
const prettyPrint = !isVeryLargeResponse;
|
||||
const processedData = filter ? applyJSONPathFilter(data, filter) : data;
|
||||
|
||||
return typeof processedData === 'string'
|
||||
? processedData
|
||||
: safeStringifyJSON(processedData, prettyPrint);
|
||||
} catch (error) {
|
||||
return typeof data === 'string' ? data : String(data);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode.includes('xml')) {
|
||||
if (isVeryLargeResponse) {
|
||||
return typeof data === 'string' ? data : safeStringifyJSON(data, false);
|
||||
}
|
||||
|
||||
let parsed = safeParseXML(data, { collapseContent: true });
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed;
|
||||
}
|
||||
return safeStringifyJSON(parsed, true);
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data, !isVeryLargeResponse);
|
||||
};
|
||||
|
||||
const formatErrorMessage = (error) => {
|
||||
if (!error) return 'Something went wrong';
|
||||
|
||||
@@ -116,7 +56,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListen
|
||||
if (isLargeResponse && !showLargeResponse) {
|
||||
return '';
|
||||
}
|
||||
return formatResponse(data, dataBuffer, responseEncoding, mode, filter);
|
||||
return formatResponse(data, dataBuffer, mode, filter);
|
||||
},
|
||||
[data, dataBuffer, responseEncoding, mode, filter, isLargeResponse, showLargeResponse]
|
||||
);
|
||||
|
||||
141
packages/bruno-app/src/utils/common/format-response.spec.js
Normal file
141
packages/bruno-app/src/utils/common/format-response.spec.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import { formatResponse } from './index';
|
||||
|
||||
describe('formatResponse', () => {
|
||||
const createBase64Buffer = (content) => Buffer.from(content).toString('base64');
|
||||
const createLargeBase64Buffer = (data) => {
|
||||
// Create buffer from the actual data without modification
|
||||
const content = typeof data === 'string' ? data : JSON.stringify(data);
|
||||
return Buffer.from(content).toString('base64');
|
||||
};
|
||||
|
||||
describe('invalid inputs', () => {
|
||||
it('should return empty string for invalid inputs', () => {
|
||||
const invalidCases = [
|
||||
[undefined, 'dGVzdA==', 'json'],
|
||||
[{ test: 'data' }, null, 'json'],
|
||||
[{ test: 'data' }, 'dGVzdA==', null],
|
||||
[undefined, undefined, undefined]
|
||||
];
|
||||
|
||||
invalidCases.forEach(([data, buffer, mode]) => {
|
||||
const result = formatResponse(data, buffer, mode);
|
||||
expect(result).toBe('');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON mode', () => {
|
||||
it('should format JSON data with JSONPath filter', () => {
|
||||
const data = { users: [{ name: 'John' }, { name: 'Jane' }] };
|
||||
const dataBuffer = createBase64Buffer(JSON.stringify(data));
|
||||
const result = formatResponse(data, dataBuffer, 'application/json', '$.users[0].name');
|
||||
|
||||
expect(result).toBe('[\n "John"\n]');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format normal sized JSON responses', () => {
|
||||
const data = { name: 'John', age: 30 };
|
||||
const dataBuffer = createBase64Buffer(JSON.stringify(data));
|
||||
const result = formatResponse(data, dataBuffer, 'application/json');
|
||||
|
||||
expect(result).toBe('{\n "name": "John",\n "age": 30\n}');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format normal sized JSON responses when data is already a JSON string', () => {
|
||||
const data = '{"name":"John","age":30}';
|
||||
const dataBuffer = createBase64Buffer(data); // Use data directly, not JSON.stringify(data)
|
||||
const result = formatResponse(data, dataBuffer, 'application/json');
|
||||
|
||||
expect(result).toBe('{\n "name": "John",\n "age": 30\n}');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should preserve bigint value after JSON format', () => {
|
||||
const data = '{ "data": 1736184243098437392 }';
|
||||
const dataBuffer = createBase64Buffer(data);
|
||||
const result = formatResponse(data, dataBuffer, 'application/json');
|
||||
|
||||
expect(result).toBe('{\n "data": 1736184243098437392\n}');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should format large JSON responses without indentation', () => {
|
||||
// This test uses a custom threshold of 100 bytes to trigger large buffer behavior
|
||||
const data = {
|
||||
test: 'value',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
content: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat'
|
||||
};
|
||||
const buffer = createLargeBase64Buffer(data);
|
||||
const result = formatResponse(data, buffer, 'application/json', undefined, 100);
|
||||
|
||||
// Since the data exceeds the 100 byte threshold, it should return unformatted JSON
|
||||
expect(result).toBe('{"test":"value","description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua","content":"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat"}');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('XML mode', () => {
|
||||
it('should format normal sized XML responses', () => {
|
||||
const xmlData = '<root><item>value</item></root>';
|
||||
const dataBuffer = createBase64Buffer(xmlData);
|
||||
const result = formatResponse(xmlData, dataBuffer, 'application/xml');
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('root');
|
||||
expect(result).toContain('item');
|
||||
});
|
||||
|
||||
it('should handle large XML responses', () => {
|
||||
const xmlData = '<root><item>value</item><description>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore</description><content>Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo</content></root>';
|
||||
const largeBuffer = createLargeBase64Buffer(xmlData);
|
||||
const result = formatResponse(xmlData, largeBuffer, 'application/xml', undefined, 100);
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('Lorem ipsum');
|
||||
});
|
||||
});
|
||||
|
||||
describe('other modes', () => {
|
||||
it('should handle string data for non-JSON/XML modes', () => {
|
||||
const data = 'plain text content';
|
||||
const dataBuffer = createBase64Buffer(data);
|
||||
const result = formatResponse(data, dataBuffer, 'text/plain');
|
||||
|
||||
expect(result).toBe('plain text content');
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
|
||||
it('should handle large object data for other modes', () => {
|
||||
const data = {
|
||||
message: 'hello',
|
||||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua',
|
||||
content: 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat'
|
||||
};
|
||||
const largeBuffer = createLargeBase64Buffer(data);
|
||||
const result = formatResponse(data, largeBuffer, 'text/plain', undefined, 100);
|
||||
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('Lorem ipsum');
|
||||
});
|
||||
});
|
||||
|
||||
describe('data type handling', () => {
|
||||
it('should handle different data types and always return string', () => {
|
||||
const testCases = [
|
||||
[123, createBase64Buffer('123'), 'application/json'],
|
||||
[true, createBase64Buffer('true'), 'application/json'],
|
||||
[null, createBase64Buffer('null'), 'application/json'],
|
||||
[[], createBase64Buffer('[]'), 'application/json']
|
||||
];
|
||||
|
||||
testCases.forEach(([data, buffer, mode]) => {
|
||||
const result = formatResponse(data, buffer, mode);
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import xmlFormat from 'xml-formatter';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { JSONPath } from 'jsonpath-plus';
|
||||
import fastJsonFormat from 'fast-json-format';
|
||||
|
||||
// a customized version of nanoid without using _ and -
|
||||
export const uuid = () => {
|
||||
@@ -277,4 +279,75 @@ export const sortByNameThenSequence = items => {
|
||||
|
||||
// return flattened sortedItems
|
||||
return sortedItems.flat();
|
||||
};
|
||||
};
|
||||
|
||||
// Memory threshold to prevent crashes when decoding large buffers
|
||||
const LARGE_BUFFER_THRESHOLD = 50 * 1024 * 1024; // 50 MB
|
||||
|
||||
const applyJSONPathFilter = (data, filter) => {
|
||||
try {
|
||||
return JSONPath({ path: filter, json: data });
|
||||
} catch (e) {
|
||||
console.warn('Could not apply JSONPath filter:', e.message);
|
||||
return data;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatResponse = (data, dataBufferString, mode, filter, bufferThreshold = LARGE_BUFFER_THRESHOLD) => {
|
||||
if (data === undefined || !dataBufferString || !mode) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let bufferSize = 0, rawData = '', isVeryLargeResponse = false;
|
||||
try {
|
||||
const dataBuffer = Buffer.from(dataBufferString, 'base64');
|
||||
bufferSize = dataBuffer.length;
|
||||
isVeryLargeResponse = bufferSize > bufferThreshold;
|
||||
if (!isVeryLargeResponse) {
|
||||
rawData = dataBuffer.toString();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to calculate buffer size:', error);
|
||||
}
|
||||
|
||||
if (mode.includes('json')) {
|
||||
try {
|
||||
if (filter) {
|
||||
return safeStringifyJSON(applyJSONPathFilter(data, filter), true);
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
if (isVeryLargeResponse) {
|
||||
return safeStringifyJSON(data, false);
|
||||
}
|
||||
|
||||
try {
|
||||
return fastJsonFormat(rawData);
|
||||
} catch (error) {}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
// Try to stringify the data, fallback to String conversion if needed
|
||||
const stringified = safeStringifyJSON(data, false);
|
||||
return typeof stringified === 'string' ? stringified : String(data);
|
||||
}
|
||||
|
||||
if (mode.includes('xml')) {
|
||||
if (isVeryLargeResponse) {
|
||||
return typeof data === 'string' ? data : safeStringifyJSON(data, false);
|
||||
}
|
||||
|
||||
let parsed = safeParseXML(data, { collapseContent: true });
|
||||
if (typeof parsed === 'string') {
|
||||
return parsed;
|
||||
}
|
||||
return safeStringifyJSON(parsed, true);
|
||||
}
|
||||
|
||||
if (typeof data === 'string') {
|
||||
return data;
|
||||
}
|
||||
|
||||
return safeStringifyJSON(data, !isVeryLargeResponse);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user