mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-25 13:45:52 +00:00
feat: show response errors while keeping response preview intact (#4082)
Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
@@ -25,6 +25,14 @@ const ContentIndicator = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ErrorIndicator = () => {
|
||||
return (
|
||||
<sup className="ml-[.125rem] opacity-80 font-medium text-red-500">
|
||||
<DotIcon width="10" ></DotIcon>
|
||||
</sup>
|
||||
);
|
||||
};
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const dispatch = useDispatch();
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
@@ -143,7 +151,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('script')} role="tab" onClick={() => selectTab('script')}>
|
||||
Script
|
||||
{(script.req || script.res) && <ContentIndicator />}
|
||||
{(script.req || script.res) && (
|
||||
item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ?
|
||||
<ErrorIndicator /> :
|
||||
<ContentIndicator />
|
||||
)}
|
||||
</div>
|
||||
<div className={getTabClassname('assert')} role="tab" onClick={() => selectTab('assert')}>
|
||||
Assert
|
||||
|
||||
@@ -6,11 +6,8 @@ import classnames from 'classnames';
|
||||
import { getContentType, safeStringifyJSON, safeParseXML } from 'utils/common';
|
||||
import { getCodeMirrorModeBasedOnContentType } from 'utils/common/codemirror';
|
||||
import QueryResultPreview from './QueryResultPreview';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useState, useMemo, useEffect } from 'react';
|
||||
import { useTheme } from 'providers/Theme/index';
|
||||
import { uuid } from 'utils/common/index';
|
||||
|
||||
@@ -62,6 +59,19 @@ const formatResponse = (data, mode, filter) => {
|
||||
return safeStringifyJSON(data, true);
|
||||
};
|
||||
|
||||
const formatErrorMessage = (error) => {
|
||||
if (!error) return 'Something went wrong';
|
||||
|
||||
const remoteMethodError = "Error invoking remote method 'send-http-request':";
|
||||
|
||||
if (error.includes(remoteMethodError)) {
|
||||
const parts = error.split(remoteMethodError);
|
||||
return parts[1]?.trim() || error;
|
||||
}
|
||||
|
||||
return error;
|
||||
};
|
||||
|
||||
const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEventListener, headers, error }) => {
|
||||
const contentType = getContentType(headers);
|
||||
const mode = getCodeMirrorModeBasedOnContentType(contentType, data);
|
||||
@@ -121,6 +131,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
}, [allowedPreviewModes, previewTab]);
|
||||
|
||||
const queryFilterEnabled = useMemo(() => mode.includes('json'), [mode]);
|
||||
const hasScriptError = item.preRequestScriptErrorMessage || item.postResponseScriptErrorMessage;
|
||||
|
||||
return (
|
||||
<StyledWrapper
|
||||
@@ -133,7 +144,7 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
</div>
|
||||
{error ? (
|
||||
<div>
|
||||
<div className="text-red-500">{error}</div>
|
||||
{hasScriptError ? null : <div className="text-red-500">{formatErrorMessage(error)}</div>}
|
||||
|
||||
{error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
|
||||
<div className="mt-6 muted text-xs">
|
||||
@@ -143,24 +154,26 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<QueryResultPreview
|
||||
previewTab={previewTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
mode={mode}
|
||||
collection={collection}
|
||||
allowedPreviewModes={allowedPreviewModes}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
displayedTheme={displayedTheme}
|
||||
/>
|
||||
{queryFilterEnabled && (
|
||||
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
|
||||
)}
|
||||
</>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex-1 relative">
|
||||
<QueryResultPreview
|
||||
previewTab={previewTab}
|
||||
data={data}
|
||||
dataBuffer={dataBuffer}
|
||||
formattedData={formattedData}
|
||||
item={item}
|
||||
contentType={contentType}
|
||||
mode={mode}
|
||||
collection={collection}
|
||||
allowedPreviewModes={allowedPreviewModes}
|
||||
disableRunEventListener={disableRunEventListener}
|
||||
displayedTheme={displayedTheme}
|
||||
/>
|
||||
{queryFilterEnabled && (
|
||||
<QueryResultFilter filter={filter} onChange={debouncedResultFilterOnChange} mode={mode} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
border-left: 4px solid ${(props) => props.theme.colors.text.danger};
|
||||
border-top: 1px solid transparent;
|
||||
border-right: 1px solid transparent;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-radius: 0.375rem;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
max-height: 200px;
|
||||
min-height: 70px;
|
||||
overflow-y: auto;
|
||||
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.5)' : 'rgba(250, 250, 250, 0.9)'};
|
||||
|
||||
.error-icon-container {
|
||||
margin-top: 0.125rem;
|
||||
padding: 0.375rem;
|
||||
border-radius: 9999px;
|
||||
background-color: ${(props) => props.theme.bg === '#1e1e1e' ? 'rgba(40, 40, 40, 0.8)' : 'rgba(240, 240, 240, 0.8)'};
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.375rem;
|
||||
color: ${(props) => props.theme.colors.text.danger};
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-family: monospace;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.25rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { IconX } from '@tabler/icons';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
|
||||
const ScriptError = ({ item, onClose }) => {
|
||||
const preRequestError = item?.preRequestScriptErrorMessage;
|
||||
const postResponseError = item?.postResponseScriptErrorMessage;
|
||||
|
||||
if (!preRequestError && !postResponseError) return null;
|
||||
|
||||
const errorMessage = preRequestError || postResponseError;
|
||||
const errorTitle = preRequestError ? 'Pre-Request Script Error' : 'Post-Response Script Error';
|
||||
|
||||
return (
|
||||
<StyledWrapper className="mt-4 mb-2">
|
||||
<div className="flex items-start gap-3 px-4 py-3">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="error-title">
|
||||
{errorTitle}
|
||||
</div>
|
||||
<div className="error-message">
|
||||
{errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="close-button flex-shrink-0 cursor-pointer"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX size={16} strokeWidth={1.5} />
|
||||
</div>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptError;
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { IconAlertCircle } from '@tabler/icons';
|
||||
import ToolHint from 'components/ToolHint';
|
||||
|
||||
const ScriptErrorIcon = ({ itemUid, onClick }) => {
|
||||
const toolhintId = `script-error-icon-${itemUid}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id={toolhintId}
|
||||
className="cursor-pointer ml-2"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className="flex items-center text-red-400">
|
||||
<IconAlertCircle size={16} strokeWidth={1.5} className="stroke-current" />
|
||||
</div>
|
||||
</div>
|
||||
<ToolHint
|
||||
toolhintId={toolhintId}
|
||||
text="Script execution error occurred"
|
||||
place="bottom"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptErrorIcon;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@@ -13,6 +13,8 @@ import ResponseSize from './ResponseSize';
|
||||
import Timeline from './Timeline';
|
||||
import TestResults from './TestResults';
|
||||
import TestResultsLabel from './TestResultsLabel';
|
||||
import ScriptError from './ScriptError';
|
||||
import ScriptErrorIcon from './ScriptErrorIcon';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import ResponseSave from 'src/components/ResponsePane/ResponseSave';
|
||||
import ResponseClear from 'src/components/ResponsePane/ResponseClear';
|
||||
@@ -22,6 +24,13 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
const tabs = useSelector((state) => state.tabs.tabs);
|
||||
const activeTabUid = useSelector((state) => state.tabs.activeTabUid);
|
||||
const isLoading = ['queued', 'sending'].includes(item.requestState);
|
||||
const [showScriptErrorCard, setShowScriptErrorCard] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage) {
|
||||
setShowScriptErrorCard(true);
|
||||
}
|
||||
}, [item?.preRequestScriptErrorMessage, item?.postResponseScriptErrorMessage]);
|
||||
|
||||
const selectTab = (tab) => {
|
||||
dispatch(
|
||||
@@ -98,6 +107,8 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
};
|
||||
|
||||
const responseHeadersCount = typeof response.headers === 'object' ? Object.entries(response.headers).length : 0;
|
||||
|
||||
const hasScriptError = item?.preRequestScriptErrorMessage || item?.postResponseScriptErrorMessage;
|
||||
|
||||
return (
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
@@ -117,6 +128,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
</div>
|
||||
{!isLoading ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
{hasScriptError && !showScriptErrorCard && (
|
||||
<ScriptErrorIcon
|
||||
itemUid={item.uid}
|
||||
onClick={() => setShowScriptErrorCard(true)}
|
||||
/>
|
||||
)}
|
||||
<ResponseClear item={item} collection={collection} />
|
||||
<ResponseSave item={item} />
|
||||
<StatusCode status={response.status} />
|
||||
@@ -126,9 +143,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
className={`flex flex-col flex-grow relative pl-3 pr-4 ${focusedTab.responsePaneTab === 'response' ? '' : 'mt-4'}`}
|
||||
>
|
||||
{isLoading ? <Overlay item={item} collection={collection} /> : null}
|
||||
{hasScriptError && showScriptErrorCard && (
|
||||
<ScriptError
|
||||
item={item}
|
||||
onClose={() => setShowScriptErrorCard(false)}
|
||||
/>
|
||||
)}
|
||||
{getTabPanel(focusedTab.responsePaneTab)}
|
||||
</section>
|
||||
</StyledWrapper>
|
||||
|
||||
@@ -1853,12 +1853,22 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
},
|
||||
runRequestEvent: (state, action) => {
|
||||
const { itemUid, collectionUid, type, requestUid } = action.payload;
|
||||
const { itemUid, collectionUid, type, requestUid, hasError } = action.payload;
|
||||
const collection = findCollectionByUid(state.collections, collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, itemUid);
|
||||
if (item) {
|
||||
if (type === 'pre-request-script-execution') {
|
||||
item.requestUid = requestUid;
|
||||
item.preRequestScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if(type === 'post-response-script-execution') {
|
||||
item.requestUid = requestUid;
|
||||
item.postResponseScriptErrorMessage = action.payload.errorMessage;
|
||||
}
|
||||
|
||||
if (type === 'request-queued') {
|
||||
const { cancelTokenUid } = action.payload;
|
||||
item.requestUid = requestUid;
|
||||
|
||||
@@ -19,6 +19,27 @@ if (!SERVER_RENDERED) {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Set default options for Bruno
|
||||
const defaultOptions = {
|
||||
esversion: 11,
|
||||
expr: true,
|
||||
asi: true,
|
||||
undef: true,
|
||||
browser: true,
|
||||
devel: true,
|
||||
predef: {
|
||||
'bru': false,
|
||||
'req': false,
|
||||
'res': false,
|
||||
'test': false,
|
||||
'expect': false
|
||||
}
|
||||
};
|
||||
|
||||
// Merge provided options with defaults
|
||||
options = Object.assign({}, defaultOptions, options);
|
||||
|
||||
if (!options.indent)
|
||||
// JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
|
||||
options.indent = 1; // JSHint default value is 4
|
||||
|
||||
@@ -410,6 +410,7 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
|
||||
|
||||
const registerNetworkIpc = (mainWindow) => {
|
||||
const onConsoleLog = (type, args) => {
|
||||
console[type](...args);
|
||||
@@ -609,19 +610,39 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
request.signal = abortController.signal;
|
||||
saveCancelToken(cancelTokenUid, abortController);
|
||||
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
try {
|
||||
await runPreRequest(
|
||||
request,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'pre-request-script-execution',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
errorMessage: null,
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'pre-request-script-execution',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
errorMessage: error?.message || 'An error occurred in pre-request script',
|
||||
});
|
||||
return Promise.reject(error);
|
||||
}
|
||||
const axiosInstance = await configureRequest(
|
||||
collectionUid,
|
||||
request,
|
||||
@@ -693,19 +714,41 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
|
||||
await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
try {
|
||||
await runPostResponse(
|
||||
request,
|
||||
response,
|
||||
requestUid,
|
||||
envVars,
|
||||
collectionPath,
|
||||
collection,
|
||||
collectionUid,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
scriptingConfig,
|
||||
runRequestByItemPathname
|
||||
);
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'post-response-script-execution',
|
||||
requestUid,
|
||||
collectionUid,
|
||||
errorMessage: null,
|
||||
itemUid: item.uid,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Post-response script error:', error);
|
||||
|
||||
// Format a more readable error message
|
||||
const errorMessage = error?.message || 'An error occurred in post-response script';
|
||||
|
||||
!runInBackground && mainWindow.webContents.send('main:run-request-event', {
|
||||
type: 'post-response-script-execution',
|
||||
requestUid,
|
||||
errorMessage,
|
||||
collectionUid,
|
||||
itemUid: item.uid,
|
||||
});
|
||||
}
|
||||
|
||||
// run assertions
|
||||
const assertions = get(request, 'assertions');
|
||||
|
||||
Reference in New Issue
Block a user