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 {