improve network error handling, oauth2 logic cleanup, tls settings, and ui/test updates (#4444)

~ axios error interceptor fixes and timeline network logs ui updates
~ axios instance error interceptor now returns promise rejects instead of plain objects
~ fixed digest_auth regression
~ removed the interceptor logic for the oauth2 token url calls
~ timeline network logs ui updates
~ updated oauth2 test collections

* ssl/tls fixes and error handling
~ set the min allowed tls version to 1.0 (TLSv1)
~ proxy/certs/tls setup error handling

* enhance JSON stringification with circular reference handling
- Add getCircularReplacer to safely handle circular references in objects
- Update safeStringifyJSON to support indentation and handle undefined values
~ we currently support digest auth for bruno-cli

---------

Co-authored-by: lohit <lohit@usebruno.com>
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
lohit 2025-04-07 23:03:49 +05:30 committed by GitHub
parent 9845363349
commit 2e5c63cfb9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 415 additions and 358 deletions

View File

@ -2,9 +2,17 @@ const Network = ({ logs }) => {
return (
<div className="bg-black/5 text-white network-logs rounded overflow-auto h-96">
<pre className="whitespace-pre-wrap">
{logs.map((entry, index) => (
<NetworkLogsEntry key={index} entry={entry} />
))}
{logs.map((currentLog, index) => {
if (index > 0 && currentLog?.type === 'separator') {
return <div className="border-t-2 border-gray-500 w-full my-2" key={index} />;
}
const nextLog = logs[index + 1];
const isSameLogType = nextLog?.type === currentLog?.type;
return <>
<NetworkLogsEntry key={index} entry={currentLog} />
{!isSameLogType && <div className="mt-4"/>}
</>;
})}
</pre>
</div>
)

View File

@ -141,7 +141,7 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
)}
{focusedTab?.responsePaneTab === "timeline" ? (
<ClearTimeline item={item} collection={collection} />
) : item?.response ? (
) : (item?.response && !item?.response?.error) ? (
<>
<ResponseClear item={item} collection={collection} />
<ResponseSave item={item} />

View File

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

View File

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

View File

@ -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'? "<request body redacted>": (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'? "<request body redacted>": (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:

View File

@ -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'? "<request body redacted>": (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
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,5 +11,5 @@ get {
}
auth:bearer {
token: {{$oauth2.keycloak.access_token}}
token: {{$oauth2.credentials.access_token}}
}

View File

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

View File

@ -0,0 +1,9 @@
{
"version": "1",
"name": "keycloak-password-credentials",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

View File

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

View File

@ -0,0 +1,6 @@
vars {
key-host: http://localhost:8080
}
vars:secret [
client_secret
]

View File

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

View File

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

View File

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