From 2e5c63cfb9625152a0d0096e1b3e2225ca89bbf5 Mon Sep 17 00:00:00 2001 From: lohit Date: Mon, 7 Apr 2025 23:03:49 +0530 Subject: [PATCH] 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 Co-authored-by: Anoop M D --- .../Timeline/TimelineItem/Network/index.js | 14 +- .../src/components/ResponsePane/index.js | 2 +- packages/bruno-app/src/utils/network/index.js | 4 + .../src/ipc/network/axios-instance.js | 78 ++-- .../bruno-electron/src/ipc/network/index.js | 45 +- packages/bruno-electron/src/utils/common.js | 32 +- packages/bruno-electron/src/utils/oauth2.js | 405 ++++++++---------- .../bruno-electron/src/utils/proxy-util.js | 25 +- .../collection.bru | 6 +- .../environments/oauth2.bru | 21 +- .../user_info_request-auth.bru | 2 +- .../collection.bru | 20 +- .../environments/oauth2.bru | 22 +- .../user_info_custom.bru | 2 +- .../user_info_request-auth.bru | 6 +- .../keycloak-password-credentials/bruno.json | 9 + .../collection.bru | 20 + .../environments/oauth2.bru | 6 + .../user_info_coll-auth.bru | 11 + .../user_info_custom.bru | 15 + .../user_info_request-auth.bru | 28 ++ 21 files changed, 415 insertions(+), 358 deletions(-) create mode 100644 packages/bruno-tests/keycloak-password-credentials/bruno.json create mode 100644 packages/bruno-tests/keycloak-password-credentials/collection.bru create mode 100644 packages/bruno-tests/keycloak-password-credentials/environments/oauth2.bru create mode 100644 packages/bruno-tests/keycloak-password-credentials/user_info_coll-auth.bru create mode 100644 packages/bruno-tests/keycloak-password-credentials/user_info_custom.bru create mode 100644 packages/bruno-tests/keycloak-password-credentials/user_info_request-auth.bru 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 +}