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 <ahoy@barelyhuman.dev>
This commit is contained in:
DaviXavier
2025-11-12 07:26:26 -03:00
committed by GitHub
parent 631b05330d
commit fc5093eab4
10 changed files with 472 additions and 327 deletions

View File

@@ -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 }) => {
<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} />
)}
@@ -149,7 +149,13 @@ const QueryUrl = ({ item, collection, handleRun }) => {
Save <span className="shortcut">({saveShortcut})</span>
</span>
</div>
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
{
item.response?.hasStreamRunning ? (
<IconX color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} />
) : (
<IconArrowRight color={theme.requestTabPanel.url.icon} strokeWidth={1.5} size={22} data-testid="send-arrow-icon" />
)
}
</div>
</div>
{generateCodeItemModalOpen && (

View File

@@ -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) => <NetworkError onClose={() => toast.dismiss(t.id)} />, {
duration: 5000
})
);
if (item.response?.hasStreamRunning) {
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

View File

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

View File

@@ -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 <StyledWrapper className="ml-4" style={{width: `${width}rem`}}>{secondsFormatted}</StyledWrapper>;
};
export default React.memo(ResponseStopWatch);

View File

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

View File

@@ -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 <ResponseHeaders headers={response.headers} />;
}
case 'timeline': {
return <Timeline collection={collection} item={item} />;
return <Timeline collection={collection} item={item} />;
}
case 'tests': {
return <TestResults
results={item.testResults}
assertionResults={item.assertionResults}
preRequestTestResults={item.preRequestTestResults}
postResponseTestResults={item.postResponseTestResults}
/>;
return (
<TestResults
results={item.testResults}
assertionResults={item.assertionResults}
preRequestTestResults={item.preRequestTestResults}
postResponseTestResults={item.postResponseTestResults}
/>
);
}
default: {
@@ -184,7 +188,10 @@ const ResponsePane = ({ item, collection }) => {
<ResponseClear item={item} collection={collection} />
<ResponseSave item={item} />
<ResponseBookmark item={item} collection={collection} responseSize={responseSize} />
<StatusCode status={response.status} />
<StatusCode status={response.status} isStreaming={item.response?.hasStreamRunning} />
{item.response?.hasStreamRunning ? (
<ResponseStopWatch startMillis={response.duration} />
) : <ResponseTime duration={response.duration} />}
<ResponseTime duration={response.duration} />
<ResponseSize size={responseSize} />
</>
@@ -193,7 +200,7 @@ const ResponsePane = ({ item, collection }) => {
) : null}
</div>
<section
className={`flex flex-col min-h-0 relative px-4 auto overflow-auto`}
className="flex flex-col min-h-0 relative px-4 auto overflow-auto"
style={{
flex: '1 1 0',
height: hasScriptError && showScriptErrorCard ? 'auto' : '100%'
@@ -206,9 +213,9 @@ const ResponsePane = ({ item, collection }) => {
onClose={() => setShowScriptErrorCard(false)}
/>
)}
<div className='flex-1 overflow-y-auto'>
<div className="flex-1 overflow-y-auto">
{!item?.response ? (
focusedTab?.responsePaneTab === "timeline" && requestTimeline?.length ? (
focusedTab?.responsePaneTab === 'timeline' && requestTimeline?.length ? (
<Timeline
collection={collection}
item={item}