mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-15 20:01:28 +00:00
Merge pull request #6074 from usebruno/feature/http-stream-internal
Feature: HTTP Streaming
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -26860,6 +26860,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"graphiql": "3.7.1",
|
||||
"graphql": "^16.6.0",
|
||||
"graphql-request": "^3.7.0",
|
||||
"hexy": "^0.3.5",
|
||||
"httpsnippet": "^3.0.9",
|
||||
"i18next": "24.1.2",
|
||||
"idb": "^7.0.0",
|
||||
|
||||
@@ -41,9 +41,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
if (!editorRef.current?.editor) return;
|
||||
const editor = editorRef.current.editor;
|
||||
const cursor = editor.getCursor();
|
||||
|
||||
|
||||
const finalUrl = value?.trim() ?? value;
|
||||
|
||||
|
||||
dispatch(
|
||||
requestUrlChanged({
|
||||
itemUid: item.uid,
|
||||
@@ -51,7 +51,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
url: finalUrl
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
// Restore cursor position only if URL was trimmed
|
||||
if (finalUrl !== value) {
|
||||
setTimeout(() => {
|
||||
@@ -81,7 +81,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelRequest = () => {
|
||||
const handleCancelRequest = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection));
|
||||
};
|
||||
|
||||
@@ -92,7 +94,6 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
<div className="flex items-center justify-center h-full w-16">
|
||||
<span className="text-xs text-indigo-500 font-bold">gRPC</span>
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<HttpMethodSelector method={method} onMethodSelect={onMethodSelect} />
|
||||
)}
|
||||
@@ -126,15 +127,8 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
handleGenerateCode(e);
|
||||
}}
|
||||
>
|
||||
<IconCode
|
||||
color={theme.requestTabs.icon.color}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className={'cursor-pointer'}
|
||||
/>
|
||||
<span className="infotiptext text-xs">
|
||||
Generate Code
|
||||
</span>
|
||||
<IconCode color={theme.requestTabs.icon.color} strokeWidth={1.5} size={22} className="cursor-pointer" />
|
||||
<span className="infotiptext text-xs">Generate Code</span>
|
||||
</div>
|
||||
<div
|
||||
title="Save Request"
|
||||
@@ -155,10 +149,9 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
Save <span className="shortcut">({saveShortcut})</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
{isLoading || item.response?.stream?.running ? (
|
||||
<IconSquareRoundedX
|
||||
color={theme.requestTabPanel.url.icon}
|
||||
color={theme.requestTabPanel.url.iconDanger}
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
data-testid="cancel-request-icon"
|
||||
@@ -175,7 +168,11 @@ const QueryUrl = ({ item, collection, handleRun }) => {
|
||||
</div>
|
||||
</div>
|
||||
{generateCodeItemModalOpen && (
|
||||
<GenerateCodeItem collectionUid={collection.uid} item={item} onClose={() => setGenerateCodeItemModalOpen(false)} />
|
||||
<GenerateCodeItem
|
||||
collectionUid={collection.uid}
|
||||
item={item}
|
||||
onClose={() => setGenerateCodeItemModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
@@ -263,11 +263,17 @@ const RequestTabPanel = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
})
|
||||
);
|
||||
if (item.response?.stream?.running) {
|
||||
dispatch(cancelRequest(item.cancelTokenUid, item, collection)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
} else if (item.requestState !== 'sending' && item.requestState !== 'queued') {
|
||||
dispatch(sendRequest(item, collection.uid)).catch((err) =>
|
||||
toast.custom((t) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
|
||||
duration: 5000
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: reaper, improve selection of panes
|
||||
|
||||
@@ -12,12 +12,25 @@ import { getInitialExampleName } from 'utils/collections/index';
|
||||
import classnames from 'classnames';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const getTitleText = ({ isResponseTooLarge, isStreamingResponse }) => {
|
||||
if (isStreamingResponse) {
|
||||
return 'Response Examples aren\'t supported in streaming responses yet.';
|
||||
}
|
||||
|
||||
if (isResponseTooLarge) {
|
||||
return 'Response size exceeds 5MB limit. Cannot save as example.';
|
||||
}
|
||||
|
||||
return 'Save current response as example';
|
||||
};
|
||||
|
||||
const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [showSaveResponseExampleModal, setShowSaveResponseExampleModal] = useState(false);
|
||||
const response = item.response || {};
|
||||
|
||||
const isResponseTooLarge = responseSize >= 5 * 1024 * 1024; // 5 MB
|
||||
const isStreamingResponse = response.stream;
|
||||
|
||||
// Only show for HTTP requests
|
||||
if (item.type !== 'http-request') {
|
||||
@@ -96,19 +109,22 @@ const ResponseBookmark = ({ item, collection, responseSize }) => {
|
||||
toast.success(`Example "${name}" created successfully`);
|
||||
};
|
||||
|
||||
const disabledMessage = getTitleText({
|
||||
isResponseTooLarge,
|
||||
isStreamingResponse
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledWrapper className="ml-2 flex items-center">
|
||||
<button
|
||||
onClick={handleSaveClick}
|
||||
disabled={isResponseTooLarge}
|
||||
disabled={isResponseTooLarge || isStreamingResponse}
|
||||
title={
|
||||
isResponseTooLarge
|
||||
? 'Response size exceeds 5MB limit. Cannot save as example.'
|
||||
: 'Save current response as example'
|
||||
disabledMessage
|
||||
}
|
||||
className={classnames('p-1', {
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge
|
||||
'opacity-50 cursor-not-allowed': isResponseTooLarge || isStreamingResponse
|
||||
})}
|
||||
data-testid="response-bookmark-btn"
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
@@ -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 () => {
|
||||
clearInterval(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 <StyledWrapper className="ml-4" style={{ width: `${width}rem` }}>{secondsFormatted}</StyledWrapper>;
|
||||
};
|
||||
|
||||
export default React.memo(ResponseStopWatch);
|
||||
@@ -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 (
|
||||
<StyledWrapper className={`response-status-code ${getTabClassname(status)}`} data-testid="response-status-code">
|
||||
{status} {statusText || statusCodePhraseMap[status]}
|
||||
{status} {statusText || statusCodePhraseMap[status]} {isStreaming ? ' - STREAMING' : null}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -195,7 +195,7 @@ const WSMessagesList = ({ order = -1, messages = [] }) => {
|
||||
<StyledWrapper className="ws-messages-list mt-1 flex flex-col">
|
||||
{ordered.map((msg, idx, src) => {
|
||||
const inFocus = order === -1 ? src.length - 1 === idx : idx === 0;
|
||||
return <WSMessageItem inFocus={inFocus} id={idx} message={msg} />;
|
||||
return <WSMessageItem key={msg.timestamp} inFocus={inFocus} id={idx} message={msg} />;
|
||||
})}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ import SkippedRequest from './SkippedRequest';
|
||||
import ClearTimeline from './ClearTimeline/index';
|
||||
import ResponseLayoutToggle from './ResponseLayoutToggle';
|
||||
import HeightBoundContainer from 'ui/HeightBoundContainer';
|
||||
import ResponseStopWatch from 'components/ResponsePane/ResponseStopWatch';
|
||||
import WSMessagesList from './WsResponsePane/WSMessagesList';
|
||||
|
||||
const ResponsePane = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -71,6 +73,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
const getTabPanel = (tab) => {
|
||||
switch (tab) {
|
||||
case 'response': {
|
||||
const isStream = item.response?.stream ?? false;
|
||||
if (isStream) {
|
||||
return <WSMessagesList order={-1} messages={item.response.data} />;
|
||||
}
|
||||
return (
|
||||
<QueryResult
|
||||
item={item}
|
||||
@@ -184,8 +190,10 @@ const ResponsePane = ({ item, collection }) => {
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
|
||||
<StatusCode status={response.status} />
|
||||
<ResponseTime duration={response.duration} />
|
||||
<StatusCode status={response.status} isStreaming={item.response?.stream?.running} />
|
||||
{item.response?.stream?.running
|
||||
? <ResponseStopWatch startMillis={response.duration} />
|
||||
: <ResponseTime duration={response.duration} />}
|
||||
<ResponseSize size={responseSize} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -15,9 +15,11 @@ import {
|
||||
collectionUnlinkEnvFileEvent,
|
||||
collectionUnlinkFileEvent,
|
||||
processEnvUpdateEvent,
|
||||
requestCancelled,
|
||||
runFolderEvent,
|
||||
runRequestEvent,
|
||||
scriptEnvironmentUpdateEvent
|
||||
scriptEnvironmentUpdateEvent,
|
||||
streamDataReceived
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { collectionAddEnvFileEvent, openCollectionEvent, hydrateCollectionWithUiStateSnapshot, mergeAndPersistEnvironment } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -137,8 +139,8 @@ const useIpcEvents = () => {
|
||||
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();
|
||||
|
||||
@@ -2,6 +2,7 @@ import { parseQueryParams, buildQueryString as stringifyQueryParams } from '@use
|
||||
import { uuid } from 'utils/common';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, findIndex } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { hexy as hexdump } from 'hexy';
|
||||
import {
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
@@ -380,9 +381,17 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
item.response = null;
|
||||
if (item.response?.stream?.running) {
|
||||
item.response.stream.running = null;
|
||||
|
||||
const startTimestamp = item.requestSent.timestamp;
|
||||
item.response.duration = startTimestamp ? Date.now() - startTimestamp : item.response.duration;
|
||||
item.response.data = [{ type: 'info', timestamp: Date.now(), message: 'Connection Closed' }].concat(item.response.data);
|
||||
} else {
|
||||
item.response = null;
|
||||
item.requestUid = null;
|
||||
}
|
||||
item.cancelTokenUid = null;
|
||||
item.requestUid = null;
|
||||
item.requestStartTime = null;
|
||||
}
|
||||
}
|
||||
@@ -395,7 +404,7 @@ export const collectionsSlice = createSlice({
|
||||
if (item) {
|
||||
item.requestState = 'received';
|
||||
item.response = action.payload.response;
|
||||
item.cancelTokenUid = null;
|
||||
item.cancelTokenUid = item.response.stream?.running ? item.cancelTokenUid : null;
|
||||
item.requestStartTime = null;
|
||||
|
||||
if (!collection.timeline) {
|
||||
@@ -590,6 +599,11 @@ export const collectionsSlice = createSlice({
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
if (item) {
|
||||
if (item.response && item.response.stream?.running) {
|
||||
item.response.data = '';
|
||||
item.response.size = 0;
|
||||
return;
|
||||
}
|
||||
item.response = null;
|
||||
}
|
||||
}
|
||||
@@ -2936,7 +2950,25 @@ 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);
|
||||
if (data.data) {
|
||||
item.response.data ||= [];
|
||||
item.response.data = [{
|
||||
type: 'incoming',
|
||||
message: data.data,
|
||||
messageHexdump: hexdump(data.data),
|
||||
timestamp: Date.now()
|
||||
}].concat(item.response.data);
|
||||
}
|
||||
item.response.dataBuffer = Buffer.concat([Buffer.from(item.response.dataBuffer), Buffer.from(data.dataBuffer)]);
|
||||
item.response.size = data.data?.length + (item.response.size || 0);
|
||||
}
|
||||
},
|
||||
addRequestTag: (state, action) => {
|
||||
const { tag, collectionUid, itemUid } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
@@ -3298,6 +3330,7 @@ export const {
|
||||
updateRequestDocs,
|
||||
updateFolderDocs,
|
||||
moveCollection,
|
||||
streamDataReceived,
|
||||
collectionAddOauth2CredentialsByUrl,
|
||||
collectionClearOauth2CredentialsByUrl,
|
||||
collectionGetOauth2CredentialsByUrl,
|
||||
|
||||
@@ -115,6 +115,7 @@ const darkTheme = {
|
||||
url: {
|
||||
bg: '#3D3D3D',
|
||||
icon: 'rgb(204, 204, 204)',
|
||||
iconDanger: '#fa5343',
|
||||
errorHoverBg: '#4a2a2a'
|
||||
},
|
||||
dragbar: {
|
||||
|
||||
@@ -115,6 +115,7 @@ const lightTheme = {
|
||||
url: {
|
||||
bg: '#f3f3f3',
|
||||
icon: '#515151',
|
||||
iconDanger: '#d91f11',
|
||||
errorHoverBg: '#fef2f2'
|
||||
},
|
||||
dragbar: {
|
||||
|
||||
@@ -10,6 +10,7 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
if (response?.error) {
|
||||
resolve(response)
|
||||
}
|
||||
|
||||
resolve({
|
||||
state: 'success',
|
||||
data: response.data,
|
||||
@@ -20,7 +21,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
duration: response.duration,
|
||||
timeline: response.timeline
|
||||
timeline: response.timeline,
|
||||
stream: response.stream
|
||||
});
|
||||
})
|
||||
.catch((err) => reject(err));
|
||||
|
||||
@@ -69,6 +69,37 @@ const getJsSandboxRuntime = (collection) => {
|
||||
return 'vm2';
|
||||
};
|
||||
|
||||
const hasStreamHeaders = (headers) => {
|
||||
const headerSplit = (headers.get('content-type') ?? '').split(';').map((d) => d.trim());
|
||||
return headerSplit.indexOf('text/event-stream') > -1;
|
||||
};
|
||||
|
||||
const promisifyStream = async (stream, abortController, closeOnFirst) => {
|
||||
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,
|
||||
@@ -595,6 +626,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
const abortController = new AbortController();
|
||||
const request = await prepareRequest(item, collection, abortController);
|
||||
request.__bruno__executionMode = 'standalone';
|
||||
request.responseType = 'stream';
|
||||
// flag to see if the stream needs to be handled as an actual stream or
|
||||
// is it just a data stream from axios
|
||||
let isResponseStream = false;
|
||||
const brunoConfig = getBrunoConfig(collectionUid, collection);
|
||||
const scriptingConfig = get(brunoConfig, 'scripts', {});
|
||||
scriptingConfig.runtime = getJsSandboxRuntime(collection);
|
||||
@@ -668,7 +703,8 @@ 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', {
|
||||
@@ -691,10 +727,15 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
});
|
||||
}
|
||||
|
||||
let response, responseTime;
|
||||
let response, responseTime, axiosDataStream;
|
||||
try {
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
isResponseStream = hasStreamHeaders(response.headers);
|
||||
|
||||
if (!isResponseStream) {
|
||||
response.data = await promisifyStream(response.data);
|
||||
}
|
||||
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
@@ -719,6 +760,10 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
// Prevents the duration on leaking to the actual result
|
||||
responseTime = response.headers.get('request-duration');
|
||||
response.headers.delete('request-duration');
|
||||
isResponseStream = hasStreamHeaders(response.headers);
|
||||
if (!isResponseStream) {
|
||||
response.data = await promisifyStream(response.data);
|
||||
}
|
||||
} else {
|
||||
await executeRequestOnFailHandler(request, error);
|
||||
|
||||
@@ -734,8 +779,13 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
// Continue with the rest of the request lifecycle - post response vars, script, assertions, tests
|
||||
if (isResponseStream) {
|
||||
axiosDataStream = response.data;
|
||||
}
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
const { data, dataBuffer } = isResponseStream
|
||||
? { data: '', dataBuffer: Buffer.alloc(0) }
|
||||
: parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
response.data = data;
|
||||
response.dataBuffer = dataBuffer;
|
||||
|
||||
@@ -752,140 +802,141 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
cookiesStore.saveCookieJar();
|
||||
|
||||
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 () => {
|
||||
let postResponseScriptResult = null;
|
||||
let postResponseError = null;
|
||||
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 (isResponseStream) {
|
||||
axiosDataStream.on('close', () => runPostScripts().then());
|
||||
} else {
|
||||
await runPostScripts();
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -894,6 +945,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
headers: response.headers,
|
||||
data: response.data,
|
||||
dataBuffer: response.dataBuffer.toString('base64'),
|
||||
stream: isResponseStream ? axiosDataStream : null,
|
||||
cancelTokenUid: cancelTokenUid,
|
||||
size: Buffer.byteLength(response.dataBuffer),
|
||||
duration: responseTime ?? 0,
|
||||
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
|
||||
@@ -917,7 +970,26 @@ 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 = { running: 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 +1007,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();
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error('cancel token not found'));
|
||||
@@ -960,10 +1033,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);
|
||||
@@ -1154,7 +1234,8 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
method: request.method,
|
||||
headers: headersSent,
|
||||
data: requestData,
|
||||
dataBuffer: requestDataBuffer
|
||||
dataBuffer: requestDataBuffer,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
// todo:
|
||||
@@ -1166,7 +1247,9 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
...eventData
|
||||
});
|
||||
|
||||
request.signal = abortController.signal;
|
||||
currentAbortController = new AbortController();
|
||||
request.signal = currentAbortController.signal;
|
||||
request.responseType = 'stream';
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
collection,
|
||||
@@ -1213,6 +1296,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
/** @type {import('axios').AxiosResponse} */
|
||||
response = await axiosInstance(request);
|
||||
response.data = await promisifyStream(response.data, currentAbortController, false);
|
||||
timeEnd = Date.now();
|
||||
|
||||
const { data, dataBuffer } = parseDataFromResponse(response, request.__brunoDisableParsingResponseJson);
|
||||
@@ -1254,6 +1338,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
}
|
||||
|
||||
if (error?.response) {
|
||||
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');
|
||||
|
||||
Reference in New Issue
Block a user