diff --git a/packages/bruno-app/src/components/Cookies/index.js b/packages/bruno-app/src/components/Cookies/index.js
index c5138bcbb..19d193945 100644
--- a/packages/bruno-app/src/components/Cookies/index.js
+++ b/packages/bruno-app/src/components/Cookies/index.js
@@ -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
/>
+
+
+
);
}
@@ -34,7 +36,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
if (!isValidTreeData(parsedData)) {
return (
-
+
+
+
);
}
@@ -50,7 +54,9 @@ export default function XmlPreview({ data, defaultExpanded = true }) {
} else if (keys.length === 0) {
// Empty object with no children
return (
-
+
+
+
);
}
}
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index 038205ed1..f6160d380 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -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) => {
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index 2459d82f9..6cdd5e9c2 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -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;
diff --git a/packages/bruno-app/src/components/SearchInput/index.js b/packages/bruno-app/src/components/SearchInput/index.js
index dd8a3d227..cb0a4555e 100644
--- a/packages/bruno-app/src/components/SearchInput/index.js
+++ b/packages/bruno-app/src/components/SearchInput/index.js
@@ -34,6 +34,7 @@ const SearchInput = ({
spellCheck="false"
className="block w-full pl-7 py-2 rounded-md"
value={searchText}
+ autoFocus
onChange={handleChange}
{...props}
/>
diff --git a/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js
index 21150c473..68e3d654a 100644
--- a/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js
+++ b/packages/bruno-app/src/components/Sidebar/Collections/CollectionSearch/index.js
@@ -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())}
diff --git a/packages/bruno-app/src/utils/response/index.js b/packages/bruno-app/src/utils/response/index.js
index c3f8fc666..08eafd8e6 100644
--- a/packages/bruno-app/src/utils/response/index.js
+++ b/packages/bruno-app/src/utils/response/index.js
@@ -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;
+};