+
{!item?.response ? (
- focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
+ focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
{
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
-
+
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item) {
item.requestState = 'received';
item.response = action.payload.response;
- item.cancelTokenUid = item.response.hasStreamRunning ? item.cancelTokenUid : null;
+ item.cancelTokenUid = item.response.stream?.running ? item.cancelTokenUid : null;
item.requestStartTime = null;
if (!collection.timeline) {
collection.timeline = [];
}
-
+
// Ensure timestamp is a number (milliseconds since epoch)
- const timestamp = item?.requestSent?.timestamp instanceof Date
- ? item.requestSent.timestamp.getTime()
+ const timestamp = item?.requestSent?.timestamp instanceof Date
+ ? item.requestSent.timestamp.getTime()
: item?.requestSent?.timestamp || Date.now();
// Append the new timeline entry with numeric timestamp
@@ -435,7 +436,7 @@ export const collectionsSlice = createSlice({
const { itemUid, collectionUid, eventType, eventData } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
-
+
const item = findItemInCollection(collection, itemUid);
if (!item) return;
const request = item.draft ? item.draft.request : item.request;
@@ -455,7 +456,7 @@ export const collectionsSlice = createSlice({
}
collection.timeline.push({
- type: 'request',
+ type: "request",
eventType: eventType, // Add the specific gRPC event type
collectionUid: collection.uid,
folderUid: null,
@@ -464,34 +465,36 @@ export const collectionsSlice = createSlice({
data: {
request: eventData || item.requestSent || item.request,
timestamp: Date.now(),
- eventData: eventData
+ eventData: eventData,
}
});
+
},
grpcResponseReceived: (state, action) => {
const { itemUid, collectionUid, eventType, eventData } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
-
+
if (!collection) return;
const item = findItemInCollection(collection, itemUid);
if (!item) return;
-
+
// Get current response state or create initial state
- const currentResponse = item.response || initiatedGrpcResponse;
+ const currentResponse = item.response || initiatedGrpcResponse
const timestamp = item?.requestSent?.timestamp;
let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) };
+
// Process based on event type
switch (eventType) {
case 'response':
const { error, res } = eventData;
-
+
// Handle error if present
if (error) {
const errorCode = error.code || 2; // Default to UNKNOWN if no code
-
+
updatedResponse.error = error.details || 'gRPC error occurred';
updatedResponse.statusCode = errorCode;
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
@@ -500,72 +503,72 @@ export const collectionsSlice = createSlice({
}
// Add response to list
- updatedResponse.responses = res
- ? [...(currentResponse?.responses || []), res]
+ updatedResponse.responses = res
+ ? [...(currentResponse?.responses || []), res]
: [...(currentResponse?.responses || [])];
break;
-
+
case 'metadata':
updatedResponse.headers = eventData.metadata;
updatedResponse.metadata = eventData.metadata;
break;
-
+
case 'status':
// Extract status info
const statusCode = eventData.status?.code;
const statusDetails = eventData.status?.details;
const statusMetadata = eventData.status?.metadata;
-
+
// Set status based on actual code and details
updatedResponse.statusCode = statusCode;
updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN';
updatedResponse.statusDescription = statusDetails;
updatedResponse.statusDetails = eventData.status;
-
+
// Store trailers (status metadata)
if (statusMetadata) {
updatedResponse.trailers = statusMetadata;
}
-
+
// Handle error status (non-zero code)
if (statusCode !== 0) {
updatedResponse.isError = true;
updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`;
}
-
+
break;
-
+
case 'error':
// Extract error details
const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code
const errorDetails = eventData.error?.details || eventData.error?.message;
const errorMetadata = eventData.error?.metadata;
-
+
updatedResponse.isError = true;
updatedResponse.error = errorDetails || 'Unknown gRPC error';
updatedResponse.statusCode = errorCode;
updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN';
updatedResponse.statusDescription = errorDetails;
-
+
// Store error metadata as trailers if present
if (errorMetadata) {
updatedResponse.trailers = errorMetadata;
}
-
+
break;
-
+
case 'end':
- state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
+ state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
break;
-
+
case 'cancel':
updatedResponse.statusCode = 1; // CANCELLED
updatedResponse.statusText = 'CANCELLED';
updatedResponse.statusDescription = 'Stream cancelled by client or server';
- state.activeConnections = state.activeConnections.filter((id) => id !== itemUid);
+ state.activeConnections = state.activeConnections.filter(id => id !== itemUid);
break;
}
-
+
item.requestState = 'received';
item.response = updatedResponse;
@@ -576,7 +579,7 @@ export const collectionsSlice = createSlice({
// Append the new timeline entry with specific gRPC event type
collection.timeline.push({
- type: 'request',
+ type: "request",
eventType: eventType, // Add the specific gRPC event type
collectionUid: collection.uid,
folderUid: null,
@@ -586,7 +589,7 @@ export const collectionsSlice = createSlice({
request: item.requestSent || item.request,
response: updatedResponse,
eventData: eventData, // Store the original event data
- timestamp: Date.now()
+ timestamp: Date.now(),
}
});
},
@@ -596,12 +599,11 @@ export const collectionsSlice = createSlice({
if (collection) {
const item = findItemInCollection(collection, action.payload.itemUid);
if (item) {
- if (item.response && item.response.hasStreamRunning) {
+ if (item.response && item.response.stream?.running) {
item.response.data = '';
item.response.size = 0;
return;
}
-
item.response = null;
}
}
@@ -928,7 +930,7 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
- const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || [];
+ const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || [];
const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({
uid: uuid(),
name,
@@ -942,7 +944,9 @@ export const collectionsSlice = createSlice({
// Update the request URL to reflect the new query params
const parts = splitOnFirst(item.draft.request.url, '?');
- const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
+ const query = stringifyQueryParams(
+ filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
+ );
// If there are enabled query params, append them to the URL
if (query && query.length) {
@@ -1173,7 +1177,7 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
- item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({
+ item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({
uid: uuid(),
name: name,
value: value,
@@ -1215,8 +1219,8 @@ export const collectionsSlice = createSlice({
if (!folder || !isItemAFolder(folder)) {
return;
}
-
- folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({
+
+ folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({
uid: uuid(),
name: name,
value: value,
@@ -1497,7 +1501,7 @@ export const collectionsSlice = createSlice({
if (!item.draft) {
item.draft = cloneDeep(item);
}
-
+
switch (item.draft.request.body.mode) {
case 'json': {
item.draft.request.body.json = action.payload.content;
@@ -1634,7 +1638,7 @@ export const collectionsSlice = createSlice({
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
if (collection) {
- const item = findItemInCollection(collection, action.payload.itemUid);
+ const item = findItemInCollection(collection, action.payload.itemUid);
if (item && isItemARequest(item)) {
if (!item.draft) {
@@ -1885,7 +1889,7 @@ export const collectionsSlice = createSlice({
break;
case 'ntlm':
set(collection, 'draft.root.request.auth.ntlm', action.payload.content);
- break;
+ break;
case 'oauth2':
set(collection, 'draft.root.request.auth.oauth2', action.payload.content);
break;
@@ -2614,7 +2618,7 @@ export const collectionsSlice = createSlice({
const { requestUid, itemUid, collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
if (!collection) return;
-
+
const item = findItemInCollection(collection, itemUid);
if (!item) return;
@@ -2647,7 +2651,7 @@ export const collectionsSlice = createSlice({
item.postResponseScriptErrorMessage = action.payload.errorMessage;
}
- if (type === 'test-script-execution') {
+ if(type === 'test-script-execution') {
item.testScriptErrorMessage = action.payload.errorMessage;
}
@@ -2662,7 +2666,7 @@ export const collectionsSlice = createSlice({
if (type === 'request-sent') {
const { cancelTokenUid, requestSent } = action.payload;
item.requestSent = requestSent;
-
+
// sometimes the response is received before the request-sent event arrives
if (item.requestState === 'queued') {
item.requestState = 'sending';
@@ -2679,12 +2683,12 @@ export const collectionsSlice = createSlice({
const { results } = action.payload;
item.testResults = results;
}
-
+
if (type === 'test-results-pre-request') {
const { results } = action.payload;
item.preRequestTestResults = results;
}
-
+
if (type === 'test-results-post-response') {
const { results } = action.payload;
item.postResponseTestResults = results;
@@ -2798,7 +2802,7 @@ export const collectionsSlice = createSlice({
if (collection) {
collection.runnerResult = null;
- collection.runnerTags = { include: [], exclude: [] };
+ collection.runnerTags = { include: [], exclude: [] }
collection.runnerTagsEnabled = false;
collection.runnerConfiguration = null;
}
@@ -2937,7 +2941,7 @@ export const collectionsSlice = createSlice({
updateFolderAuthMode: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
-
+
if (folder) {
if (!folder.draft) {
folder.draft = cloneDeep(folder.root);
@@ -2952,7 +2956,16 @@ export const collectionsSlice = createSlice({
if (collection) {
const item = findItemInCollection(collection, itemUid);
- item.response.data = data.data + (item.response.data || '');
+ if (data.data) {
+ item.response.data ||= [];
+ item.response.data = [{
+ type: 'incoming',
+ message: data.data,
+ messageHexdump: hexdump(data.data),
+ timestamp: Date.now()
+ }].concat(item.response.data);
+ }
+ item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
item.response.size = data.data?.length + (item.response.size || 0);
}
},
@@ -2997,7 +3010,7 @@ export const collectionsSlice = createSlice({
updateCollectionTagsList: (state, action) => {
const { collectionUid } = action.payload;
const collection = findCollectionByUid(state.collections, collectionUid);
-
+
if (collection) {
collection.allTags = getUniqueTagsFromItems(collection.items);
}
diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js
index 8b191b0a6..e94fd8be6 100644
--- a/packages/bruno-app/src/utils/common/index.js
+++ b/packages/bruno-app/src/utils/common/index.js
@@ -2,6 +2,7 @@ import { customAlphabet } from 'nanoid';
import xmlFormat from 'xml-formatter';
import { JSONPath } from 'jsonpath-plus';
import fastJsonFormat from 'fast-json-format';
+import { format, applyEdits } from 'jsonc-parser';
import { patternHasher } from '@usebruno/common/utils';
// a customized version of nanoid without using _ and -
@@ -294,7 +295,7 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
}
try {
- return prettifyJsonString(rawData);
+ return fastJsonFormat(rawData);
} catch (error) {}
if (typeof data === 'string') {
@@ -326,9 +327,11 @@ export const formatResponse = (data, dataBufferString, mode, filter, bufferThres
export const prettifyJsonString = (jsonDataString) => {
if (typeof jsonDataString !== 'string') return jsonDataString;
+
try {
const { hashed, restore } = patternHasher(jsonDataString);
- const formattedJsonDataStringHashed = fastJsonFormat(hashed);
+ const edits = format(hashed, undefined, { tabSize: 2, insertSpaces: true });
+ const formattedJsonDataStringHashed = applyEdits(hashed, edits);
const formattedJsonDataString = restore(formattedJsonDataStringHashed);
return formattedJsonDataString;
} catch (error) {
diff --git a/packages/bruno-app/src/utils/common/index.spec.js b/packages/bruno-app/src/utils/common/index.spec.js
index 8ed85932e..55958b954 100644
--- a/packages/bruno-app/src/utils/common/index.spec.js
+++ b/packages/bruno-app/src/utils/common/index.spec.js
@@ -218,16 +218,16 @@ describe('common utils', () => {
});
test('should format complex json string', () => {
- const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with "quotes" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
+ const input = `{"id": 123456789123456789123456789,"name": "Test 'JSON' Data with \"quotes\" — Pretty Print ","active": true,"price": 199.9999999,"decimals": 1.00,"nullValue": null,"unicodeText": "こんにちは世界 ","escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style","nestedObject": { "level1": { "level2": { "emptyArray": [], "specialChars": "@#$%^&*()_+-=[]{}|;':,./<>?~", "booleanValues": [ true, false, true ], "numbers": [ 0, -1, 1.23e10, 3.1415926535 ] } }},"mixedArray": [ "string with 'apostrophe'", 42, false, null, { "innerObj": { "keyWithQuotes": "value containing \`backticks\` and 'single quotes'", "nestedArray": [ { "a": "O'Reilly" }{ "b": "'inline code'" }, [ "deep", "array", { "c": "contains 'quotes'" } ] ] } }],"nonStringVariable": {{nonStringVar}},"withBrunoVariable": "{{string}} '{{with}}' "{{variety}}" of '{{variables}}'","dateExample": "2025-11-07T12:34:56Z","regexExample": "^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$","urls": { "website": "https://example.com?param='value'&flag='true'", "escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"},"multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "}`;
const expectedOutput = `{
"id": 123456789123456789123456789,
- "name": "Test 'JSON' Data with "quotes" — Pretty Print ",
+ "name": "Test 'JSON' Data with \"quotes\" — Pretty Print ",
"active": true,
"price": 199.9999999,
"decimals": 1.00,
"nullValue": null,
"unicodeText": "こんにちは世界 ",
- "escapedCharacters": "Line1\nLine2\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
+ "escapedCharacters": "Line1\\nLine2\\tTabbed\"Quoted\" and 'single quoted' with 'code' style",
"nestedObject": {
"level1": {
"level2": {
@@ -280,7 +280,7 @@ describe('common utils', () => {
"website": "https://example.com?param='value'&flag='true'",
"escapedURL": "https:\/\/escaped-url.com\/path\?q='search'\&debug='on'"
},
- "multiLineString": "This is a long text\nthat spans multiple\nlines with \`backticks\` 'quotes' and 'code' snippets "
+ "multiLineString": "This is a long text\\nthat spans multiple\\nlines with \`backticks\` 'quotes' and 'code' snippets "
}`;
expect(prettifyJsonString(input)).toBe(expectedOutput);
});
diff --git a/packages/bruno-app/src/utils/curl/content-type.js b/packages/bruno-app/src/utils/curl/content-type.js
new file mode 100644
index 000000000..0a1e54610
--- /dev/null
+++ b/packages/bruno-app/src/utils/curl/content-type.js
@@ -0,0 +1,29 @@
+const normalizeContentType = (contentType) => {
+ if (!contentType || typeof contentType !== 'string') {
+ return '';
+ }
+
+ return contentType.toLowerCase();
+};
+
+export const isJsonLikeContentType = (contentType) => {
+ const normalized = normalizeContentType(contentType);
+
+ return normalized.includes('application/json') || normalized.includes('+json');
+};
+
+export const isXmlLikeContentType = (contentType) => {
+ const normalized = normalizeContentType(contentType);
+
+ return normalized.includes('application/xml') || normalized.includes('+xml') || normalized.includes('text/xml');
+};
+
+export const isPlainTextContentType = (contentType) => {
+ const normalized = normalizeContentType(contentType);
+
+ return normalized.includes('text/plain');
+};
+
+export const isStructuredContentType = (contentType) => {
+ return isJsonLikeContentType(contentType) || isXmlLikeContentType(contentType) || isPlainTextContentType(contentType);
+};
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js
index 21daf8283..24269f9a9 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.js
@@ -10,6 +10,7 @@ import parseCurlCommand from './parse-curl';
import * as querystring from 'query-string';
import * as jsesc from 'jsesc';
import { buildQueryString } from '@usebruno/common/utils';
+import { isStructuredContentType } from './content-type';
function getContentType(headers = {}) {
const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type');
@@ -34,7 +35,7 @@ function getDataString(request) {
const contentType = getContentType(request.headers);
- if (contentType && (contentType.includes('application/json') || contentType.includes('application/xml') || contentType.includes('text/plain'))) {
+ if (isStructuredContentType(contentType)) {
return { data: request.data };
}
diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
index 058064391..c4133a3e0 100644
--- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
+++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js
@@ -120,4 +120,37 @@ describe('curlToJson', () => {
]
});
});
+
+ it('should parse custom json content-types', () => {
+ const curlCommand = `curl 'https://api.example.com/test'
+ -H 'content-type: application/x.custom+json;version=1'
+ --data-raw '{"test":"data"}'
+ `;
+
+ const result = curlToJson(curlCommand);
+
+ expect(result).toEqual({
+ url: 'https://api.example.com/test',
+ raw_url: 'https://api.example.com/test',
+ method: 'post',
+ headers: {
+ 'content-type': 'application/x.custom+json;version=1'
+ },
+ data: '{"test":"data"}'
+ });
+ });
+
+ it('should parse vendor tree json content-types', () => {
+ const curlCommand = `curl --request POST \\
+ --url https://api.example.com/orders/42/preferences \\
+ --header 'accept: */*' \\
+ --header 'content-type: application/vnd.vendor+json' \\
+ --data '{\\n "data": {\\n "type": "order-preferences",\\n "attributes": {\\n "notes": "Leave at door",\\n "priority": true\\n }\\n }\\n}'`;
+
+ const result = curlToJson(curlCommand);
+ expect(result.data).toContain('"type": "order-preferences"');
+ expect(result.data).toContain('"notes": "Leave at door"');
+ expect(result.data).toContain('"priority": true');
+ expect(result.headers['content-type']).toBe('application/vnd.vendor+json');
+ });
});
diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js
index 3fa30a95f..866df7b32 100644
--- a/packages/bruno-app/src/utils/curl/index.js
+++ b/packages/bruno-app/src/utils/curl/index.js
@@ -1,6 +1,7 @@
import { forOwn } from 'lodash';
import curlToJson from './curl-to-json';
import { prettifyJsonString } from 'utils/common/index';
+import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type';
export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => {
const parseFormData = (parsedBody) => {
@@ -59,25 +60,27 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque
};
if (parsedBody && contentType && typeof contentType === 'string') {
- if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) {
+ const normalizedContentType = contentType.toLowerCase();
+
+ if (requestType === 'graphql-request' && (isJsonLikeContentType(contentType) || normalizedContentType.includes('application/graphql'))) {
body.mode = 'graphql';
body.graphql = parseGraphQL(parsedBody);
} else if (requestType === 'http-request' && request.isDataBinary) {
body.mode = 'file';
body.file = parsedBody;
- }else if (contentType.includes('application/json')) {
+ } else if (isJsonLikeContentType(contentType)) {
body.mode = 'json';
body.json = prettifyJsonString(parsedBody);
- } else if (contentType.includes('xml')) {
+ } else if (isXmlLikeContentType(contentType) || normalizedContentType.includes('xml')) {
body.mode = 'xml';
body.xml = parsedBody;
- } else if (contentType.includes('application/x-www-form-urlencoded')) {
+ } else if (normalizedContentType.includes('application/x-www-form-urlencoded')) {
body.mode = 'formUrlEncoded';
body.formUrlEncoded = parseFormData(parsedBody);
- } else if (contentType.includes('multipart/form-data')) {
+ } else if (normalizedContentType.includes('multipart/form-data')) {
body.mode = 'multipartForm';
body.multipartForm = parsedBody;
- } else if (contentType.includes('text/plain')) {
+ } else if (isPlainTextContentType(contentType)) {
body.mode = 'text';
body.text = parsedBody;
}
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index c788a1a93..f5cfd7ba4 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -10,6 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
if (response?.error) {
resolve(response)
}
+
resolve({
state: 'success',
data: response.data,
@@ -21,7 +22,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
statusText: response.statusText,
duration: response.duration,
timeline: response.timeline,
- hasStreamRunning: response.hasStreamRunning
+ stream: response.stream
});
})
.catch((err) => reject(err));
@@ -32,17 +33,19 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
startGrpcRequest(item, collection, environment, runtimeVariables)
- .then((initialState) => {
- // Return an initial state object to update the UI
- // The real response data will be handled by event listeners
- resolve({
- ...initialState,
- timeline: []
- });
- })
- .catch((err) => reject(err));
+ .then((initialState) => {
+ // Return an initial state object to update the UI
+ // The real response data will be handled by event listeners
+ resolve({
+ ...initialState,
+ timeline: []
+ });
+ })
+ .catch((err) => reject(err));
});
-};
+}
+
+
const sendHttpRequest = async (item, collection, environment, runtimeVariables) => {
return new Promise((resolve, reject) => {
@@ -82,19 +85,19 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
const request = item.draft ? item.draft : item;
-
+
ipcRenderer.invoke('grpc:start-connection', {
- request,
- collection,
- environment,
+ request,
+ collection,
+ environment,
runtimeVariables
})
- .then(() => {
- resolve();
- })
- .catch((err) => {
- reject(err);
- });
+ .then(() => {
+ resolve();
+ })
+ .catch(err => {
+ reject(err);
+ });
});
};
@@ -187,7 +190,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('grpc:is-connection-active', connectionId)
- .then((response) => {
+ .then(response => {
if (response.success) {
resolve(response.isActive);
} else {
@@ -196,7 +199,7 @@ export const isGrpcConnectionActive = async (connectionId) => {
resolve(false);
}
})
- .catch((err) => {
+ .catch(err => {
console.error('Failed to check connection status:', err);
// On error, assume the connection is not active
resolve(false);
@@ -214,14 +217,14 @@ export const isGrpcConnectionActive = async (connectionId) => {
export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
-
- ipcRenderer.invoke('grpc:generate-sample-message', {
- methodPath,
- existingMessage,
- options
+
+ ipcRenderer.invoke('grpc:generate-sample-message', {
+ methodPath,
+ existingMessage,
+ options
})
- .then(resolve)
- .catch(reject);
+ .then(resolve)
+ .catch(reject);
});
};
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 14956c035..4fa7c2274 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -69,8 +69,9 @@ const getJsSandboxRuntime = (collection) => {
return 'vm2';
};
-const isStream = (headers) => {
- return headers.get('content-type') === 'text/event-stream';
+const hasStreamHeaders = (headers) => {
+ const headerSplit = (headers.get('content-type') ?? '').split(';').map((d) => d.trim());
+ return headerSplit.indexOf('text/event-stream') > -1;
};
const promisifyStream = async (stream, abortController, closeOnFirst) => {
@@ -95,18 +96,20 @@ const promisifyStream = async (stream, abortController, closeOnFirst) => {
});
stream.on('close', doResolve);
- stream.on('error', err => reject(err));
+ stream.on('error', (err) => reject(err));
});
};
-const configureRequest = async (collectionUid,
+const configureRequest = async (
+ collectionUid,
collection,
request,
envVars,
runtimeVariables,
processEnvVars,
collectionPath,
- globalEnvironmentVariables) => {
+ globalEnvironmentVariables
+) => {
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
if (!protocolRegex.test(request.url)) {
request.url = `http://${request.url}`;
@@ -125,7 +128,7 @@ const configureRequest = async (collectionUid,
// Get followRedirects setting, default to true for backward compatibility
const followRedirects = request.settings?.followRedirects ?? true;
-
+
// Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;
@@ -166,12 +169,14 @@ const configureRequest = async (collectionUid,
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
if (tokenPlacement == 'header' && credentials?.access_token) {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim();
- } else {
+ }
+ else {
try {
const url = new URL(request.url);
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
request.url = url?.toString();
- } catch (error) {}
+ }
+ catch(error) {}
}
break;
case 'implicit':
@@ -180,7 +185,8 @@ const configureRequest = async (collectionUid,
request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid };
if (tokenPlacement == 'header') {
request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`;
- } else {
+ }
+ else {
try {
const url = new URL(request.url);
url?.searchParams?.set(tokenQueryKey, credentials?.access_token);
@@ -242,7 +248,8 @@ const configureRequest = async (collectionUid,
if (preferencesUtil.shouldSendCookies()) {
const cookieString = getCookieStringForUrl(request.url);
if (cookieString && typeof cookieString === 'string' && cookieString.length) {
- const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie'
+ const existingCookieHeaderName = Object.keys(request.headers).find(
+ name => name.toLowerCase() === 'cookie'
);
const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : '';
@@ -306,7 +313,8 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
// Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars
const processEnvVars = getProcessEnvVars(collection.uid);
- const resolvedVars = merge({},
+ const resolvedVars = merge(
+ {},
globalEnvironmentVars,
collectionVariables,
envVars,
@@ -319,7 +327,8 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
...processEnvVars
}
}
- });
+ }
+ );
const collectionRoot = collection?.draft?.root || collection?.root || {};
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
@@ -336,14 +345,16 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
const collectionPath = collection.pathname;
- const axiosInstance = await configureRequest(collection.uid,
+ const axiosInstance = await configureRequest(
+ collection.uid,
collection,
request,
envVars,
collection.runtimeVariables,
processEnvVars,
collectionPath,
- collection.globalEnvironmentVariables);
+ collection.globalEnvironmentVariables
+ );
const response = await axiosInstance(request);
@@ -378,10 +389,10 @@ const registerNetworkIpc = (mainWindow) => {
};
const notifyScriptExecution = ({
- channel, // 'main:run-request-event' | 'main:run-folder-event'
- basePayload, // request-level or runner-level identifiers
- scriptType, // 'pre-request' | 'post-response' | 'test'
- error // optional Error
+ channel, // 'main:run-request-event' | 'main:run-folder-event'
+ basePayload, // request-level or runner-level identifiers
+ scriptType, // 'pre-request' | 'post-response' | 'test'
+ error // optional Error
}) => {
mainWindow.webContents.send(channel, {
type: `${scriptType}-script-execution`,
@@ -390,7 +401,8 @@ const registerNetworkIpc = (mainWindow) => {
});
};
- const runPreRequest = async (request,
+ const runPreRequest = async (
+ request,
requestUid,
envVars,
collectionPath,
@@ -399,10 +411,11 @@ const registerNetworkIpc = (mainWindow) => {
runtimeVariables,
processEnvVars,
scriptingConfig,
- runRequestByItemPathname) => {
+ runRequestByItemPathname
+ ) => {
// run pre-request script
let scriptResult;
- const collectionName = collection?.name;
+ const collectionName = collection?.name
const requestScript = get(request, 'script.req');
if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
@@ -480,7 +493,8 @@ const registerNetworkIpc = (mainWindow) => {
return scriptResult;
};
- const runPostResponse = async (request,
+ const runPostResponse = async (
+ request,
response,
requestUid,
envVars,
@@ -537,7 +551,7 @@ const registerNetworkIpc = (mainWindow) => {
// run post-response script
const responseScript = get(request, 'script.res');
let scriptResult;
- const collectionName = collection?.name;
+ const collectionName = collection?.name
if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runResponseScript(
@@ -612,9 +626,11 @@ const registerNetworkIpc = (mainWindow) => {
const abortController = new AbortController();
const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'standalone';
- request.responseType = "stream";
+ request.responseType = 'stream';
+ // flag to see if the stream needs to be handled as an actual stream or
+ // is it just a data stream from axios
+ let isResponseStream = false;
const brunoConfig = getBrunoConfig(collectionUid, collection);
-
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
@@ -625,7 +641,8 @@ const registerNetworkIpc = (mainWindow) => {
let preRequestScriptResult = null;
let preRequestError = null;
try {
- preRequestScriptResult = await runPreRequest(request,
+ preRequestScriptResult = await runPreRequest(
+ request,
requestUid,
envVars,
collectionPath,
@@ -634,7 +651,8 @@ const registerNetworkIpc = (mainWindow) => {
runtimeVariables,
processEnvVars,
scriptingConfig,
- runRequestByItemPathname);
+ runRequestByItemPathname
+ );
} catch (error) {
preRequestError = error;
}
@@ -659,14 +677,16 @@ const registerNetworkIpc = (mainWindow) => {
if (preRequestError) {
return Promise.reject(preRequestError);
}
- const axiosInstance = await configureRequest(collectionUid,
+ const axiosInstance = await configureRequest(
+ collectionUid,
collection,
request,
envVars,
runtimeVariables,
processEnvVars,
collectionPath,
- collection.globalEnvironmentVariables);
+ collection.globalEnvironmentVariables
+ );
const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
@@ -685,7 +705,7 @@ const registerNetworkIpc = (mainWindow) => {
data: requestData,
dataBuffer: requestDataBuffer,
timestamp: Date.now()
- };
+ }
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
type: 'request-sent',
@@ -707,13 +727,13 @@ const registerNetworkIpc = (mainWindow) => {
});
}
- let response, responseTime;
+ let response, responseTime, axiosDataStream;
try {
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
- request.isStream = isStream(response.headers);
+ isResponseStream = hasStreamHeaders(response.headers);
- if (!request.isStream) {
+ if (!isResponseStream) {
response.data = await promisifyStream(response.data);
}
@@ -740,9 +760,8 @@ const registerNetworkIpc = (mainWindow) => {
// Prevents the duration on leaking to the actual result
responseTime = response.headers.get('request-duration');
response.headers.delete('request-duration');
-
- request.isStream = isStream(response.headers);
- if (!request.isStream) {
+ isResponseStream = hasStreamHeaders(response.headers);
+ if (!isResponseStream) {
response.data = await promisifyStream(response.data);
}
} else {
@@ -755,21 +774,21 @@ const registerNetworkIpc = (mainWindow) => {
statusText: error.statusText,
error: error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST,
timeline: error.timeline
- };
+ }
}
}
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
-
- if (request.isStream) {
- response.stream = response.data;
+ if (isResponseStream) {
+ axiosDataStream = response.data;
}
- const { data, dataBuffer } = request.isStream
- ? { data: '', dataBuffer: new ArrayBuffer(0) }
+ const { data, dataBuffer } = isResponseStream
+ ? { data: '', dataBuffer: Buffer.alloc(0) }
: parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
-
response.data = data;
+ response.dataBuffer = dataBuffer;
+
response.responseTime = responseTime;
// save cookies
@@ -783,9 +802,9 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
cookiesStore.saveCookieJar();
- let postResponseScriptResult = null;
- let postResponseError = null;
const runPostScripts = async () => {
+ let postResponseScriptResult = null;
+ let postResponseError = null;
try {
postResponseScriptResult = await runPostResponse(request,
response,
@@ -914,9 +933,8 @@ const registerNetworkIpc = (mainWindow) => {
cookiesStore.saveCookieJar();
}
};
-
- if (request.isStream) {
- response.stream.on('close', () => runPostScripts().then());
+ if (isResponseStream) {
+ axiosDataStream.on('close', () => runPostScripts().then());
} else {
await runPostScripts();
}
@@ -926,8 +944,10 @@ const registerNetworkIpc = (mainWindow) => {
statusText: response.statusText,
headers: response.headers,
data: response.data,
- stream: request.isStream ? response.stream : null,
+ dataBuffer: response.dataBuffer.toString('base64'),
+ stream: isResponseStream ? axiosDataStream : null,
cancelTokenUid: cancelTokenUid,
+ size: Buffer.byteLength(response.dataBuffer),
duration: responseTime ?? 0,
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
timeline: response.timeline
@@ -953,12 +973,11 @@ const registerNetworkIpc = (mainWindow) => {
const response = await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false });
if (response.stream) {
const stream = response.stream;
- response.stream = undefined;
- response.hasStreamRunning = response.status >= 200 && response.status < 300;
+ response.stream = { running: response.status >= 200 && response.status < 300 };
- stream.on('data', newData => {
+ stream.on('data', (newData) => {
const parsed = parseDataFromResponse({ data: newData, headers: {} });
- mainWindow.webContents.send('main:http-stream-new-data', {collectionUid, itemUid: item.uid, data: parsed});
+ mainWindow.webContents.send('main:http-stream-new-data', { collectionUid, itemUid: item.uid, data: parsed });
});
stream.on('close', () => {
@@ -966,7 +985,7 @@ const registerNetworkIpc = (mainWindow) => {
return;
}
- mainWindow.webContents.send('main:http-stream-end', {collectionUid, itemUid: item.uid});
+ mainWindow.webContents.send('main:http-stream-end', { collectionUid, itemUid: item.uid });
deleteCancelToken(response.cancelTokenUid);
});
}
@@ -990,7 +1009,7 @@ const registerNetworkIpc = (mainWindow) => {
if (cancelTokenUid && cancelTokens[cancelTokenUid]) {
const abortController = cancelTokens[cancelTokenUid];
deleteCancelToken(cancelTokenUid);
- abortController.abort(); // Ensure the on stream end event is called after the token is deleted
+ abortController.abort();
resolve();
} else {
reject(new Error('cancel token not found'));
@@ -999,7 +1018,7 @@ const registerNetworkIpc = (mainWindow) => {
});
// handler for fetch-gql-schema
- ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler);
+ ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler)
ipcMain.handle(
'renderer:run-collection-folder',
@@ -1033,7 +1052,7 @@ const registerNetworkIpc = (mainWindow) => {
}
const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname));
if(_item) {
- const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
+ const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true });
resolve(res);
}
reject(`bru.runRequest: invalid request path - ${itemPathname}`);
@@ -1065,8 +1084,9 @@ const registerNetworkIpc = (mainWindow) => {
}
});
+
// sort requests by seq property
- folderRequests = sortByNameThenSequence(folderRequests);
+ folderRequests = sortByNameThenSequence(folderRequests)
}
// Filter requests based on tags
@@ -1075,7 +1095,7 @@ const registerNetworkIpc = (mainWindow) => {
const excludeTags = tags.exclude ? tags.exclude : [];
folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => {
requestTags = draft?.tags || requestTags || [];
- return isRequestTagsIncluded(requestTags, includeTags, excludeTags);
+ return isRequestTagsIncluded(requestTags, includeTags, excludeTags)
});
}
@@ -1128,14 +1148,15 @@ const registerNetworkIpc = (mainWindow) => {
const request = await prepareRequest(item, collection, abortController);
request.__bruno__executionMode = 'runner';
-
+
const requestUid = uuid();
try {
let preRequestScriptResult;
let preRequestError = null;
try {
- preRequestScriptResult = await runPreRequest(request,
+ preRequestScriptResult = await runPreRequest(
+ request,
requestUid,
envVars,
collectionPath,
@@ -1144,7 +1165,8 @@ const registerNetworkIpc = (mainWindow) => {
runtimeVariables,
processEnvVars,
scriptingConfig,
- runRequestByItemPathname);
+ runRequestByItemPathname
+ );
} catch (error) {
console.error('Pre-request script error:', error);
preRequestError = error;
@@ -1214,7 +1236,7 @@ const registerNetworkIpc = (mainWindow) => {
data: requestData,
dataBuffer: requestDataBuffer,
timestamp: Date.now()
- };
+ }
// todo:
// i have no clue why electron can't send the request object
@@ -1228,8 +1250,8 @@ const registerNetworkIpc = (mainWindow) => {
currentAbortController = new AbortController();
request.signal = currentAbortController.signal;
request.responseType = 'stream';
-
- const axiosInstance = await configureRequest(collectionUid,
+ const axiosInstance = await configureRequest(
+ collectionUid,
collection,
request,
envVars,
@@ -1246,7 +1268,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid,
credentialsId: request?.oauth2Credentials?.credentialsId,
...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }),
- debugInfo: request?.oauth2Credentials?.debugInfo
+ debugInfo: request?.oauth2Credentials?.debugInfo,
});
collection.oauth2Credentials = updateCollectionOauth2Credentials({
@@ -1274,10 +1296,7 @@ const registerNetworkIpc = (mainWindow) => {
/** @type {import('axios').AxiosResponse} */
response = await axiosInstance(request);
-
- request.isStream = isStream(response.headers);
- response.data = await promisifyStream(response.data, currentAbortController, true);
-
+ response.data = await promisifyStream(response.data, currentAbortController, false);
timeEnd = Date.now();
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
@@ -1319,9 +1338,7 @@ const registerNetworkIpc = (mainWindow) => {
}
if (error?.response) {
- request.isStream = isStream(error.response.headers);
error.response.data = await promisifyStream(error.response.data, currentAbortController, true);
-
const { data, dataBuffer } = parseDataFromResponse(error.response);
error.response.responseTime = error.response.headers.get('request-duration');
error.response.headers.delete('request-duration');
@@ -1338,7 +1355,7 @@ const registerNetworkIpc = (mainWindow) => {
size: Buffer.byteLength(dataBuffer),
data: error.response.data,
responseTime: error.response.responseTime,
- timeline: error.response.timeline
+ timeline: error.response.timeline,
};
// if we get a response from the server, we consider it as a success
@@ -1359,7 +1376,8 @@ const registerNetworkIpc = (mainWindow) => {
let postResponseScriptResult;
let postResponseError = null;
try {
- postResponseScriptResult = await runPostResponse(request,
+ postResponseScriptResult = await runPostResponse(
+ request,
response,
requestUid,
envVars,
@@ -1369,7 +1387,8 @@ const registerNetworkIpc = (mainWindow) => {
runtimeVariables,
processEnvVars,
scriptingConfig,
- runRequestByItemPathname);
+ runRequestByItemPathname
+ );
} catch (error) {
console.error('Post-response script error:', error);
postResponseError = error;
@@ -1406,12 +1425,14 @@ const registerNetworkIpc = (mainWindow) => {
const assertions = get(item, 'request.assertions');
if (assertions) {
const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime });
- const results = assertRuntime.runAssertions(assertions,
+ const results = assertRuntime.runAssertions(
+ assertions,
request,
response,
envVars,
runtimeVariables,
- processEnvVars);
+ processEnvVars
+ );
mainWindow.webContents.send('main:run-folder-event', {
type: 'assertion-results',
@@ -1422,14 +1443,15 @@ const registerNetworkIpc = (mainWindow) => {
}
const testFile = get(request, 'tests');
- const collectionName = collection?.name;
+ const collectionName = collection?.name
if (typeof testFile === 'string') {
let testResults = null;
let testError = null;
try {
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
- testResults = await testRuntime.runTests(decomment(testFile),
+ testResults = await testRuntime.runTests(
+ decomment(testFile),
request,
response,
envVars,
@@ -1439,10 +1461,11 @@ const registerNetworkIpc = (mainWindow) => {
processEnvVars,
scriptingConfig,
runRequestByItemPathname,
- collectionName);
+ collectionName
+ );
} catch (error) {
testError = error;
-
+
if (error.partialResults) {
testResults = error.partialResults;
} else {
@@ -1476,7 +1499,7 @@ const registerNetworkIpc = (mainWindow) => {
mainWindow.webContents.send('main:global-environment-variables-update', {
globalEnvironmentVariables: testResults.globalEnvironmentVariables
});
-
+
collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables;
notifyScriptExecution({
@@ -1505,7 +1528,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid,
folderUid,
statusText: 'collection run was terminated!',
- runCompletionTime: new Date().toISOString()
+ runCompletionTime: new Date().toISOString(),
});
break;
}
@@ -1522,7 +1545,7 @@ const registerNetworkIpc = (mainWindow) => {
if (nextRequestIdx >= 0) {
currentRequestIndex = nextRequestIdx;
} else {
- console.error('Could not find request with name \'' + nextRequestName + '\'');
+ console.error("Could not find request with name '" + nextRequestName + "'");
currentRequestIndex++;
}
} else {
@@ -1535,10 +1558,10 @@ const registerNetworkIpc = (mainWindow) => {
type: 'testrun-ended',
collectionUid,
folderUid,
- runCompletionTime: new Date().toISOString()
+ runCompletionTime: new Date().toISOString(),
});
} catch (error) {
- console.log('error', error);
+ console.log("error", error);
deleteCancelToken(cancelTokenUid);
mainWindow.webContents.send('main:run-folder-event', {
type: 'testrun-ended',
@@ -1635,13 +1658,14 @@ const executeRequestOnFailHandler = async (request, error) => {
}
};
+
const registerAllNetworkIpc = (mainWindow) => {
registerNetworkIpc(mainWindow);
registerGrpcEventHandlers(mainWindow);
registerWsEventHandlers(mainWindow);
-};
+}
-module.exports = registerAllNetworkIpc;
+module.exports = registerAllNetworkIpc
module.exports.configureRequest = configureRequest;
module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig;
module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;
diff --git a/publishing.md b/publishing.md
index 458077b20..cfac63958 100644
--- a/publishing.md
+++ b/publishing.md
@@ -10,6 +10,7 @@
| [正體中文](docs/publishing/publishing_zhtw.md)
| [日本語](docs/publishing/publishing_ja.md)
| [Nederlands](docs/publishing/publishing_nl.md)
+| [فارسی](docs/publishing/publishing_fa.md)
### Publishing Bruno to a new package manager
diff --git a/readme.md b/readme.md
index 8a1bad84c..ea55920ed 100644
--- a/readme.md
+++ b/readme.md
@@ -29,6 +29,7 @@
| [日本語](docs/readme/readme_ja.md)
| [ქართული](docs/readme/readme_ka.md)
| [Nederlands](docs/readme/readme_nl.md)
+| [فارسی](docs/readme/readme_fa.md)
Bruno is a new and innovative API client, aimed at revolutionizing the status quo represented by Postman and similar tools out there.
@@ -52,6 +53,7 @@ We strive to strike a harmonious balance between [open-source principles and sus
You can explore our [paid versions](https://www.usebruno.com/pricing) to see if there are additional features that you or your team may find useful!
## Table of Contents
+
- [Installation](#installation)
- [Features](#features)
- [Run across multiple platforms 🖥️](#run-across-multiple-platforms-%EF%B8%8F)
diff --git a/tests/response/json-response-formatting/fixtures/collection/request.bru b/tests/response/json-response-formatting/fixtures/collection/request.bru
index 3b27e47e1..46442f2e4 100644
--- a/tests/response/json-response-formatting/fixtures/collection/request.bru
+++ b/tests/response/json-response-formatting/fixtures/collection/request.bru
@@ -13,6 +13,7 @@ post {
body:json {
{
"bigint": 1736184243098437392,
- "unicode": ["\u4e00","\u4e8c","\u4e09"]
+ "unicode": ["\u4e00","\u4e8c","\u4e09"],
+ "forwardslashes": "\/url\/path\/"
}
}
diff --git a/tests/response/json-response-formatting/json-response-formatting.spec.ts b/tests/response/json-response-formatting/json-response-formatting.spec.ts
index 7f77d738f..29154d762 100644
--- a/tests/response/json-response-formatting/json-response-formatting.spec.ts
+++ b/tests/response/json-response-formatting/json-response-formatting.spec.ts
@@ -35,6 +35,9 @@ test.describe.serial('JSON Response Formatting', () => {
await expect(responseBody).toContainText('一');
await expect(responseBody).toContainText('二');
await expect(responseBody).toContainText('三');
+
+ // The response should handle escaped forward slashes
+ await expect(responseBody).toContainText('/url/path/');
});
});
});