fix(bru-1447): json response formatting using fast-json-format library (#5932)

This commit is contained in:
lohit
2025-10-31 20:34:36 +05:30
committed by GitHub
parent e47d1ed353
commit 6826e98945
10 changed files with 398 additions and 150 deletions

View File

@@ -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",

View File

@@ -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]
);

View 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');
});
});
});
});

View File

@@ -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);
};