From fc5093eab4ca2bc7e14f4a3f3dabf7e3300699aa Mon Sep 17 00:00:00 2001 From: DaviXavier <47609623+davirxavier@users.noreply.github.com> Date: Wed, 12 Nov 2025 07:26:26 -0300 Subject: [PATCH] fix: #1884 - Fixes infinite loading issue for text/event-stream requests (#4472) * #1884 - Add support for text/event-stream content-type * #1884 - Fix bugs with streaming Fix bug when streaming response is not ok Fix bug when clearing response of streaming request Show text signaling that the response is being streamed in the reponse status Update response size when new data is streamed in * #1884 - Fix multiple requests when spamming send button * #1884 - Add time counter for streamed response and fix final time * #1884 - Run post script only at end of streamed request * #1884 - add support for automatic "upgrade" to streaming data * #1884 - adjustments for stopwatch in stream implementation and remove unused imports * #1884 - fix imports indentation in useIpcEvents.js * #1884 - remove stream data ended export function from collections --------- Co-authored-by: Siddharth Gelera --- .../components/RequestPane/QueryUrl/index.js | 12 +- .../src/components/RequestTabPanel/index.js | 31 +- .../ResponseStopWatch/StyledWrapper.js | 10 + .../ResponsePane/ResponseStopWatch/index.js | 27 + .../ResponsePane/StatusCode/index.js | 4 +- .../src/components/ResponsePane/index.js | 29 +- .../src/providers/App/useIpcEvents.js | 18 +- .../ReduxStore/slices/collections/index.js | 130 +++-- packages/bruno-app/src/utils/network/index.js | 63 ++- .../bruno-electron/src/ipc/network/index.js | 475 ++++++++++-------- 10 files changed, 472 insertions(+), 327 deletions(-) create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js create mode 100644 packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index bcfd07f24..2c0fb3dd1 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -5,7 +5,7 @@ import { requestUrlChanged, updateRequestMethod } from 'providers/ReduxStore/sli import { saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import HttpMethodSelector from './HttpMethodSelector'; import { useTheme } from 'providers/Theme'; -import { IconDeviceFloppy, IconArrowRight, IconCode } from '@tabler/icons'; +import { IconDeviceFloppy, IconArrowRight, IconCode, IconX } from '@tabler/icons'; import SingleLineEditor from 'components/SingleLineEditor'; import { isMacOS } from 'utils/common/platform'; import { hasRequestChanges } from 'utils/collections'; @@ -87,7 +87,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
gRPC
- + ) : ( )} @@ -149,7 +149,13 @@ const QueryUrl = ({ item, collection, handleRun }) => { Save ({saveShortcut}) - + { + item.response?.hasStreamRunning ? ( + + ) : ( + + ) + } {generateCodeItemModalOpen && ( diff --git a/packages/bruno-app/src/components/RequestTabPanel/index.js b/packages/bruno-app/src/components/RequestTabPanel/index.js index f7880e509..a347c1d0a 100644 --- a/packages/bruno-app/src/components/RequestTabPanel/index.js +++ b/packages/bruno-app/src/components/RequestTabPanel/index.js @@ -10,7 +10,7 @@ import GrpcResponsePane from 'components/ResponsePane/GrpcResponsePane'; import Welcome from 'components/Welcome'; import { findItemInCollection } from 'utils/collections'; import { updateRequestPaneTabWidth } from 'providers/ReduxStore/slices/tabs'; -import { sendRequest } from 'providers/ReduxStore/slices/collections/actions'; +import { cancelRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions'; import RequestNotFound from './RequestNotFound'; import QueryUrl from 'components/RequestPane/QueryUrl/index'; import GrpcQueryUrl from 'components/RequestPane/GrpcQueryUrl/index'; @@ -74,8 +74,7 @@ const RequestTabPanel = () => { const screenWidth = useSelector((state) => state.app.screenWidth); let asideWidth = useSelector((state) => state.app.leftSidebarWidth); const [leftPaneWidth, setLeftPaneWidth] = useState( - focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2 - ); // 2.2 is intentional to make both panes appear to be of equal width + focusedTab && focusedTab.requestPaneWidth ? focusedTab.requestPaneWidth : (screenWidth - asideWidth) / 2.2); // 2.2 is intentional to make both panes appear to be of equal width const [topPaneHeight, setTopPaneHeight] = useState(focusedTab?.requestPaneHeight || MIN_TOP_PANE_HEIGHT); const [dragging, setDragging] = useState(false); const dragOffset = useRef({ x: 0, y: 0 }); @@ -141,12 +140,10 @@ const RequestTabPanel = () => { setDragging(false); if (!isVerticalLayout) { const mainRect = mainSectionRef.current.getBoundingClientRect(); - dispatch( - updateRequestPaneTabWidth({ - uid: activeTabUid, - requestPaneWidth: e.clientX - mainRect.left - }) - ); + dispatch(updateRequestPaneTabWidth({ + uid: activeTabUid, + requestPaneWidth: e.clientX - mainRect.left + })); } } }; @@ -263,11 +260,17 @@ const RequestTabPanel = () => { return; } - dispatch(sendRequest(item, collection.uid)).catch((err) => - toast.custom((t) => toast.dismiss(t.id)} />, { - duration: 5000 - }) - ); + if (item.response?.hasStreamRunning) { + dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) => + toast.custom((t) => toast.dismiss(t.id)} />, { + duration: 5000 + })); + } else if (item.requestState !== 'sending' && item.requestState !== 'queued') { + dispatch(sendRequest(item, collection.uid)).catch((err) => + toast.custom((t) => toast.dismiss(t.id)} />, { + duration: 5000 + })); + } }; // TODO: reaper, improve selection of panes diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js new file mode 100644 index 000000000..c2fe19cfb --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/StyledWrapper.js @@ -0,0 +1,10 @@ +import styled from 'styled-components'; + +const Wrapper = styled.div` + font-size: 0.75rem; + font-weight: 600; + color: ${(props) => props.theme.requestTabPanel.responseStatus}; + text-align: center; +`; + +export default Wrapper; diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js new file mode 100644 index 000000000..ef96bc891 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ResponseStopWatch/index.js @@ -0,0 +1,27 @@ +import React, { useState, useEffect } from 'react'; +import StyledWrapper from './StyledWrapper'; + +const ResponseStopWatch = ({ startMillis }) => { + const [milliseconds, setMilliseconds] = useState(startMillis); + + const tickInterval = 100; + const tick = () => { + setMilliseconds(_milliseconds => _milliseconds + tickInterval); + }; + + useEffect(() => { + let timerID = setInterval(() => { + tick() + }, tickInterval); + return () => { + clearTimeout(timerID); + }; + }, []); + + let seconds = milliseconds / 1000; + let secondsFormatted = `${seconds.toFixed(1)}s`; + let width = secondsFormatted.length * 0.4; // Calculate width manually to stop parent layout from "flickering" by changing width too fast + return {secondsFormatted}; +}; + +export default React.memo(ResponseStopWatch); diff --git a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js index 222aad9e8..302b9a5e2 100644 --- a/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js +++ b/packages/bruno-app/src/components/ResponsePane/StatusCode/index.js @@ -4,7 +4,7 @@ import statusCodePhraseMap from './get-status-code-phrase'; import StyledWrapper from './StyledWrapper'; // Todo: text-error class is not getting pulled in for 500 errors -const StatusCode = ({ status, statusText }) => { +const StatusCode = ({ status, statusText, isStreaming }) => { const getTabClassname = (status) => { return classnames('ml-2', { 'text-ok': status >= 100 && status < 200, @@ -17,7 +17,7 @@ const StatusCode = ({ status, statusText }) => { return ( - {status} {statusText || statusCodePhraseMap[status]} + {status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null} ); }; diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index 6feb98bdb..2f173280d 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -21,6 +21,8 @@ import ResponseClear from 'src/components/ResponsePane/ResponseClear'; import ResponseBookmark from 'src/components/ResponsePane/ResponseBookmark'; import SkippedRequest from './SkippedRequest'; import ClearTimeline from './ClearTimeline/index'; +import StopWatch from 'components/StopWatch'; +import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch'; import ResponseLayoutToggle from './ResponseLayoutToggle'; import HeightBoundContainer from 'ui/HeightBoundContainer'; @@ -87,15 +89,17 @@ const ResponsePane = ({ item, collection }) => { return ; } case 'timeline': { - return ; + return ; } case 'tests': { - return ; + return ( + + ); } default: { @@ -184,7 +188,10 @@ const ResponsePane = ({ item, collection }) => { - + + {item.response?.hasStreamRunning ? ( + + ) : } @@ -193,7 +200,7 @@ const ResponsePane = ({ item, collection }) => { ) : null}
{ onClose={() => setShowScriptErrorCard(false)} /> )} -
+
{!item?.response ? ( - focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? ( + focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? ( { dispatch(processEnvUpdateEvent(val)); }); - const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { - console[val.type](...val.args); + const removeConsoleLogListener = ipcRenderer.on('main:console-log', (val) => { + console[val.type](...val.args); dispatch(addLog({ type: val.type, args: val.args, @@ -188,6 +190,14 @@ const useIpcEvents = () => { dispatch(collectionAddOauth2CredentialsByUrl(payload)); }); + const removeHttpStreamNewDataListener = ipcRenderer.on('main:http-stream-new-data', (val) => { + dispatch(streamDataReceived(val)); + }); + + const removeHttpStreamEndListener = ipcRenderer.on('main:http-stream-end', (val) => { + dispatch(requestCancelled(val)); + }); + const removeCollectionLoadingStateListener = ipcRenderer.on('main:collection-loading-state-updated', (val) => { dispatch(updateCollectionLoadingState(val)); }); @@ -212,6 +222,8 @@ const useIpcEvents = () => { removeGlobalEnvironmentsUpdatesListener(); removeSnapshotHydrationListener(); removeCollectionOauth2CredentialsUpdatesListener(); + removeHttpStreamNewDataListener(); + removeHttpStreamEndListener(); removeCollectionLoadingStateListener(); removePersistentEnvVariablesUpdateListener(); removeSystemResourcesListener(); diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index e6c7ae880..eafba26bd 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -83,8 +83,8 @@ const initiatedGrpcResponse = { isError: false, duration: 0, responses: [], - timestamp: Date.now(), -} + timestamp: Date.now() +}; const initiatedWsResponse = { status: 'PENDING', @@ -380,7 +380,15 @@ export const collectionsSlice = createSlice({ if (collection) { const item = findItemInCollection(collection, itemUid); if (item) { - item.response = null; + if (item.response?.hasStreamRunning) { + item.response.hasStreamRunning = null; + + const startTimestamp = item.requestSent.timestamp; + item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration; + } else { + item.response = null; + } + item.cancelTokenUid = null; item.requestUid = null; item.requestStartTime = null; @@ -389,22 +397,22 @@ export const collectionsSlice = createSlice({ }, responseReceived: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); - + if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); if (item) { item.requestState = 'received'; item.response = action.payload.response; - item.cancelTokenUid = null; + item.cancelTokenUid = item.response.hasStreamRunning ? item.cancelTokenUid : null; item.requestStartTime = null; if (!collection.timeline) { collection.timeline = []; } - + // Ensure timestamp is a number (milliseconds since epoch) - const timestamp = item?.requestSent?.timestamp instanceof Date - ? item.requestSent.timestamp.getTime() + const timestamp = item?.requestSent?.timestamp instanceof Date + ? item.requestSent.timestamp.getTime() : item?.requestSent?.timestamp || Date.now(); // Append the new timeline entry with numeric timestamp @@ -427,7 +435,7 @@ export const collectionsSlice = createSlice({ const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) return; - + const item = findItemInCollection(collection, itemUid); if (!item) return; const request = item.draft ? item.draft.request : item.request; @@ -447,7 +455,7 @@ export const collectionsSlice = createSlice({ } collection.timeline.push({ - type: "request", + type: 'request', eventType: eventType, // Add the specific gRPC event type collectionUid: collection.uid, folderUid: null, @@ -456,36 +464,34 @@ export const collectionsSlice = createSlice({ data: { request: eventData || item.requestSent || item.request, timestamp: Date.now(), - eventData: eventData, + eventData: eventData } }); - }, grpcResponseReceived: (state, action) => { const { itemUid, collectionUid, eventType, eventData } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - + if (!collection) return; const item = findItemInCollection(collection, itemUid); if (!item) return; - + // Get current response state or create initial state - const currentResponse = item.response || initiatedGrpcResponse + const currentResponse = item.response || initiatedGrpcResponse; const timestamp = item?.requestSent?.timestamp; let updatedResponse = { ...currentResponse, duration: Date.now() - (timestamp || Date.now()) }; - // Process based on event type switch (eventType) { case 'response': const { error, res } = eventData; - + // Handle error if present if (error) { const errorCode = error.code || 2; // Default to UNKNOWN if no code - + updatedResponse.error = error.details || 'gRPC error occurred'; updatedResponse.statusCode = errorCode; updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN'; @@ -494,72 +500,72 @@ export const collectionsSlice = createSlice({ } // Add response to list - updatedResponse.responses = res - ? [...(currentResponse?.responses || []), res] + updatedResponse.responses = res + ? [...(currentResponse?.responses || []), res] : [...(currentResponse?.responses || [])]; break; - + case 'metadata': updatedResponse.headers = eventData.metadata; updatedResponse.metadata = eventData.metadata; break; - + case 'status': // Extract status info const statusCode = eventData.status?.code; const statusDetails = eventData.status?.details; const statusMetadata = eventData.status?.metadata; - + // Set status based on actual code and details updatedResponse.statusCode = statusCode; updatedResponse.statusText = grpcStatusCodes[statusCode] || 'UNKNOWN'; updatedResponse.statusDescription = statusDetails; updatedResponse.statusDetails = eventData.status; - + // Store trailers (status metadata) if (statusMetadata) { updatedResponse.trailers = statusMetadata; } - + // Handle error status (non-zero code) if (statusCode !== 0) { updatedResponse.isError = true; updatedResponse.error = statusDetails || `gRPC error with code ${statusCode} (${updatedResponse.statusText})`; } - + break; - + case 'error': // Extract error details const errorCode = eventData.error?.code || 2; // Default to UNKNOWN if no code const errorDetails = eventData.error?.details || eventData.error?.message; const errorMetadata = eventData.error?.metadata; - + updatedResponse.isError = true; updatedResponse.error = errorDetails || 'Unknown gRPC error'; updatedResponse.statusCode = errorCode; updatedResponse.statusText = grpcStatusCodes[errorCode] || 'UNKNOWN'; updatedResponse.statusDescription = errorDetails; - + // Store error metadata as trailers if present if (errorMetadata) { updatedResponse.trailers = errorMetadata; } - + break; - + case 'end': - state.activeConnections = state.activeConnections.filter(id => id !== itemUid); + state.activeConnections = state.activeConnections.filter((id) => id !== itemUid); break; - + case 'cancel': updatedResponse.statusCode = 1; // CANCELLED updatedResponse.statusText = 'CANCELLED'; updatedResponse.statusDescription = 'Stream cancelled by client or server'; - state.activeConnections = state.activeConnections.filter(id => id !== itemUid); + state.activeConnections = state.activeConnections.filter((id) => id !== itemUid); break; } - + item.requestState = 'received'; item.response = updatedResponse; @@ -570,7 +576,7 @@ export const collectionsSlice = createSlice({ // Append the new timeline entry with specific gRPC event type collection.timeline.push({ - type: "request", + type: 'request', eventType: eventType, // Add the specific gRPC event type collectionUid: collection.uid, folderUid: null, @@ -580,7 +586,7 @@ export const collectionsSlice = createSlice({ request: item.requestSent || item.request, response: updatedResponse, eventData: eventData, // Store the original event data - timestamp: Date.now(), + timestamp: Date.now() } }); }, @@ -590,6 +596,12 @@ export const collectionsSlice = createSlice({ if (collection) { const item = findItemInCollection(collection, action.payload.itemUid); if (item) { + if (item.response && item.response.hasStreamRunning) { + item.response.data = ''; + item.response.size = 0; + return; + } + item.response = null; } } @@ -916,7 +928,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - const existingOtherParams = item.draft.request.params?.filter(p => p.type !== 'query') || []; + const existingOtherParams = item.draft.request.params?.filter((p) => p.type !== 'query') || []; const newQueryParams = map(params, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name, @@ -930,9 +942,7 @@ export const collectionsSlice = createSlice({ // Update the request URL to reflect the new query params const parts = splitOnFirst(item.draft.request.url, '?'); - const query = stringifyQueryParams( - filter(item.draft.request.params, (p) => p.enabled && p.type === 'query') - ); + const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')); // If there are enabled query params, append them to the URL if (query && query.length) { @@ -1163,7 +1173,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - item.draft.request.headers = map(action.payload.headers, ({name = '', value = '', enabled = true}) => ({ + item.draft.request.headers = map(action.payload.headers, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name: name, value: value, @@ -1205,8 +1215,8 @@ export const collectionsSlice = createSlice({ if (!folder || !isItemAFolder(folder)) { return; } - - folder.root.request.headers = map(headers, ({name = '', value = '', enabled = true}) => ({ + + folder.root.request.headers = map(headers, ({ name = '', value = '', enabled = true }) => ({ uid: uuid(), name: name, value: value, @@ -1487,7 +1497,7 @@ export const collectionsSlice = createSlice({ if (!item.draft) { item.draft = cloneDeep(item); } - + switch (item.draft.request.body.mode) { case 'json': { item.draft.request.body.json = action.payload.content; @@ -1624,7 +1634,7 @@ export const collectionsSlice = createSlice({ const collection = findCollectionByUid(state.collections, action.payload.collectionUid); if (collection) { - const item = findItemInCollection(collection, action.payload.itemUid); + const item = findItemInCollection(collection, action.payload.itemUid); if (item && isItemARequest(item)) { if (!item.draft) { @@ -1875,7 +1885,7 @@ export const collectionsSlice = createSlice({ break; case 'ntlm': set(collection, 'draft.root.request.auth.ntlm', action.payload.content); - break; + break; case 'oauth2': set(collection, 'draft.root.request.auth.oauth2', action.payload.content); break; @@ -2604,7 +2614,7 @@ export const collectionsSlice = createSlice({ const { requestUid, itemUid, collectionUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); if (!collection) return; - + const item = findItemInCollection(collection, itemUid); if (!item) return; @@ -2637,7 +2647,7 @@ export const collectionsSlice = createSlice({ item.postResponseScriptErrorMessage = action.payload.errorMessage; } - if(type === 'test-script-execution') { + if (type === 'test-script-execution') { item.testScriptErrorMessage = action.payload.errorMessage; } @@ -2652,7 +2662,7 @@ export const collectionsSlice = createSlice({ if (type === 'request-sent') { const { cancelTokenUid, requestSent } = action.payload; item.requestSent = requestSent; - + // sometimes the response is received before the request-sent event arrives if (item.requestState === 'queued') { item.requestState = 'sending'; @@ -2669,12 +2679,12 @@ export const collectionsSlice = createSlice({ const { results } = action.payload; item.testResults = results; } - + if (type === 'test-results-pre-request') { const { results } = action.payload; item.preRequestTestResults = results; } - + if (type === 'test-results-post-response') { const { results } = action.payload; item.postResponseTestResults = results; @@ -2788,7 +2798,7 @@ export const collectionsSlice = createSlice({ if (collection) { collection.runnerResult = null; - collection.runnerTags = { include: [], exclude: [] } + collection.runnerTags = { include: [], exclude: [] }; collection.runnerTagsEnabled = false; collection.runnerConfiguration = null; } @@ -2927,7 +2937,7 @@ export const collectionsSlice = createSlice({ updateFolderAuthMode: (state, action) => { const collection = findCollectionByUid(state.collections, action.payload.collectionUid); const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null; - + if (folder) { if (!folder.draft) { folder.draft = cloneDeep(folder.root); @@ -2936,7 +2946,16 @@ export const collectionsSlice = createSlice({ set(folder, 'draft.request.auth.mode', action.payload.mode); } }, + streamDataReceived: (state, action) => { + const { itemUid, collectionUid, data } = action.payload; + const collection = findCollectionByUid(state.collections, collectionUid); + if (collection) { + const item = findItemInCollection(collection, itemUid); + item.response.data = data.data + (item.response.data || ''); + item.response.size = data.data?.length + (item.response.size || 0); + } + }, addRequestTag: (state, action) => { const { tag, collectionUid, itemUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); @@ -2978,7 +2997,7 @@ export const collectionsSlice = createSlice({ updateCollectionTagsList: (state, action) => { const { collectionUid } = action.payload; const collection = findCollectionByUid(state.collections, collectionUid); - + if (collection) { collection.allTags = getUniqueTagsFromItems(collection.items); } @@ -3298,6 +3317,7 @@ export const { updateRequestDocs, updateFolderDocs, moveCollection, + streamDataReceived, collectionAddOauth2CredentialsByUrl, collectionClearOauth2CredentialsByUrl, collectionGetOauth2CredentialsByUrl, diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js index 7c1e3b74a..c788a1a93 100644 --- a/packages/bruno-app/src/utils/network/index.js +++ b/packages/bruno-app/src/utils/network/index.js @@ -20,7 +20,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV status: response.status, statusText: response.statusText, duration: response.duration, - timeline: response.timeline + timeline: response.timeline, + hasStreamRunning: response.hasStreamRunning }); }) .catch((err) => reject(err)); @@ -31,19 +32,17 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV export const sendGrpcRequest = async (item, collection, environment, runtimeVariables) => { return new Promise((resolve, reject) => { startGrpcRequest(item, collection, environment, runtimeVariables) - .then((initialState) => { - // Return an initial state object to update the UI - // The real response data will be handled by event listeners - resolve({ - ...initialState, - timeline: [] - }); - }) - .catch((err) => reject(err)); + .then((initialState) => { + // Return an initial state object to update the UI + // The real response data will be handled by event listeners + resolve({ + ...initialState, + timeline: [] + }); + }) + .catch((err) => reject(err)); }); -} - - +}; const sendHttpRequest = async (item, collection, environment, runtimeVariables) => { return new Promise((resolve, reject) => { @@ -83,19 +82,19 @@ export const startGrpcRequest = async (item, collection, environment, runtimeVar return new Promise((resolve, reject) => { const { ipcRenderer } = window; const request = item.draft ? item.draft : item; - + ipcRenderer.invoke('grpc:start-connection', { - request, - collection, - environment, + request, + collection, + environment, runtimeVariables }) - .then(() => { - resolve(); - }) - .catch(err => { - reject(err); - }); + .then(() => { + resolve(); + }) + .catch((err) => { + reject(err); + }); }); }; @@ -188,7 +187,7 @@ export const isGrpcConnectionActive = async (connectionId) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; ipcRenderer.invoke('grpc:is-connection-active', connectionId) - .then(response => { + .then((response) => { if (response.success) { resolve(response.isActive); } else { @@ -197,7 +196,7 @@ export const isGrpcConnectionActive = async (connectionId) => { resolve(false); } }) - .catch(err => { + .catch((err) => { console.error('Failed to check connection status:', err); // On error, assume the connection is not active resolve(false); @@ -215,14 +214,14 @@ export const isGrpcConnectionActive = async (connectionId) => { export const generateGrpcSampleMessage = async (methodPath, existingMessage = null, options = {}) => { return new Promise((resolve, reject) => { const { ipcRenderer } = window; - - ipcRenderer.invoke('grpc:generate-sample-message', { - methodPath, - existingMessage, - options + + ipcRenderer.invoke('grpc:generate-sample-message', { + methodPath, + existingMessage, + options }) - .then(resolve) - .catch(reject); + .then(resolve) + .catch(reject); }); }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 611182a27..14956c035 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -69,16 +69,44 @@ const getJsSandboxRuntime = (collection) => { return 'vm2'; }; -const configureRequest = async ( - collectionUid, +const isStream = (headers) => { + return headers.get('content-type') === 'text/event-stream'; +}; + +const promisifyStream = async (stream, abortController, closeOnFirst) => { + const chunks = []; + + return new Promise((resolve, reject) => { + const doResolve = () => { + const fullBuffer = Buffer.concat(chunks); + resolve(fullBuffer.buffer.slice(fullBuffer.byteOffset, fullBuffer.byteOffset + fullBuffer.byteLength)); + }; + + stream.on('data', (chunk) => { + chunks.push(chunk); + + if (closeOnFirst) { + doResolve(); + + if (abortController) { + abortController.abort(); + } + } + }); + + stream.on('close', doResolve); + stream.on('error', err => reject(err)); + }); +}; + +const configureRequest = async (collectionUid, collection, request, envVars, runtimeVariables, processEnvVars, collectionPath, - globalEnvironmentVariables -) => { + globalEnvironmentVariables) => { const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; if (!protocolRegex.test(request.url)) { request.url = `http://${request.url}`; @@ -97,7 +125,7 @@ const configureRequest = async ( // Get followRedirects setting, default to true for backward compatibility const followRedirects = request.settings?.followRedirects ?? true; - + // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5 let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5; @@ -138,14 +166,12 @@ const configureRequest = async ( request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; if (tokenPlacement == 'header' && credentials?.access_token) { request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials.access_token}`.trim(); - } - else { + } else { try { const url = new URL(request.url); url?.searchParams?.set(tokenQueryKey, credentials?.access_token); request.url = url?.toString(); - } - catch(error) {} + } catch (error) {} } break; case 'implicit': @@ -154,8 +180,7 @@ const configureRequest = async ( request.oauth2Credentials = { credentials, url: oauth2Url, collectionUid, credentialsId, debugInfo, folderUid: request.oauth2Credentials?.folderUid }; if (tokenPlacement == 'header') { request.headers['Authorization'] = `${tokenHeaderPrefix} ${credentials?.access_token}`; - } - else { + } else { try { const url = new URL(request.url); url?.searchParams?.set(tokenQueryKey, credentials?.access_token); @@ -217,8 +242,7 @@ const configureRequest = async ( if (preferencesUtil.shouldSendCookies()) { const cookieString = getCookieStringForUrl(request.url); if (cookieString && typeof cookieString === 'string' && cookieString.length) { - const existingCookieHeaderName = Object.keys(request.headers).find( - name => name.toLowerCase() === 'cookie' + const existingCookieHeaderName = Object.keys(request.headers).find((name) => name.toLowerCase() === 'cookie' ); const existingCookieString = existingCookieHeaderName ? request.headers[existingCookieHeaderName] : ''; @@ -282,8 +306,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col // Precedence: runtimeVars > requestVariables > folderVars > envVars > collectionVariables > globalEnvironmentVars const processEnvVars = getProcessEnvVars(collection.uid); - const resolvedVars = merge( - {}, + const resolvedVars = merge({}, globalEnvironmentVars, collectionVariables, envVars, @@ -296,8 +319,7 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col ...processEnvVars } } - } - ); + }); const collectionRoot = collection?.draft?.root || collection?.root || {}; const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot); @@ -314,16 +336,14 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col const collectionPath = collection.pathname; - const axiosInstance = await configureRequest( - collection.uid, + const axiosInstance = await configureRequest(collection.uid, collection, request, envVars, collection.runtimeVariables, processEnvVars, collectionPath, - collection.globalEnvironmentVariables - ); + collection.globalEnvironmentVariables); const response = await axiosInstance(request); @@ -358,10 +378,10 @@ const registerNetworkIpc = (mainWindow) => { }; const notifyScriptExecution = ({ - channel, // 'main:run-request-event' | 'main:run-folder-event' - basePayload, // request-level or runner-level identifiers - scriptType, // 'pre-request' | 'post-response' | 'test' - error // optional Error + channel, // 'main:run-request-event' | 'main:run-folder-event' + basePayload, // request-level or runner-level identifiers + scriptType, // 'pre-request' | 'post-response' | 'test' + error // optional Error }) => { mainWindow.webContents.send(channel, { type: `${scriptType}-script-execution`, @@ -370,8 +390,7 @@ const registerNetworkIpc = (mainWindow) => { }); }; - const runPreRequest = async ( - request, + const runPreRequest = async (request, requestUid, envVars, collectionPath, @@ -380,11 +399,10 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ) => { + runRequestByItemPathname) => { // run pre-request script let scriptResult; - const collectionName = collection?.name + const collectionName = collection?.name; const requestScript = get(request, 'script.req'); if (requestScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); @@ -462,8 +480,7 @@ const registerNetworkIpc = (mainWindow) => { return scriptResult; }; - const runPostResponse = async ( - request, + const runPostResponse = async (request, response, requestUid, envVars, @@ -520,7 +537,7 @@ const registerNetworkIpc = (mainWindow) => { // run post-response script const responseScript = get(request, 'script.res'); let scriptResult; - const collectionName = collection?.name + const collectionName = collection?.name; if (responseScript?.length) { const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); scriptResult = await scriptRuntime.runResponseScript( @@ -595,7 +612,9 @@ const registerNetworkIpc = (mainWindow) => { const abortController = new AbortController(); const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'standalone'; + request.responseType = "stream"; const brunoConfig = getBrunoConfig(collectionUid, collection); + const scriptingConfig = get(brunoConfig, 'scripts', {}); scriptingConfig.runtime = getJsSandboxRuntime(collection); @@ -606,8 +625,7 @@ const registerNetworkIpc = (mainWindow) => { let preRequestScriptResult = null; let preRequestError = null; try { - preRequestScriptResult = await runPreRequest( - request, + preRequestScriptResult = await runPreRequest(request, requestUid, envVars, collectionPath, @@ -616,8 +634,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { preRequestError = error; } @@ -642,16 +659,14 @@ const registerNetworkIpc = (mainWindow) => { if (preRequestError) { return Promise.reject(preRequestError); } - const axiosInstance = await configureRequest( - collectionUid, + const axiosInstance = await configureRequest(collectionUid, collection, request, envVars, runtimeVariables, processEnvVars, collectionPath, - collection.globalEnvironmentVariables - ); + collection.globalEnvironmentVariables); const { data: requestData, dataBuffer: requestDataBuffer } = parseDataFromRequest(request); @@ -668,8 +683,9 @@ const registerNetworkIpc = (mainWindow) => { method: request.method, headers: headersSent, data: requestData, - dataBuffer: requestDataBuffer - } + dataBuffer: requestDataBuffer, + timestamp: Date.now() + }; !runInBackground && mainWindow.webContents.send('main:run-request-event', { type: 'request-sent', @@ -695,6 +711,11 @@ const registerNetworkIpc = (mainWindow) => { try { /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); + request.isStream = isStream(response.headers); + + if (!request.isStream) { + response.data = await promisifyStream(response.data); + } // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); @@ -719,6 +740,11 @@ const registerNetworkIpc = (mainWindow) => { // Prevents the duration on leaking to the actual result responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); + + request.isStream = isStream(response.headers); + if (!request.isStream) { + response.data = await promisifyStream(response.data); + } } else { await executeRequestOnFailHandler(request, error); @@ -729,16 +755,21 @@ const registerNetworkIpc = (mainWindow) => { statusText: error.statusText, error: error.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST, timeline: error.timeline - } + }; } } // Continue with the rest of the request lifecycle - post response vars, script, assertions, tests - const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); - response.data = data; - response.dataBuffer = dataBuffer; + if (request.isStream) { + response.stream = response.data; + } + const { data, dataBuffer } = request.isStream + ? { data: '', dataBuffer: new ArrayBuffer(0) } + : parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); + + response.data = data; response.responseTime = responseTime; // save cookies @@ -754,138 +785,140 @@ const registerNetworkIpc = (mainWindow) => { let postResponseScriptResult = null; let postResponseError = null; - try { - postResponseScriptResult = await runPostResponse( - request, - response, - requestUid, - envVars, - collectionPath, - collection, - collectionUid, - runtimeVariables, - processEnvVars, - scriptingConfig, - runRequestByItemPathname - ); - } catch (error) { - console.error('Post-response script error:', error); - postResponseError = error; - } - - if (postResponseScriptResult?.results) { - mainWindow.webContents.send('main:run-request-event', { - type: 'test-results-post-response', - results: postResponseScriptResult.results, - itemUid: item.uid, - requestUid, - collectionUid - }); - } - - !runInBackground && notifyScriptExecution({ - channel: 'main:run-request-event', - basePayload: { requestUid, collectionUid, itemUid: item.uid }, - scriptType: 'post-response', - error: postResponseError - }); - - // run assertions - const assertions = get(request, 'assertions'); - if (assertions) { - const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); - const results = assertRuntime.runAssertions( - assertions, - request, - response, - envVars, - runtimeVariables, - processEnvVars - ); - - !runInBackground && mainWindow.webContents.send('main:run-request-event', { - type: 'assertion-results', - results: results, - itemUid: item.uid, - requestUid, - collectionUid - }); - } - - const testFile = get(request, 'tests'); - const collectionName = collection?.name - if (typeof testFile === 'string') { - const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - let testResults = null; - let testError = null; - + const runPostScripts = async () => { try { - testResults = await testRuntime.runTests( - decomment(testFile), - request, + postResponseScriptResult = await runPostResponse(request, response, + requestUid, envVars, - runtimeVariables, collectionPath, - onConsoleLog, + collection, + collectionUid, + runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname, - collectionName - ); + runRequestByItemPathname); } catch (error) { - testError = error; - - if (error.partialResults) { - testResults = error.partialResults; - } else { - testResults = { - request, - envVariables: envVars, - runtimeVariables, - globalEnvironmentVariables: request?.globalEnvironmentVariables || {}, - results: [], - nextRequestName: null - }; - } + console.error('Post-response script error:', error); + postResponseError = error; } - !runInBackground && mainWindow.webContents.send('main:run-request-event', { - type: 'test-results', - results: testResults.results, - itemUid: item.uid, - requestUid, - collectionUid - }); - - mainWindow.webContents.send('main:script-environment-update', { - envVariables: testResults.envVariables, - runtimeVariables: testResults.runtimeVariables, - requestUid, - collectionUid - }); - - mainWindow.webContents.send('main:persistent-env-variables-update', { - persistentEnvVariables: testResults.persistentEnvVariables, - collectionUid - }); - - mainWindow.webContents.send('main:global-environment-variables-update', { - globalEnvironmentVariables: testResults.globalEnvironmentVariables - }); - - collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + if (postResponseScriptResult?.results) { + mainWindow.webContents.send('main:run-request-event', { + type: 'test-results-post-response', + results: postResponseScriptResult.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } !runInBackground && notifyScriptExecution({ channel: 'main:run-request-event', basePayload: { requestUid, collectionUid, itemUid: item.uid }, - scriptType: 'test', - error: testError + scriptType: 'post-response', + error: postResponseError }); - const domainsWithCookiesTest = await getDomainsWithCookies(); - mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); - cookiesStore.saveCookieJar(); + // run assertions + const assertions = get(request, 'assertions'); + if (assertions) { + const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); + const results = assertRuntime.runAssertions(assertions, + request, + response, + envVars, + runtimeVariables, + processEnvVars); + + !runInBackground && mainWindow.webContents.send('main:run-request-event', { + type: 'assertion-results', + results: results, + itemUid: item.uid, + requestUid, + collectionUid + }); + } + + const testFile = get(request, 'tests'); + const collectionName = collection?.name; + if (typeof testFile === 'string') { + const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); + let testResults = null; + let testError = null; + + try { + testResults = await testRuntime.runTests(decomment(testFile), + request, + response, + envVars, + runtimeVariables, + collectionPath, + onConsoleLog, + processEnvVars, + scriptingConfig, + runRequestByItemPathname, + collectionName); + } catch (error) { + testError = error; + + if (error.partialResults) { + testResults = error.partialResults; + } else { + testResults = { + request, + envVariables: envVars, + runtimeVariables, + globalEnvironmentVariables: request?.globalEnvironmentVariables || {}, + results: [], + nextRequestName: null + }; + } + } + + !runInBackground && mainWindow.webContents.send('main:run-request-event', { + type: 'test-results', + results: testResults.results, + itemUid: item.uid, + requestUid, + collectionUid + }); + + mainWindow.webContents.send('main:script-environment-update', { + envVariables: testResults.envVariables, + runtimeVariables: testResults.runtimeVariables, + requestUid, + collectionUid + }); + + mainWindow.webContents.send('main:persistent-env-variables-update', { + persistentEnvVariables: testResults.persistentEnvVariables, + collectionUid + }); + + mainWindow.webContents.send('main:global-environment-variables-update', { + globalEnvironmentVariables: testResults.globalEnvironmentVariables + }); + + collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; + + !runInBackground && notifyScriptExecution({ + channel: 'main:run-request-event', + basePayload: { requestUid, collectionUid, itemUid: item.uid }, + scriptType: 'test', + error: testError + }); + + const domainsWithCookiesTest = await getDomainsWithCookies(); + mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookiesTest))); + cookiesStore.saveCookieJar(); + } + }; + + if (request.isStream) { + response.stream.on('close', () => runPostScripts().then()); + } else { + await runPostScripts(); } return { @@ -893,8 +926,8 @@ const registerNetworkIpc = (mainWindow) => { statusText: response.statusText, headers: response.headers, data: response.data, - dataBuffer: response.dataBuffer.toString('base64'), - size: Buffer.byteLength(response.dataBuffer), + stream: request.isStream ? response.stream : null, + cancelTokenUid: cancelTokenUid, duration: responseTime ?? 0, url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null, timeline: response.timeline @@ -917,7 +950,27 @@ const registerNetworkIpc = (mainWindow) => { const collectionUid = collection.uid; const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); - return await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false }); + const response = await runRequest({ item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: false }); + if (response.stream) { + const stream = response.stream; + response.stream = undefined; + response.hasStreamRunning = response.status >= 200 && response.status < 300; + + stream.on('data', newData => { + const parsed = parseDataFromResponse({ data: newData, headers: {} }); + mainWindow.webContents.send('main:http-stream-new-data', {collectionUid, itemUid: item.uid, data: parsed}); + }); + + stream.on('close', () => { + if (!cancelTokens[response.cancelTokenUid]) { + return; + } + + mainWindow.webContents.send('main:http-stream-end', {collectionUid, itemUid: item.uid}); + deleteCancelToken(response.cancelTokenUid); + }); + } + return response; }); ipcMain.handle('clear-oauth2-cache', async (event, uid, url, credentialsId) => { @@ -935,8 +988,9 @@ const registerNetworkIpc = (mainWindow) => { ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => { return new Promise((resolve, reject) => { if (cancelTokenUid && cancelTokens[cancelTokenUid]) { - cancelTokens[cancelTokenUid].abort(); + const abortController = cancelTokens[cancelTokenUid]; deleteCancelToken(cancelTokenUid); + abortController.abort(); // Ensure the on stream end event is called after the token is deleted resolve(); } else { reject(new Error('cancel token not found')); @@ -945,7 +999,7 @@ const registerNetworkIpc = (mainWindow) => { }); // handler for fetch-gql-schema - ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler) + ipcMain.handle('fetch-gql-schema', fetchGqlSchemaHandler); ipcMain.handle( 'renderer:run-collection-folder', @@ -960,10 +1014,17 @@ const registerNetworkIpc = (mainWindow) => { const envVars = getEnvVars(environment); const processEnvVars = getProcessEnvVars(collectionUid); let stopRunnerExecution = false; + let currentAbortController; const abortController = new AbortController(); saveCancelToken(cancelTokenUid, abortController); + abortController.signal.addEventListener('abort', () => { + if (currentAbortController) { + currentAbortController.abort(); + } + }); + const runRequestByItemPathname = async (relativeItemPathname) => { return new Promise(async (resolve, reject) => { let itemPathname = path.join(collection?.pathname, relativeItemPathname); @@ -972,7 +1033,7 @@ const registerNetworkIpc = (mainWindow) => { } const _item = cloneDeep(findItemInCollectionByPathname(collection, itemPathname)); if(_item) { - const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); + const res = await runRequest({ item: _item, collection, envVars, processEnvVars, runtimeVariables, runInBackground: true }); resolve(res); } reject(`bru.runRequest: invalid request path - ${itemPathname}`); @@ -1004,9 +1065,8 @@ const registerNetworkIpc = (mainWindow) => { } }); - // sort requests by seq property - folderRequests = sortByNameThenSequence(folderRequests) + folderRequests = sortByNameThenSequence(folderRequests); } // Filter requests based on tags @@ -1015,7 +1075,7 @@ const registerNetworkIpc = (mainWindow) => { const excludeTags = tags.exclude ? tags.exclude : []; folderRequests = folderRequests.filter(({ tags: requestTags = [], draft }) => { requestTags = draft?.tags || requestTags || []; - return isRequestTagsIncluded(requestTags, includeTags, excludeTags) + return isRequestTagsIncluded(requestTags, includeTags, excludeTags); }); } @@ -1068,15 +1128,14 @@ const registerNetworkIpc = (mainWindow) => { const request = await prepareRequest(item, collection, abortController); request.__bruno__executionMode = 'runner'; - + const requestUid = uuid(); try { let preRequestScriptResult; let preRequestError = null; try { - preRequestScriptResult = await runPreRequest( - request, + preRequestScriptResult = await runPreRequest(request, requestUid, envVars, collectionPath, @@ -1085,8 +1144,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { console.error('Pre-request script error:', error); preRequestError = error; @@ -1154,8 +1212,9 @@ const registerNetworkIpc = (mainWindow) => { method: request.method, headers: headersSent, data: requestData, - dataBuffer: requestDataBuffer - } + dataBuffer: requestDataBuffer, + timestamp: Date.now() + }; // todo: // i have no clue why electron can't send the request object @@ -1166,9 +1225,11 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); - request.signal = abortController.signal; - const axiosInstance = await configureRequest( - collectionUid, + currentAbortController = new AbortController(); + request.signal = currentAbortController.signal; + request.responseType = 'stream'; + + const axiosInstance = await configureRequest(collectionUid, collection, request, envVars, @@ -1185,7 +1246,7 @@ const registerNetworkIpc = (mainWindow) => { collectionUid, credentialsId: request?.oauth2Credentials?.credentialsId, ...(request?.oauth2Credentials?.folderUid ? { folderUid: request.oauth2Credentials.folderUid } : { itemUid: item.uid }), - debugInfo: request?.oauth2Credentials?.debugInfo, + debugInfo: request?.oauth2Credentials?.debugInfo }); collection.oauth2Credentials = updateCollectionOauth2Credentials({ @@ -1213,6 +1274,10 @@ const registerNetworkIpc = (mainWindow) => { /** @type {import('axios').AxiosResponse} */ response = await axiosInstance(request); + + request.isStream = isStream(response.headers); + response.data = await promisifyStream(response.data, currentAbortController, true); + timeEnd = Date.now(); const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson); @@ -1254,6 +1319,9 @@ const registerNetworkIpc = (mainWindow) => { } if (error?.response) { + request.isStream = isStream(error.response.headers); + error.response.data = await promisifyStream(error.response.data, currentAbortController, true); + const { data, dataBuffer } = parseDataFromResponse(error.response); error.response.responseTime = error.response.headers.get('request-duration'); error.response.headers.delete('request-duration'); @@ -1270,7 +1338,7 @@ const registerNetworkIpc = (mainWindow) => { size: Buffer.byteLength(dataBuffer), data: error.response.data, responseTime: error.response.responseTime, - timeline: error.response.timeline, + timeline: error.response.timeline }; // if we get a response from the server, we consider it as a success @@ -1291,8 +1359,7 @@ const registerNetworkIpc = (mainWindow) => { let postResponseScriptResult; let postResponseError = null; try { - postResponseScriptResult = await runPostResponse( - request, + postResponseScriptResult = await runPostResponse(request, response, requestUid, envVars, @@ -1302,8 +1369,7 @@ const registerNetworkIpc = (mainWindow) => { runtimeVariables, processEnvVars, scriptingConfig, - runRequestByItemPathname - ); + runRequestByItemPathname); } catch (error) { console.error('Post-response script error:', error); postResponseError = error; @@ -1340,14 +1406,12 @@ const registerNetworkIpc = (mainWindow) => { const assertions = get(item, 'request.assertions'); if (assertions) { const assertRuntime = new AssertRuntime({ runtime: scriptingConfig?.runtime }); - const results = assertRuntime.runAssertions( - assertions, + const results = assertRuntime.runAssertions(assertions, request, response, envVars, runtimeVariables, - processEnvVars - ); + processEnvVars); mainWindow.webContents.send('main:run-folder-event', { type: 'assertion-results', @@ -1358,15 +1422,14 @@ const registerNetworkIpc = (mainWindow) => { } const testFile = get(request, 'tests'); - const collectionName = collection?.name + const collectionName = collection?.name; if (typeof testFile === 'string') { let testResults = null; let testError = null; try { const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); - testResults = await testRuntime.runTests( - decomment(testFile), + testResults = await testRuntime.runTests(decomment(testFile), request, response, envVars, @@ -1376,11 +1439,10 @@ const registerNetworkIpc = (mainWindow) => { processEnvVars, scriptingConfig, runRequestByItemPathname, - collectionName - ); + collectionName); } catch (error) { testError = error; - + if (error.partialResults) { testResults = error.partialResults; } else { @@ -1414,7 +1476,7 @@ const registerNetworkIpc = (mainWindow) => { mainWindow.webContents.send('main:global-environment-variables-update', { globalEnvironmentVariables: testResults.globalEnvironmentVariables }); - + collection.globalEnvironmentVariables = testResults.globalEnvironmentVariables; notifyScriptExecution({ @@ -1443,7 +1505,7 @@ const registerNetworkIpc = (mainWindow) => { collectionUid, folderUid, statusText: 'collection run was terminated!', - runCompletionTime: new Date().toISOString(), + runCompletionTime: new Date().toISOString() }); break; } @@ -1460,7 +1522,7 @@ const registerNetworkIpc = (mainWindow) => { if (nextRequestIdx >= 0) { currentRequestIndex = nextRequestIdx; } else { - console.error("Could not find request with name '" + nextRequestName + "'"); + console.error('Could not find request with name \'' + nextRequestName + '\''); currentRequestIndex++; } } else { @@ -1473,10 +1535,10 @@ const registerNetworkIpc = (mainWindow) => { type: 'testrun-ended', collectionUid, folderUid, - runCompletionTime: new Date().toISOString(), + runCompletionTime: new Date().toISOString() }); } catch (error) { - console.log("error", error); + console.log('error', error); deleteCancelToken(cancelTokenUid); mainWindow.webContents.send('main:run-folder-event', { type: 'testrun-ended', @@ -1573,14 +1635,13 @@ const executeRequestOnFailHandler = async (request, error) => { } }; - const registerAllNetworkIpc = (mainWindow) => { registerNetworkIpc(mainWindow); registerGrpcEventHandlers(mainWindow); registerWsEventHandlers(mainWindow); -} +}; -module.exports = registerAllNetworkIpc +module.exports = registerAllNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler;