fix: timeline url race condition (#7154)

* fix: timeline url race condition

* add: requestSent in catch block

* add: requestSent in catch
This commit is contained in:
Pooja
2026-04-06 17:09:37 +05:30
committed by GitHub
parent fabba4d296
commit 4d6032ba0d
5 changed files with 105 additions and 13 deletions

View File

@@ -591,10 +591,11 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
} else {
sendNetworkRequest(itemCopy, collectionCopy, environment, collectionCopy.runtimeVariables)
.then((response) => {
const { requestSent, ...responseData } = response;
// Ensure any timestamps in the response are converted to numbers
const serializedResponse = {
...response,
timeline: response.timeline?.map((entry) => ({
...responseData,
timeline: responseData.timeline?.map((entry) => ({
...entry,
timestamp: entry.timestamp instanceof Date ? entry.timestamp.getTime() : entry.timestamp
}))
@@ -604,18 +605,23 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
responseReceived({
itemUid,
collectionUid,
response: serializedResponse
response: serializedResponse,
requestSent
})
);
})
.then(resolve)
.catch((err) => {
const request = itemCopy.draft?.request || itemCopy.request;
const requestSent = request ? { url: request.url, method: request.method } : undefined;
if (err && err.message === 'Error invoking remote method \'send-http-request\': Error: Request cancelled') {
dispatch(
responseReceived({
itemUid,
collectionUid,
response: null
response: null,
requestSent
})
);
return;
@@ -633,7 +639,8 @@ export const sendRequest = (item, collectionUid) => (dispatch, getState) => {
responseReceived({
itemUid,
collectionUid,
response: errorResponse
response: errorResponse,
requestSent
})
);
});

View File

@@ -538,10 +538,12 @@ export const collectionsSlice = createSlice({
collection.timeline = [];
}
const timelineRequest = action.payload.requestSent || item.requestSent || item.request;
// Ensure timestamp is a number (milliseconds since epoch)
const timestamp = item?.requestSent?.timestamp instanceof Date
? item.requestSent.timestamp.getTime()
: item?.requestSent?.timestamp || Date.now();
const timestamp = timelineRequest?.timestamp instanceof Date
? timelineRequest.timestamp.getTime()
: timelineRequest?.timestamp || Date.now();
// Append the new timeline entry with numeric timestamp
collection.timeline.push({
@@ -552,7 +554,7 @@ export const collectionsSlice = createSlice({
requestUid: item.requestUid,
timestamp: timestamp,
data: {
request: item.requestSent || item.request,
request: timelineRequest,
response: action.payload.response,
timestamp: timestamp
}

View File

@@ -19,7 +19,8 @@ export const sendNetworkRequest = async (item, collection, environment, runtimeV
statusText: response.statusText,
duration: response.duration,
timeline: response.timeline,
stream: response.stream
stream: response.stream,
requestSent: response.requestSent
});
})
.catch((err) => reject(err));

View File

@@ -774,6 +774,7 @@ const registerNetworkIpc = (mainWindow) => {
// 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;
let requestSent;
const brunoConfig = getBrunoConfig(collectionUid, collection);
const scriptingConfig = get(brunoConfig, 'scripts', {});
scriptingConfig.runtime = getJsSandboxRuntime(collection);
@@ -864,7 +865,7 @@ const registerNetworkIpc = (mainWindow) => {
}
});
let requestSent = {
requestSent = {
url: request.url,
method: request.method,
headers: headersSent,
@@ -1141,7 +1142,8 @@ const registerNetworkIpc = (mainWindow) => {
size: Buffer.byteLength(response.dataBuffer),
duration: responseTime ?? 0,
url: response.request ? response.request.protocol + '//' + response.request.host + response.request.path : null,
timeline: response.timeline
timeline: response.timeline,
requestSent
};
} catch (error) {
deleteCancelToken(cancelTokenUid);
@@ -1151,7 +1153,8 @@ const registerNetworkIpc = (mainWindow) => {
return {
status: error?.status,
error: error?.message || ERROR_OCCURRED_WHILE_EXECUTING_REQUEST,
timeline: error?.timeline
timeline: error?.timeline,
requestSent
};
}
};

View File

@@ -0,0 +1,79 @@
import { test, expect } from '../../../playwright';
import {
closeAllCollections,
createCollection,
createRequest,
sendRequest
} from '../../utils/page/actions';
/**
* Select a tab in the response pane, handling the overflow dropdown (>>) if the tab is hidden.
*/
const selectResponsePaneTab = async (page, tabName: string) => {
await test.step(`Select response pane tab "${tabName}"`, async () => {
const responsePaneTabs = page.locator('.response-pane .tabs');
const visibleTab = responsePaneTabs.getByRole('tab', { name: tabName });
if (await visibleTab.isVisible()) {
await visibleTab.click();
return;
}
// Tab is hidden in the overflow dropdown (>> button)
const overflowButton = responsePaneTabs.locator('.more-tabs');
if (await overflowButton.isVisible()) {
await overflowButton.click();
const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName });
await dropdownItem.click();
}
});
};
test.describe('Timeline URL Update', () => {
test.afterAll(async ({ page }) => {
await closeAllCollections(page);
});
test('should show correct URL in timeline after changing request URL between sends', async ({ page, createTmpDir }) => {
const collectionName = 'timeline-url-test';
const firstUrl = 'http://localhost:8081/ping';
const secondUrl = 'http://localhost:8081/headers';
await test.step('Create collection and request', async () => {
await createCollection(page, collectionName, await createTmpDir(collectionName));
await createRequest(page, 'url-change-test', collectionName, { url: firstUrl });
});
await test.step('Send first request', async () => {
await sendRequest(page, 200);
});
await test.step('Change URL and send second request', async () => {
// Click into the URL field, select all, then type the new URL
// (fillRequestUrl appends in CodeMirror, so we clear manually)
const urlEditor = page.locator('#request-url .CodeMirror');
await urlEditor.click();
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control';
await page.keyboard.press(`${modifier}+a`);
await page.keyboard.type(secondUrl);
await page.waitForTimeout(200);
await sendRequest(page, 200);
});
await test.step('Open Timeline tab and verify URLs', async () => {
await selectResponsePaneTab(page, 'Timeline');
// Get all timeline entries
const timelineItems = page.locator('.timeline-item');
await expect(timelineItems).toHaveCount(2, { timeout: 5000 });
// Most recent entry (first in list) should show the second URL
const firstEntry = timelineItems.nth(0);
await expect(firstEntry).toContainText('/headers');
// Older entry (second in list) should show the first URL
const secondEntry = timelineItems.nth(1);
await expect(secondEntry).toContainText('/ping');
});
});
});