diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js index 31f1759cb..a52d1499b 100644 --- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js +++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js @@ -171,7 +171,9 @@ const QueryResult = ({ item, collection, data, dataBuffer, disableRunEventListen {error ? (
- {hasScriptError ? null :
{formatErrorMessage(error)}
} + {hasScriptError ? null : ( +
{formatErrorMessage(error)}
+ )} {error && typeof error === 'string' && error.toLowerCase().includes('self signed certificate') ? (
diff --git a/packages/bruno-app/src/utils/codemirror/autocomplete.js b/packages/bruno-app/src/utils/codemirror/autocomplete.js index 282763a4b..9e9dc5d6e 100644 --- a/packages/bruno-app/src/utils/codemirror/autocomplete.js +++ b/packages/bruno-app/src/utils/codemirror/autocomplete.js @@ -27,7 +27,8 @@ const STATIC_API_HINTS = { 'req.setTimeout(timeout)', 'req.getExecutionMode()', 'req.getName()', - 'req.disableParsingResponseJson()' + 'req.disableParsingResponseJson()', + 'req.onFail(function(err) {})', ], res: [ 'res', diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4da75f9ec..aa70ab8c6 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -697,7 +697,6 @@ const registerNetworkIpc = (mainWindow) => { timeline: error.timeline }; } - if (error?.response) { response = error.response; @@ -705,6 +704,8 @@ const registerNetworkIpc = (mainWindow) => { responseTime = response.headers.get('request-duration'); response.headers.delete('request-duration'); } else { + await executeRequestOnFailHandler(request, error); + // if it's not a network error, don't continue // we are not rejecting the promise here and instead returning a response object with `error` which is handled in the `send-http-request` invocation // timeline prop won't be accessible in the usual way in the renderer process if we reject the promise @@ -1163,7 +1164,12 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); } catch (error) { - if (error?.response && !axios.isCancel(error)) { + // Skip further processing if request was cancelled + if (axios.isCancel(error)) { + throw Promise.reject(error); + } + + if (error?.response) { const { data, dataBuffer } = parseDataFromResponse(error.response); error.response.data = data; @@ -1187,6 +1193,8 @@ const registerNetworkIpc = (mainWindow) => { ...eventData }); } else { + await executeRequestOnFailHandler(request, error); + // if it's not a network error, don't continue throw Promise.reject(error); } @@ -1432,7 +1440,27 @@ const registerNetworkIpc = (mainWindow) => { }); }; +/** + * Executes the custom error handler if it exists on the request + * @param {Object} request - The request object that may contain an onFailHandler + * @param {Error} error - The error that occurred + */ +const executeRequestOnFailHandler = async (request, error) => { + if (!request || typeof request.onFailHandler !== 'function') { + return; + } + + try { + await request.onFailHandler(error); + } catch (handlerError) { + console.error('Error executing onFail handler', handlerError); + // @TODO: This is a temporary solution to display the error message in the response pane. Revisit and handle properly. + error.message = `1. Request failed: ${error.message || 'Error occured while executing the request!'}\n2. Error executing onFail handler: ${handlerError.message || 'Unknown error'}`; + } +}; + module.exports = registerNetworkIpc; module.exports.configureRequest = configureRequest; module.exports.getCertsAndProxyConfig = getCertsAndProxyConfig; module.exports.fetchGqlSchemaHandler = fetchGqlSchemaHandler; +module.exports.executeRequestOnFailHandler = executeRequestOnFailHandler; diff --git a/packages/bruno-electron/tests/network/execute-request-error-handler.spec.js b/packages/bruno-electron/tests/network/execute-request-error-handler.spec.js new file mode 100644 index 000000000..3fe168adb --- /dev/null +++ b/packages/bruno-electron/tests/network/execute-request-error-handler.spec.js @@ -0,0 +1,112 @@ +const { executeRequestOnFailHandler } = require('../../src/ipc/network/index'); +const axios = require('axios'); + +describe('executeRequestOnFailHandler', () => { + let consoleSpy; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + it('should do nothing when request is null', async () => { + const error = new Error('Test error'); + + await executeRequestOnFailHandler(null, error); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('should do nothing when request is undefined', async () => { + const error = new Error('Test error'); + + await executeRequestOnFailHandler(undefined, error); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('should do nothing when onFailHandler is not a function', async () => { + const request = { onFailHandler: 'not a function' }; + const error = new Error('Test error'); + + await executeRequestOnFailHandler(request, error); + + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('should call onFailHandler when it exists and is a function', async () => { + const mockHandler = jest.fn(); + const request = { onFailHandler: mockHandler }; + const error = new Error('Test error'); + + await executeRequestOnFailHandler(request, error); + + expect(mockHandler).toHaveBeenCalledWith(error); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(consoleSpy).not.toHaveBeenCalled(); + }); + + it('should handle errors when onFailHandler fails by mutating the error message', async () => { + const handlerError = new Error('Handler failed'); + const mockHandler = jest.fn(() => { + throw handlerError; + }); + const request = { onFailHandler: mockHandler }; + const error = new Error('Original error'); + + await executeRequestOnFailHandler(request, error); + + expect(mockHandler).toHaveBeenCalledWith(error); + expect(error.message).toContain('1. Request failed: Original error'); + expect(error.message).toContain('2. Error executing onFail handler: Handler failed'); + }); + + it('should pass the correct hard error object to the handler for DNS failure', async () => { + const mockHandler = jest.fn(); + const request = { onFailHandler: mockHandler }; + + let error; + try { + await axios.get('https://this-domain-definitely-does-not-exist-12345.com/api/test', { + timeout: 5000 + }); + } catch (err) { + error = err; + } + + // Verify this is actually a hard error (no response) + expect(error.response).toBeUndefined(); + + await executeRequestOnFailHandler(request, error); + + expect(mockHandler).toHaveBeenCalledWith(error); + expect(error.message).toContain('ENOTFOUND'); // DNS resolution failed + }); + + it('should pass the correct hard error object to the handler for connection timeout', async () => { + const mockHandler = jest.fn(); + const request = { onFailHandler: mockHandler }; + + let error; + try { + await axios.get('http://192.168.255.255:9999/api/test', { + timeout: 100 + }); + } catch (err) { + error = err; + } + + // Verify this is actually a hard error (no response) + expect(error.response).toBeUndefined(); + + await executeRequestOnFailHandler(request, error); + + expect(mockHandler).toHaveBeenCalledWith(error); + const passedError = mockHandler.mock.calls[0][0]; + expect(passedError.response).toBeUndefined(); // Should be undefined for hard errors + expect(passedError.code).toBe('ECONNABORTED'); // Connection aborted due to timeout + }); +}); \ No newline at end of file diff --git a/packages/bruno-js/src/bruno-request.js b/packages/bruno-js/src/bruno-request.js index 3ee2127ca..b08e6515d 100644 --- a/packages/bruno-js/src/bruno-request.js +++ b/packages/bruno-js/src/bruno-request.js @@ -148,6 +148,14 @@ class BrunoRequest { this.timeout = timeout; this.req.timeout = timeout; } + + onFail(callback) { + if (typeof callback === 'function') { + this.req.onFailHandler = callback; + } else if (callback) { + throw new Error(`${callback} is not a function`); + } + } __safeParseJSON(str) { try {