Fix/response pane optimizations (#6395)

* refactor: update content type detection to use base64 decoding

* fix: some styling issues and autofocus issues in input resolved

* refactor: enhance ResponsePane and QueryResult components for improved response handling and size display

* refactor: simplify size display logic in ResponseSize component

* refactor: improve size formatting logic in ResponseSize component for better readability

* refactor: enhance base64 decoding function to handle invalid input and improve error handling
This commit is contained in:
Abhishek S Lal
2025-12-15 19:32:57 +05:30
committed by GitHub
parent 71cf1a8f26
commit 014817810d
7 changed files with 90 additions and 48 deletions

View File

@@ -137,6 +137,7 @@ const CollectionProperties = ({ onClose }) => {
value={searchText || ''}
onChange={(e) => setSearchText(e.target.value)}
className="block textbox non-passphrase-input ml-auto font-normal"
autoFocus
/>
<button
type="submit"

View File

@@ -20,7 +20,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
// Check for parsing error
if (parsedData && typeof parsedData === 'object' && parsedData.error) {
return (
<ErrorAlert title="Cannot preview as XML" message={parsedData.error} />
<div className="px-2">
<ErrorAlert title="Cannot preview as XML" message={parsedData.error} />
</div>
);
}
@@ -34,7 +36,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
if (!isValidTreeData(parsedData)) {
return (
<ErrorAlert title="Cannot preview as XML" message="Data cannot be rendered as a tree. Expected a valid XML string." />
<div className="px-2">
<ErrorAlert title="Cannot preview as XML" message="Data cannot be rendered as a tree. Expected a valid XML string." />
</div>
);
}
@@ -50,7 +54,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
} else if (keys.length === 0) {
// Empty object with no children
return (
<ErrorAlert title="Cannot preview as XML" message="Cannot render XML tree. Root object is empty." />
<div className="px-2">
<ErrorAlert title="Cannot preview as XML" message="Cannot render XML tree. Root object is empty." />
</div>
);
}
}

View File

