diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 6cdf4a6d7..7bbc903b2 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -25,6 +25,14 @@ const ContentIndicator = () => { ); }; +const ErrorIndicator = () => { + return ( + + + + ); +}; + const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { const dispatch = useDispatch(); const tabs = useSelector((state) => state.tabs.tabs); @@ -143,7 +151,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
selectTab('script')}> Script - {(script.req || script.res) && } + {(script.req || script.res) && ( + item.preScriptResponseErrorMessage || item.postResponseScriptErrorMessage ? + : + + )}
selectTab('assert')}> Assert diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index b6b8d751d..c1631a358 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -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 ( {error ? (
-
{error}
+ {hasScriptError ? null :
{formatErrorMessage(error)}
} {error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
@@ -143,24 +154,26 @@ const QueryResult = ({ item, collection, data, dataBuffer, width, disableRunEven ) : null}
) : ( - <> - - {queryFilterEnabled && ( - - )} - +
+
+ + {queryFilterEnabled && ( + + )} +
+
)} ); diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js new file mode 100644 index 000000000..c4a38e80f --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/StyledWrapper.js @@ -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; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js new file mode 100644 index 000000000..4af07c587 --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ScriptError/index.js @@ -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 ( + +
+
+
+ {errorTitle} +
+
+ {errorMessage} +
+
+
+ +
+
+
+ ); +}; + +export default ScriptError; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js b/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js new file mode 100644 index 000000000..208a49ffd --- /dev/null +++ b/packages/bruno-app/src/components/ResponsePane/ScriptErrorIcon/index.js @@ -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 ( + <> +
+
+ +
+
+ + + ); +}; + +export default ScriptErrorIcon; \ No newline at end of file diff --git a/packages/bruno-app/src/components/ResponsePane/index.js b/packages/bruno-app/src/components/ResponsePane/index.js index f0df42e3e..dbb3b1a78 100644 --- a/packages/bruno-app/src/components/ResponsePane/index.js +++ b/packages/bruno-app/src/components/ResponsePane/index.js @@ -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 ( @@ -117,6 +128,12 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => {
{!isLoading ? (
+ {hasScriptError && !showScriptErrorCard && ( + setShowScriptErrorCard(true)} + /> + )} @@ -126,9 +143,15 @@ const ResponsePane = ({ rightPaneWidth, item, collection }) => { ) : null}
{isLoading ? : null} + {hasScriptError && showScriptErrorCard && ( + setShowScriptErrorCard(false)} + /> + )} {getTabPanel(focusedTab.responsePaneTab)}
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 905576a2f..58efc93e0 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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; diff --git a/packages/bruno-app/src/utils/codemirror/javascript-lint.js b/packages/bruno-app/src/utils/codemirror/javascript-lint.js index 475686f5d..ec612e5f9 100644 --- a/packages/bruno-app/src/utils/codemirror/javascript-lint.js +++ b/packages/bruno-app/src/utils/codemirror/javascript-lint.js @@ -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 diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 2271a3d33..50b234ca5 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -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');