diff --git a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js
index 373fd5ea6..25d704e63 100644
--- a/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/Timeline/TimelineItem/Network/index.js
@@ -2,9 +2,17 @@ const Network = ({ logs }) => {
return (
- {logs.map((entry, index) => (
-
- ))}
+ {logs.map((currentLog, index) => {
+ if (index > 0 && currentLog?.type === 'separator') {
+ return ;
+ }
+ const nextLog = logs[index + 1];
+ const isSameLogType = nextLog?.type === currentLog?.type;
+ return <>
+
+ {!isSameLogType && }
+ >;
+ })}
)
diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js
index 2d2a6ec25..454c2926c 100644
--- a/packages/bruno-app/src/components/ResponsePane/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/index.js
@@ -141,7 +141,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
)}
{focusedTab?.responsePaneTab === "timeline" ? (
- ) : item?.response ? (
+ ) : (item?.response && !item?.response?.error) ? (
<>
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index eb5b9fb9d..529b38d8a 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -5,6 +5,10 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
if (['http-request', 'graphql-request'].includes(item.type)) {
sendHttpRequest(item, collection, environment, runtimeVariables)
.then((response) => {
+ // if there is an error, we return the response object as is
+ if (response?.error) {
+ resolve(response)
+ }
resolve({
state: 'success',
data: response.data,
diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js
index 9d6f9b1f0..5406f9869 100644
--- a/packages/bruno-electron/src/ipc/network/axios-instance.js
+++ b/packages/bruno-electron/src/ipc/network/axios-instance.js
@@ -102,9 +102,12 @@ function makeAxiosInstance({
const url = URL.parse(config.url);
config.metadata = config.metadata || {};
config.metadata.startTime = new Date().getTime();
- const timeline = config.metadata.timeline || []
-
+ const timeline = config.metadata.timeline || [];
// Add initial request details to the timeline
+ timeline.push({
+ timestamp: new Date(),
+ type: 'separator'
+ });
timeline.push({
timestamp: new Date(),
type: 'info',
@@ -173,10 +176,13 @@ function makeAxiosInstance({
});
}
catch(err) {
+ if (err.timeline) {
+ timeline = err.timeline;
+ }
timeline.push({
timestamp: new Date(),
type: 'error',
- message: err?.message,
+ message: `Error setting up proxy agents: ${err?.message}`,
});
}
config.metadata.timeline = timeline;
@@ -264,21 +270,12 @@ function makeAxiosInstance({
if (redirectCount >= requestMaxRedirects) {
const errorResponseData = error.response.data;
- const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
timeline?.push({
timestamp: new Date(),
type: 'error',
message: safeStringifyJSON(errorResponseData?.toString?.())
});
- return {
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: errorResponseData?.toString?.(),
- size: Buffer.byteLength(dataBuffer),
- duration: error.response.headers.get('request-duration') ?? 0,
- timeline: error.response.timeline
- };
+ return Promise.reject(error);
}
// Increase redirect count
@@ -319,14 +316,26 @@ function makeAxiosInstance({
}
}
- setupProxyAgents({
- requestConfig,
- proxyMode,
- proxyConfig,
- httpsAgentRequestFields,
- interpolationOptions,
- timeline
- });
+ try {
+ setupProxyAgents({
+ requestConfig,
+ proxyMode,
+ proxyConfig,
+ httpsAgentRequestFields,
+ interpolationOptions,
+ timeline
+ });
+ }
+ catch(err) {
+ if (err.timeline) {
+ timeline = err.timeline;
+ }
+ timeline.push({
+ timestamp: new Date(),
+ type: 'error',
+ message: `Error setting up proxy agents: ${err?.message}`,
+ });
+ }
requestConfig.metadata.timeline = timeline;
// Make the redirected request
@@ -334,7 +343,11 @@ function makeAxiosInstance({
}
else {
const errorResponseData = error.response.data;
- const dataBuffer = Buffer.isBuffer(errorResponseData) ? errorResponseData : Buffer.from(errorResponseData);
+ timeline.push({
+ timestamp: new Date(),
+ type: 'response',
+ message: `HTTP/${error.response.httpVersion || '1.1'} ${error.response.status} ${error.response.statusText}`,
+ });
Object.entries(error?.response?.headers || {}).forEach(([key, value]) => {
timeline.push({
timestamp: new Date(),
@@ -357,15 +370,8 @@ function makeAxiosInstance({
type: 'error',
message: safeStringifyJSON(error?.errors)
});
- return {
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: errorResponseData?.toString?.(),
- size: Buffer.byteLength(dataBuffer),
- duration: error.response.headers.get('request-duration') ?? 0,
- timeline
- };
+ error.response.timeline = timeline;
+ return Promise.reject(error);
}
}
else if (error?.code) {
@@ -386,13 +392,9 @@ function makeAxiosInstance({
type: 'error',
message: safeStringifyJSON(error?.errors)
});
- return {
- status: '-',
- statusText: error.code,
- headers: error?.config?.headers,
- data: 'request failed, check timeline network logs',
- timeline
- };
+ error.timeline = timeline;
+ error.statusText = error.code;
+ return Promise.reject(error);
}
return Promise.reject(error);
}
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 2fc32cba7..325ff0391 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -20,7 +20,7 @@ const { prepareRequest } = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
const { makeAxiosInstance } = require('./axios-instance');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
-const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse } = require('../../utils/common');
+const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common');
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies');
const { createFormData } = require('../../utils/form-data');
@@ -557,16 +557,14 @@ const registerNetworkIpc = (mainWindow) => {
processEnvVars,
collectionPath
);
- const requestData = request.mode == 'file'? "": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
+
+ const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
let requestSent = {
url: request.url,
method: request.method,
headers: request.headers,
data: requestData,
- timestamp: Date.now()
- }
- if (requestData) {
- requestSent.dataBuffer = Buffer.from(requestData);
+ dataBuffer: requestDataBuffer
}
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
@@ -602,9 +600,14 @@ const registerNetworkIpc = (mainWindow) => {
// if it's a cancel request, don't continue
if (axios.isCancel(error)) {
- let error = new Error('Request cancelled');
- error.isCancel = true;
- return Promise.reject(error);
+ // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
+ // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
+ return {
+ statusText: 'REQUEST_CANCELLED',
+ isCancel: true,
+ error: 'REQUEST_CANCELLED',
+ timeline: error.timeline
+ };
}
if (error?.response) {
@@ -615,7 +618,13 @@ const registerNetworkIpc = (mainWindow) => {
response.headers.delete('request-duration');
} else {
// if it's not a network error, don't continue
- return Promise.reject(error);
+ // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
+ // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
+ return {
+ statusText: error.statusText,
+ error: error.message,
+ timeline: error.timeline
+ }
}
}
@@ -743,7 +752,13 @@ const registerNetworkIpc = (mainWindow) => {
} catch (error) {
deleteCancelToken(cancelTokenUid);
- return Promise.reject(error);
+ // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation
+ // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise
+ return {
+ status: error?.status,
+ error: error?.message || 'an error ocurred: debug',
+ timeline: error?.timeline
+ };
}
}
@@ -992,15 +1007,13 @@ const registerNetworkIpc = (mainWindow) => {
continue;
}
- const requestData = request.mode == 'file'? "": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
+ const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request);
let requestSent = {
url: request.url,
method: request.method,
headers: request.headers,
- data: requestData
- }
- if (requestData) {
- requestSent.dataBuffer = Buffer.from(requestData);
+ data: requestData,
+ dataBuffer: requestDataBuffer
}
// todo:
diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js
index 71bdfa09e..a855e5523 100644
--- a/packages/bruno-electron/src/utils/common.js
+++ b/packages/bruno-electron/src/utils/common.js
@@ -1,5 +1,6 @@
const { customAlphabet } = require('nanoid');
const iconv = require('iconv-lite');
+const { cloneDeep } = require('lodash');
// a customized version of nanoid without using _ and -
const uuid = () => {
@@ -26,10 +27,24 @@ const parseJson = async (obj) => {
}
};
-const safeStringifyJSON = (data) => {
+const getCircularReplacer = () => {
+ const seen = new WeakSet();
+ return (key, value) => {
+ if (typeof value === "object" && value !== null) {
+ if (seen.has(value)) return "[Circular]";
+ seen.add(value);
+ }
+ return value;
+ };
+};
+
+const safeStringifyJSON = (data, indent = null) => {
+ if (data === undefined) return undefined;
try {
- return JSON.stringify(data);
+ // getCircularReplacer - removes circular references that cause an error when stringifying
+ return JSON.stringify(data, getCircularReplacer(), indent);
} catch (e) {
+ console.warn('Failed to stringify data:', e.message);
return data;
}
};
@@ -112,6 +127,16 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
return { data, dataBuffer };
};
+const parseDataFromRequest = (request) => {
+ const requestDataString = request.mode == 'file'? "": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data));
+ const requestCopy = cloneDeep(request);
+ if (!requestCopy.data) {
+ return { data: null, dataBuffer: null };
+ }
+ requestCopy.data = requestDataString;
+ return parseDataFromResponse(requestCopy);
+};
+
module.exports = {
uuid,
stringifyJson,
@@ -121,5 +146,6 @@ module.exports = {
simpleHash,
generateUidBasedOnHash,
flattenDataForDotNotation,
- parseDataFromResponse
+ parseDataFromResponse,
+ parseDataFromRequest
};
diff --git a/packages/bruno-electron/src/utils/oauth2.js b/packages/bruno-electron/src/utils/oauth2.js
index 882f39767..614e0224c 100644
--- a/packages/bruno-electron/src/utils/oauth2.js
+++ b/packages/bruno-electron/src/utils/oauth2.js
@@ -42,6 +42,10 @@ const isTokenExpired = (credentials) => {
return Date.now() > expiryTime;
};
+const safeParseJSONBuffer = (data) => {
+ return safeParseJSON(Buffer.isBuffer(data) ? data.toString() : data);
+}
+
// AUTHORIZATION CODE
const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, forceFetch = false, certsAndProxyConfig }) => {
@@ -143,68 +147,46 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
-
- // Initialize variables to hold request and response data for debugging
- let axiosRequestInfo = null;
- let axiosResponseInfo = null;
-
try {
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
- // Interceptor to capture request data
- axiosInstance.interceptors.request.use((config) => {
- const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
- axiosRequestInfo = {
- method: config.method.toUpperCase(),
- url: config.url,
- headers: config.headers,
- data: requestData,
- timestamp: Date.now(),
- };
- return config;
- });
-
- // Interceptor to capture response data
- axiosInstance.interceptors.response.use((response) => {
- axiosResponseInfo = {
+ let responseInfo, parsedResponseData;
+ try {
+ const response = await axiosInstance(requestCopy);
+ parsedResponseData = safeParseJSONBuffer(response.data);
+ responseInfo = {
url: response?.url,
- status: response.status,
- statusText: response.statusText,
- headers: response.headers,
- data: response.data,
+ status: response?.status,
+ statusText: response?.statusText,
+ headers: response?.headers,
+ data: parsedResponseData,
timestamp: Date.now(),
timeline: response?.timeline
};
- return response;
- }, (error) => {
+ }
+ catch(error) {
if (error.response) {
- axiosResponseInfo = {
+ responseInfo = {
url: error?.response?.url,
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: error.response.data,
+ status: error?.response?.status,
+ statusText: error?.response?.statusText,
+ headers: error?.response?.headers,
+ data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
timestamp: Date.now(),
timeline: error?.response?.timeline,
- error: 'fetching access token failed! check timeline network logs'
+ error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
};
}
else if(error?.code) {
- axiosResponseInfo = {
+ responseInfo = {
status: '-',
- statusText: error.code,
+ statusText: error?.code,
headers: error?.config?.headers,
data: safeStringifyJSON(error?.errors),
timeline: error?.response?.timeline
};
}
- return axiosResponseInfo;
- });
-
- const response = await axiosInstance(requestCopy);
- const parsedResponseData = safeParseJSON(
- Buffer.isBuffer(response.data) ? response.data?.toString() : response.data
- );
+ }
// Ensure debugInfo.data is initialized
if (!debugInfo) {
debugInfo = { data: [] };
@@ -216,33 +198,32 @@ const getOAuth2TokenUsingAuthorizationCode = async ({ request, collectionUid, fo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
- url: axiosRequestInfo?.url,
- method: axiosRequestInfo?.method,
- headers: axiosRequestInfo?.headers || {},
- data: axiosRequestInfo?.data,
+ url: url,
+ method: 'POST',
+ headers: requestCopy?.headers,
+ data: requestCopy?.data,
error: null
},
response: {
- url: axiosResponseInfo?.url,
- headers: axiosResponseInfo?.headers,
- data: parsedResponseData,
- status: axiosResponseInfo?.status,
- statusText: axiosResponseInfo?.statusText,
- error: axiosResponseInfo?.error,
- timeline: axiosResponseInfo?.timeline
+ url: responseInfo?.url,
+ headers: responseInfo?.headers,
+ data: responseInfo?.data,
+ status: responseInfo?.status,
+ statusText: responseInfo?.statusText,
+ error: responseInfo?.error,
+ timeline: responseInfo?.timeline
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
-
debugInfo.data.push(axiosMainRequest);
- persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
+ parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
- return Promise.reject(safeStringifyJSON(error?.response?.data));
+ return Promise.reject(error);
}
};
@@ -369,96 +350,79 @@ const getOAuth2TokenUsingClientCredentials = async ({ request, collectionUid, fo
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
-
- // Initialize variables to hold request and response data for debugging
- let axiosRequestInfo = null;
- let axiosResponseInfo = null;
let debugInfo = { data: [] };
-
try {
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
- axiosInstance.interceptors.request.use((config) => {
- const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
- axiosRequestInfo = {
- method: config.method.toUpperCase(),
- url: config.url,
- headers: config.headers,
- data: requestData,
- timestamp: Date.now(),
- };
- return config;
- });
-
- // Interceptor to capture response data
- axiosInstance.interceptors.response.use((response) => {
- axiosResponseInfo = {
+ let responseInfo, parsedResponseData;
+ try {
+ const response = await axiosInstance(requestCopy);
+ parsedResponseData = safeParseJSONBuffer(response.data);
+ responseInfo = {
url: response?.url,
- status: response.status,
- statusText: response.statusText,
- headers: response.headers,
- data: response.data,
+ status: response?.status,
+ statusText: response?.statusText,
+ headers: response?.headers,
+ data: parsedResponseData,
timestamp: Date.now(),
timeline: response?.timeline
};
- return response;
- }, (error) => {
+ }
+ catch(error) {
if (error.response) {
- axiosResponseInfo = {
+ responseInfo = {
url: error?.response?.url,
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: error.response.data,
+ status: error?.response?.status,
+ statusText: error?.response?.statusText,
+ headers: error?.response?.headers,
+ data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
timestamp: Date.now(),
timeline: error?.response?.timeline,
- error: 'fetching access token failed! check timeline network logs'
+ error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
};
}
else if(error?.code) {
- axiosResponseInfo = {
+ responseInfo = {
status: '-',
- statusText: error.code,
+ statusText: error?.code,
headers: error?.config?.headers,
data: safeStringifyJSON(error?.errors),
timeline: error?.response?.timeline
};
}
- return axiosResponseInfo;
- });
-
- const response = await axiosInstance(requestCopy);
- const parsedResponseData = safeParseJSON(
- Buffer.isBuffer(response.data) ? response.data.toString() : response.data
- );
+ }
+ if (!debugInfo) {
+ debugInfo = { data: [] };
+ } else if (!debugInfo.data) {
+ debugInfo.data = [];
+ }
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
- url: axiosRequestInfo?.url,
- method: axiosRequestInfo?.method,
- headers: axiosRequestInfo?.headers || {},
- data: axiosRequestInfo?.data,
+ url: url,
+ method: 'POST',
+ headers: requestCopy?.headers,
+ data: requestCopy?.data,
error: null
},
response: {
- url: axiosResponseInfo.url,
- headers: axiosResponseInfo?.headers,
- data: parsedResponseData,
- status: axiosResponseInfo?.status,
- statusText: axiosResponseInfo?.statusText,
- timeline: axiosResponseInfo?.timeline,
- error: null
+ url: responseInfo?.url,
+ headers: responseInfo?.headers,
+ data: responseInfo?.data,
+ status: responseInfo?.status,
+ statusText: responseInfo?.statusText,
+ error: responseInfo?.error,
+ timeline: responseInfo?.timeline
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
-
debugInfo.data.push(axiosMainRequest);
- persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
+ parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
return Promise.reject(safeStringifyJSON(error?.response?.data));
@@ -557,95 +521,79 @@ const getOAuth2TokenUsingPasswordCredentials = async ({ request, collectionUid,
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
-
- // Initialize variables to hold request and response data for debugging
- let axiosRequestInfo = null;
- let axiosResponseInfo = null;
let debugInfo = { data: [] };
-
try {
const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
- axiosInstance.interceptors.request.use((config) => {
- const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
- axiosRequestInfo = {
- method: config.method.toUpperCase(),
- url: config.url,
- headers: config.headers,
- data: requestData,
- timestamp: Date.now(),
- };
- return config;
- });
-
- // Interceptor to capture response data
- axiosInstance.interceptors.response.use((response) => {
- axiosResponseInfo = {
+ let responseInfo, parsedResponseData;
+ try {
+ const response = await axiosInstance(requestCopy);
+ parsedResponseData = safeParseJSONBuffer(response.data);
+ responseInfo = {
url: response?.url,
- status: response.status,
- statusText: response.statusText,
- headers: response.headers,
- data: response.data,
+ status: response?.status,
+ statusText: response?.statusText,
+ headers: response?.headers,
+ data: parsedResponseData,
timestamp: Date.now(),
timeline: response?.timeline
};
- return response;
- }, (error) => {
+ }
+ catch(error) {
if (error.response) {
- axiosResponseInfo = {
+ responseInfo = {
url: error?.response?.url,
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: error.response.data,
+ status: error?.response?.status,
+ statusText: error?.response?.statusText,
+ headers: error?.response?.headers,
+ data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
timestamp: Date.now(),
timeline: error?.response?.timeline,
- error: 'fetching access token failed! check timeline network logs'
+ error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
};
}
else if(error?.code) {
- axiosResponseInfo = {
+ responseInfo = {
status: '-',
- statusText: error.code,
+ statusText: error?.code,
headers: error?.config?.headers,
data: safeStringifyJSON(error?.errors),
timeline: error?.response?.timeline
};
}
- return axiosResponseInfo;
- });
- const response = await axiosInstance(requestCopy);
- const parsedResponseData = safeParseJSON(
- Buffer.isBuffer(response.data) ? response.data.toString() : response.data
- );
+ }
+ if (!debugInfo) {
+ debugInfo = { data: [] };
+ } else if (!debugInfo.data) {
+ debugInfo.data = [];
+ }
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
- url: axiosRequestInfo?.url,
- method: axiosRequestInfo?.method,
- headers: axiosRequestInfo?.headers || {},
- data: axiosRequestInfo?.data,
+ url: url,
+ method: 'POST',
+ headers: requestCopy?.headers,
+ data: requestCopy?.data,
error: null
},
response: {
- url: axiosResponseInfo?.url,
- headers: axiosResponseInfo?.headers,
- data: parsedResponseData,
- status: axiosResponseInfo?.status,
- statusText: axiosResponseInfo?.statusText,
- timeline: axiosResponseInfo?.timeline,
- error: null
+ url: responseInfo?.url,
+ headers: responseInfo?.headers,
+ data: responseInfo?.data,
+ status: responseInfo?.status,
+ statusText: responseInfo?.statusText,
+ error: responseInfo?.error,
+ timeline: responseInfo?.timeline
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
-
debugInfo.data.push(axiosMainRequest);
- persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
+ parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
return Promise.reject(safeStringifyJSON(error?.response?.data));
@@ -677,101 +625,82 @@ const refreshOauth2Token = async ({ requestCopy, collectionUid, certsAndProxyCon
requestCopy.data = qs.stringify(data);
requestCopy.url = url;
requestCopy.responseType = 'arraybuffer';
-
- // Initialize variables to hold request and response data for debugging
- let axiosRequestInfo = null;
- let axiosResponseInfo = null;
- let debugInfo = { data: [] };
-
- const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
- const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
- axiosInstance.interceptors.request.use((config) => {
- const requestData = typeof config?.data === 'string' ? config?.data : safeStringifyJSON(config?.data);
- axiosRequestInfo = {
- method: config.method.toUpperCase(),
- url: config.url,
- headers: config.headers,
- data: requestData,
- timestamp: Date.now(),
- };
- return config;
- });
-
- // Interceptor to capture response data
- axiosInstance.interceptors.response.use((response) => {
- axiosResponseInfo = {
- url: response?.url,
- status: response.status,
- statusText: response.statusText,
- headers: response.headers,
- data: response.data,
- timestamp: Date.now(),
- timeline: response?.timeline
- };
- return response;
- }, (error) => {
- if (error.response) {
- axiosResponseInfo = {
- url: error?.response?.url,
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: error.response.data,
- timestamp: Date.now(),
- timeline: error?.response?.timeline,
- error: 'fetching access token failed! check timeline network logs'
- };
- }
- else if(error?.code) {
- axiosResponseInfo = {
- status: '-',
- statusText: error.code,
- headers: error?.config?.headers,
- data: safeStringifyJSON(error?.errors),
- timeline: error?.response?.timeline
- };
- }
- return axiosResponseInfo;
- });
-
-
try {
- const response = await axiosInstance(requestCopy);
- const parsedResponseData = safeParseJSON(
- Buffer.isBuffer(response.data) ? response.data.toString() : response.data
- );
+ const { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
+ const axiosInstance = makeAxiosInstance({ proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions });
+ let responseInfo, parsedResponseData;
+ try {
+ const response = await axiosInstance(requestCopy);
+ parsedResponseData = safeParseJSONBuffer(response.data);
+ responseInfo = {
+ url: response?.url,
+ status: response?.status,
+ statusText: response?.statusText,
+ headers: response?.headers,
+ data: parsedResponseData,
+ timestamp: Date.now(),
+ timeline: response?.timeline
+ };
+ }
+ catch(error) {
+ if (error.response) {
+ responseInfo = {
+ url: error?.response?.url,
+ status: error?.response?.status,
+ statusText: error?.response?.statusText,
+ headers: error?.response?.headers,
+ data: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
+ timestamp: Date.now(),
+ timeline: error?.response?.timeline,
+ error: safeStringifyJSON(safeParseJSONBuffer(error?.response?.data)),
+ };
+ }
+ else if(error?.code) {
+ responseInfo = {
+ status: '-',
+ statusText: error?.code,
+ headers: error?.config?.headers,
+ data: safeStringifyJSON(error?.errors),
+ timeline: error?.response?.timeline
+ };
+ }
+ }
+ if (!debugInfo) {
+ debugInfo = { data: [] };
+ } else if (!debugInfo.data) {
+ debugInfo.data = [];
+ }
// Add the axios request and response info as a main request in debugInfo
const axiosMainRequest = {
requestId: Date.now().toString(),
request: {
- url: axiosRequestInfo?.url,
- method: axiosRequestInfo?.method,
- headers: axiosRequestInfo?.headers || {},
- data: axiosRequestInfo?.data,
+ url: url,
+ method: 'POST',
+ headers: requestCopy?.headers,
+ data: requestCopy?.data,
error: null
},
response: {
- url: axiosResponseInfo?.url,
- headers: axiosResponseInfo?.headers,
- data: parsedResponseData,
- status: axiosResponseInfo?.status,
- statusText: axiosResponseInfo?.statusText,
- timeline: axiosResponseInfo?.timeline,
- error: null
+ url: responseInfo?.url,
+ headers: responseInfo?.headers,
+ data: responseInfo?.data,
+ status: responseInfo?.status,
+ statusText: responseInfo?.statusText,
+ error: responseInfo?.error,
+ timeline: responseInfo?.timeline
},
fromCache: false,
completed: true,
requests: [], // No sub-requests in this context
};
-
debugInfo.data.push(axiosMainRequest);
if (parsedResponseData?.error) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
return { collectionUid, url, credentials: null, credentialsId, debugInfo };
}
- persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
+ parsedResponseData && persistOauth2Credentials({ collectionUid, url, credentials: parsedResponseData, credentialsId });
return { collectionUid, url, credentials: parsedResponseData, credentialsId, debugInfo };
} catch (error) {
clearOauth2Credentials({ collectionUid, url, credentialsId });
diff --git a/packages/bruno-electron/src/utils/proxy-util.js b/packages/bruno-electron/src/utils/proxy-util.js
index dc8dd5382..b44c702db 100644
--- a/packages/bruno-electron/src/utils/proxy-util.js
+++ b/packages/bruno-electron/src/utils/proxy-util.js
@@ -168,10 +168,21 @@ function createTimelineAgentClass(BaseAgentClass) {
message: `Trying ${host}:${port}...`,
});
- const socket = super.createConnection(options, callback);
+ let socket;
+ try {
+ socket = super.createConnection(options, callback);
+ } catch (error) {
+ this.timeline.push({
+ timestamp: new Date(),
+ type: 'error',
+ message: `Error creating connection: ${error.message}`,
+ });
+ error.timeline = this.timeline;
+ throw error;
+ }
// Attach event listeners to the socket
- socket.on('lookup', (err, address, family, host) => {
+ socket?.on('lookup', (err, address, family, host) => {
if (err) {
this.timeline.push({
timestamp: new Date(),
@@ -187,7 +198,7 @@ function createTimelineAgentClass(BaseAgentClass) {
}
});
- socket.on('connect', () => {
+ socket?.on('connect', () => {
const address = socket.remoteAddress || host;
const remotePort = socket.remotePort || port;
@@ -198,7 +209,7 @@ function createTimelineAgentClass(BaseAgentClass) {
});
});
- socket.on('secureConnect', () => {
+ socket?.on('secureConnect', () => {
const protocol = socket.getProtocol() || 'SSL/TLS';
const cipher = socket.getCipher();
const cipherSuite = cipher ? `${cipher.name} (${cipher.version})` : 'Unknown cipher';
@@ -270,7 +281,7 @@ function createTimelineAgentClass(BaseAgentClass) {
}
});
- socket.on('error', (err) => {
+ socket?.on('error', (err) => {
this.timeline.push({
timestamp: new Date(),
type: 'error',
@@ -294,6 +305,10 @@ function setupProxyAgents({
// Ensure TLS options are properly set
const tlsOptions = {
...httpsAgentRequestFields,
+ // Enable all secure protocols by default
+ secureProtocol: undefined,
+ // Allow Node.js to choose the protocol
+ minVersion: 'TLSv1',
rejectUnauthorized: httpsAgentRequestFields.rejectUnauthorized !== undefined ? httpsAgentRequestFields.rejectUnauthorized : true,
};
diff --git a/packages/bruno-tests/keycloak-authorization_code/collection.bru b/packages/bruno-tests/keycloak-authorization_code/collection.bru
index 7b098feca..c858dac93 100644
--- a/packages/bruno-tests/keycloak-authorization_code/collection.bru
+++ b/packages/bruno-tests/keycloak-authorization_code/collection.bru
@@ -7,8 +7,9 @@ auth:oauth2 {
callback_url: {{key-host}}/realms/bruno/account
authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
+ refresh_token_url:
client_id: account
- client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
+ client_secret: {{client_secret}}
scope: openid
state:
pkce: true
@@ -16,5 +17,6 @@ auth:oauth2 {
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
- reuse_token:
+ auto_fetch_token: true
+ auto_refresh_token: false
}
diff --git a/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru b/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru
index 315cbf625..8d4ce79a8 100644
--- a/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru
+++ b/packages/bruno-tests/keycloak-authorization_code/environments/oauth2.bru
@@ -1,21 +1,6 @@
vars {
- host: http://localhost:8081
- bearer_auth_token: your_secret_token
- basic_auth_password: della
- client_id: client_id_1
- client_secret: client_secret_1
- password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
- password_credentials_username: foo
- password_credentials_password: bar
- password_credentials_scope:
- authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
- authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
- authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
- authorization_code_access_token: null
- client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
- client_credentials_client_id: client_id_1
- client_credentials_client_secret: client_secret_1
- client_credentials_scope: admin
- client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
key-host: http://localhost:8080
}
+vars:secret [
+ client_secret
+]
diff --git a/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru
index 10777b762..eabd03b54 100644
--- a/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru
+++ b/packages/bruno-tests/keycloak-authorization_code/user_info_request-auth.bru
@@ -17,7 +17,7 @@ auth:oauth2 {
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
refresh_token_url:
client_id: account
- client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
+ client_secret: {{client_secret}}
scope: openid
state:
pkce: true
diff --git a/packages/bruno-tests/keycloak-client-credentials/collection.bru b/packages/bruno-tests/keycloak-client-credentials/collection.bru
index baff96f6c..e488de865 100644
--- a/packages/bruno-tests/keycloak-client-credentials/collection.bru
+++ b/packages/bruno-tests/keycloak-client-credentials/collection.bru
@@ -3,18 +3,16 @@ auth {
}
auth:oauth2 {
- grant_type: authorization_code
- callback_url: {{key-host}}/realms/bruno/account
- authorization_url: {{key-host}}/realms/bruno/protocol/openid-connect/auth
+ grant_type: client_credentials
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
+ refresh_token_url:
client_id: account
- client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
+ client_secret: {{client_secret}}
scope: openid
- state:
- pkce: true
- tokenId: keycloak
- tokenPlacement: header
- tokenHeaderPrefix: Bearer
- tokenQueryKey: access_token
- reuseToken:
+ credentials_placement: body
+ credentials_id: credentials
+ token_placement: header
+ token_header_prefix: Bearer
+ auto_fetch_token: true
+ auto_refresh_token: false
}
diff --git a/packages/bruno-tests/keycloak-client-credentials/environments/oauth2.bru b/packages/bruno-tests/keycloak-client-credentials/environments/oauth2.bru
index b9a0c5468..8d4ce79a8 100644
--- a/packages/bruno-tests/keycloak-client-credentials/environments/oauth2.bru
+++ b/packages/bruno-tests/keycloak-client-credentials/environments/oauth2.bru
@@ -1,22 +1,6 @@
vars {
- host: http://localhost:8080
- bearer_auth_token: your_secret_token
- basic_auth_password: della
- client_id: client_id_1
- client_secret: client_secret_1
- password_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/password_credentials/token
- password_credentials_username: foo
- password_credentials_password: bar
- password_credentials_scope:
- authorization_code_authorize_url: http://localhost:8081/api/auth/oauth2/authorization_code/authorize
- authorization_code_callback_url: http://localhost:8081/api/auth/oauth2/authorization_code/callback
- authorization_code_access_token_url: http://localhost:8081/api/auth/oauth2/authorization_code/token
- authorization_code_access_token: null
- client_credentials_access_token_url: http://localhost:8081/api/auth/oauth2/client_credentials/token
- client_credentials_client_id: client_id_1
- client_credentials_client_secret: client_secret_1
- client_credentials_scope: admin
- client_credentials_access_token: 870132a2ed28a3c94d34f868e6514720
key-host: http://localhost:8080
- key-host-1: http://localhost:8082
}
+vars:secret [
+ client_secret
+]
diff --git a/packages/bruno-tests/keycloak-client-credentials/user_info_custom.bru b/packages/bruno-tests/keycloak-client-credentials/user_info_custom.bru
index 58cadf9cf..c5a757ed0 100644
--- a/packages/bruno-tests/keycloak-client-credentials/user_info_custom.bru
+++ b/packages/bruno-tests/keycloak-client-credentials/user_info_custom.bru
@@ -11,5 +11,5 @@ get {
}
auth:bearer {
- token: {{$oauth2.keycloak.access_token}}
+ token: {{$oauth2.credentials.access_token}}
}
diff --git a/packages/bruno-tests/keycloak-client-credentials/user_info_request-auth.bru b/packages/bruno-tests/keycloak-client-credentials/user_info_request-auth.bru
index c142cda58..a8a69792b 100644
--- a/packages/bruno-tests/keycloak-client-credentials/user_info_request-auth.bru
+++ b/packages/bruno-tests/keycloak-client-credentials/user_info_request-auth.bru
@@ -13,12 +13,14 @@ get {
auth:oauth2 {
grant_type: client_credentials
access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
+ refresh_token_url:
client_id: account
- client_secret: Lh3NkRikMZpO12rwSBwVimde9v89B5Rw
+ client_secret: {{client_secret}}a
scope: openid
credentials_placement: body
credentials_id: credentials
token_placement: header
token_header_prefix: Bearer
- reuse_token:
+ auto_fetch_token: true
+ auto_refresh_token: false
}
diff --git a/packages/bruno-tests/keycloak-password-credentials/bruno.json b/packages/bruno-tests/keycloak-password-credentials/bruno.json
new file mode 100644
index 000000000..495eacff7
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/bruno.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "keycloak-password-credentials",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
\ No newline at end of file
diff --git a/packages/bruno-tests/keycloak-password-credentials/collection.bru b/packages/bruno-tests/keycloak-password-credentials/collection.bru
new file mode 100644
index 000000000..0bcc69e2d
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/collection.bru
@@ -0,0 +1,20 @@
+auth {
+ mode: oauth2
+}
+
+auth:oauth2 {
+ grant_type: password
+ access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
+ refresh_token_url:
+ username: bruno
+ password: bruno
+ client_id: account
+ client_secret: {{client_secret}}
+ scope: openid
+ credentials_placement: body
+ credentials_id: credentials
+ token_placement: header
+ token_header_prefix: Bearer
+ auto_fetch_token: true
+ auto_refresh_token: false
+}
diff --git a/packages/bruno-tests/keycloak-password-credentials/environments/oauth2.bru b/packages/bruno-tests/keycloak-password-credentials/environments/oauth2.bru
new file mode 100644
index 000000000..8d4ce79a8
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/environments/oauth2.bru
@@ -0,0 +1,6 @@
+vars {
+ key-host: http://localhost:8080
+}
+vars:secret [
+ client_secret
+]
diff --git a/packages/bruno-tests/keycloak-password-credentials/user_info_coll-auth.bru b/packages/bruno-tests/keycloak-password-credentials/user_info_coll-auth.bru
new file mode 100644
index 000000000..ec838c9fa
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/user_info_coll-auth.bru
@@ -0,0 +1,11 @@
+meta {
+ name: user_info_coll-auth
+ type: http
+ seq: 1
+}
+
+get {
+ url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
+ body: none
+ auth: inherit
+}
diff --git a/packages/bruno-tests/keycloak-password-credentials/user_info_custom.bru b/packages/bruno-tests/keycloak-password-credentials/user_info_custom.bru
new file mode 100644
index 000000000..c5a757ed0
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/user_info_custom.bru
@@ -0,0 +1,15 @@
+meta {
+ name: user_info_custom
+ type: http
+ seq: 2
+}
+
+get {
+ url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
+ body: none
+ auth: bearer
+}
+
+auth:bearer {
+ token: {{$oauth2.credentials.access_token}}
+}
diff --git a/packages/bruno-tests/keycloak-password-credentials/user_info_request-auth.bru b/packages/bruno-tests/keycloak-password-credentials/user_info_request-auth.bru
new file mode 100644
index 000000000..c17b6cf0f
--- /dev/null
+++ b/packages/bruno-tests/keycloak-password-credentials/user_info_request-auth.bru
@@ -0,0 +1,28 @@
+meta {
+ name: user_info_request-auth
+ type: http
+ seq: 3
+}
+
+get {
+ url: {{key-host}}/realms/bruno/protocol/openid-connect/userinfo
+ body: none
+ auth: oauth2
+}
+
+auth:oauth2 {
+ grant_type: password
+ access_token_url: {{key-host}}/realms/bruno/protocol/openid-connect/token
+ refresh_token_url:
+ username: admin
+ password: admin
+ client_id: account
+ client_secret: {{client_secret}}
+ scope: openid
+ credentials_placement: body
+ credentials_id: credentials
+ token_placement: header
+ token_header_prefix: Bearer
+ auto_fetch_token: true
+ auto_refresh_token: false
+}