@@ -2,13 +2,11 @@ import { debounce } from 'lodash';
import { useTheme } from 'providers/Theme/index';
import React, { useMemo, useState } from 'react';
import { formatResponse, getContentType } from 'utils/common';
import { getEncoding } from 'utils/common/index';
import { getDefaultResponseFormat } from 'utils/response';
import { getDefaultResponseFormat, detectContentTypeFromBase64 } from 'utils/response';
import LargeResponseWarning from '../LargeResponseWarning';
import QueryResultFilter from './QueryResultFilter';
import QueryResultPreview from './QueryResultPreview';
import StyledWrapper from './StyledWrapper';
import { detectContentTypeFromBuffer } from 'utils/response/index';
const PREVIEW_FORMAT_OPTIONS = [
{
@@ -46,15 +44,7 @@ const formatErrorMessage = (error) => {
// Custom hook to determine the initial format and tab based on the data buffer and headers
export const useInitialResponseFormat = (dataBuffer, headers) => {
return useMemo(() => {
let buffer = null;
try {
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
} catch (error) {
console.error('Error converting dataBuffer to Buffer:', error);
buffer = null;
}
const detectedContentType = detectContentTypeFromBuffer(buffer);
const detectedContentType = detectContentTypeFromBase64(dataBuffer);
const contentType = getContentType(headers);
// Wait until both content types are available
@@ -70,15 +60,7 @@ export const useInitialResponseFormat = (dataBuffer, headers) => {
// Custom hook to determine preview format options based on content type
export const useResponsePreviewFormatOptions = (dataBuffer, headers) => {
return useMemo(() => {
let buffer = null;
try {
buffer = dataBuffer ? Buffer.from(dataBuffer, 'base64') : null;
} catch (error) {
console.error('Error converting dataBuffer to Buffer:', error);
buffer = null;
}
const detectedContentType = detectContentTypeFromBuffer(buffer);
const detectedContentType = detectContentTypeFromBase64(dataBuffer);
const contentType = getContentType(headers);
const byteFormatTypes = ['image', 'video', 'audio', 'pdf', 'zip'];
@@ -115,18 +97,9 @@ const QueryResult = ({
selectedFormat, // one of the options in PREVIEW_FORMAT_OPTIONS
selectedTab // 'editor' or 'preview'
}) => {
let buffer = null;
try {
buffer = Buffer.from(dataBuffer, 'base64'); // dataBuffer is already a base64 string, convert it to actual Buffer
} catch (error) {
console.error('Error converting dataBuffer to Buffer:', error);
buffer = null;
}
const detectedContentType = detectContentTypeFromBuffer(buffer);
const contentType = getContentType(headers);
const [filter, setFilter] = useState(null);
const [showLargeResponse, setShowLargeResponse] = useState(false);
const responseEncoding = getEncoding(headers);
const { displayedTheme } = useTheme();
const responseSize = useMemo(() => {
@@ -135,19 +108,19 @@ const QueryResult = ({
return response.size;
}
if (!dataBuffer) return 0;
try {
// dataBuffer is base64 encoded, so we need to calculate the actual size
const buffer = Buffer.from(dataBuffer, 'base64');
return buffer.length;
} catch (error) {
return 0;
// Fallback: estimate from base64 length (base64 is ~4/3 of original size)
if (dataBuffer && typeof dataBuffer === 'string') {
return Math.floor(dataBuffer.length * 0.75);
}
return 0;
}, [dataBuffer, item.response]);
const isLargeResponse = responseSize > 10 * 1024 * 1024; // 10 MB
const detectedContentType = useMemo(() => {
return detectContentTypeFromBase64(dataBuffer);
}, [dataBuffer, isLargeResponse]);
const formattedData = useMemo(
() => {
if (isLargeResponse && !showLargeResponse) {
@@ -155,7 +128,7 @@ const QueryResult = ({
}
return formatResponse(data, dataBuffer, selectedFormat, filter);
},
[data, dataBuffer, responseEncoding, selectedFormat, filter, isLargeResponse, showLargeResponse]
[data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]
);
const debouncedResultFilterOnChange = debounce((e) => {

View File

@@ -39,9 +39,11 @@ const ResponsePane = ({ item, collection }) => {
const [selectedTab, setSelectedTab] = useState('editor');
const rightContentRef = useRef(null);
// Initialize format and tab only once when data loads
const { initialFormat, initialTab } = useInitialResponseFormat(item.response?.dataBuffer, item.response?.headers);
const previewFormatOptions = useResponsePreviewFormatOptions(item.response?.dataBuffer, item.response?.headers);
const response = item.response || {};
// Initialize format and tab only once when data loads.
const { initialFormat, initialTab } = useInitialResponseFormat(response?.dataBuffer, response?.headers);
const previewFormatOptions = useResponsePreviewFormatOptions(response?.dataBuffer, response?.headers);
useEffect(() => {
if (initialFormat !== null && initialTab !== null) {
@@ -68,9 +70,6 @@ const ResponsePane = ({ item, collection }) => {
})
);
};
const response = item.response || {};
const responseSize = useMemo(() => {
if (typeof response.size === 'number') {
return response.size;

View File

@@ -34,6 +34,7 @@ const SearchInput = ({
spellCheck="false"
className="block w-full pl-7 py-2 rounded-md"
value={searchText}
autoFocus
onChange={handleChange}
{...props}
/>

View File

@@ -13,6 +13,7 @@ const CollectionSearch = ({ searchText, setSearchText }) => {
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
autoFocus
spellCheck="false"
value={searchText}
onChange={(e) => setSearchText(e.target.value.toLowerCase())}

View File

@@ -170,6 +170,47 @@ export const isValidHtmlSnippet = (snippet) => {
}
};
/**
* Decode only the first N bytes from a Base64 string
* Returns an empty buffer for invalid/missing input
*/
const decodeBase64Head = (base64, byteCount) => {
// Validate input is a non-empty string
if (!base64 || typeof base64 !== 'string') {
return Buffer.alloc(0);
}
try {
// Safely remove data URL prefix (e.g., "data:image/png;base64,")
const prefixMatch = base64.match(/^data:[^;]*;base64,/);
const cleanedBase64 = prefixMatch ? base64.slice(prefixMatch[0].length) : base64;
// Return empty buffer if nothing left after stripping prefix
if (!cleanedBase64) {
return Buffer.alloc(0);
}
// How many base64 chars needed to reconstruct "byteCount" bytes
const neededChars = Math.ceil(byteCount / 3) * 4;
// Slice only required chars
let slice = cleanedBase64.slice(0, neededChars);
// Sanitize: remove any non-base64 characters (whitespace, invalid chars)
slice = slice.replace(/[^A-Za-z0-9+/=]/g, '');
// Pad to valid base64 length (must be multiple of 4)
const padLength = (4 - (slice.length % 4)) % 4;
slice = slice + '='.repeat(padLength);
// Decode and trim to requested bytes
return Buffer.from(slice, 'base64').subarray(0, byteCount);
} catch (error) {
// On any decoding error, return an empty buffer
return Buffer.alloc(0);
}
};
/**
* Detects content type from buffer by checking magic numbers (file signatures)
* @param {Buffer} buffer - The data buffer to analyze
@@ -258,3 +299,23 @@ export const detectContentTypeFromBuffer = (buffer) => {
return null;
};
/**
* Main: detect from base64 string
*/
export const detectContentTypeFromBase64 = (base64) => {
if (!base64) return null;
// 1. Decode first 12 bytes (magic numbers)
const magicHead = decodeBase64Head(base64, 12);
const magicType = detectContentTypeFromBuffer(magicHead);
if (magicType) return magicType;
// 2. If not binary → decode up to 512 bytes for text detection
const textHead = decodeBase64Head(base64, 512);
if (isLikelyText(textHead)) return 'text/plain';
return null;
};