+
+
Configure request settings for this item.
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
!isTimeoutInherited && onTimeoutChange(e)}
+ onCustomValueReset={() => onTimeoutChange({ target: { value: 'inherit' } })}
+ />
+
);
diff --git a/packages/bruno-app/src/components/SettingsInput/index.js b/packages/bruno-app/src/components/SettingsInput/index.js
new file mode 100644
index 000000000..70f980652
--- /dev/null
+++ b/packages/bruno-app/src/components/SettingsInput/index.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { useTheme } from 'providers/Theme';
+
+const SettingsInput = ({
+ id,
+ label,
+ value,
+ onChange,
+ className = '',
+ description = '',
+ onKeyDown
+}) => {
+ const { theme } = useTheme();
+
+ return (
+
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+
+ );
+};
+
+export default SettingsInput;
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 14d63a555..cdaa7929a 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -617,6 +617,9 @@ export const collectionsSlice = createSlice({
if (item && item.draft) {
item.request = item.draft.request;
+ if (item.draft.settings) {
+ item.settings = item.draft.settings;
+ }
item.draft = null;
}
}
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index 8b4c77a02..95f4787ab 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -346,14 +346,24 @@ const runSingleRequest = async function (
}
}
- let requestMaxRedirects = request.maxRedirects
- request.maxRedirects = 0
-
- // Set default value for requestMaxRedirects if not explicitly set
- if (requestMaxRedirects === undefined) {
+ // Get followRedirects setting, default to true for backward compatibility
+ const followRedirects = request.settings?.followRedirects ?? true;
+
+ // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
+ let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;
+
+ // Ensure it's a valid number
+ if (typeof requestMaxRedirects !== 'number' || requestMaxRedirects < 0) {
requestMaxRedirects = 5; // Default to 5 redirects
}
+ // If followRedirects is disabled, set maxRedirects to 0 to disable all redirects
+ if (!followRedirects) {
+ requestMaxRedirects = 0;
+ }
+
+ request.maxRedirects = 0;
+
// Handle OAuth2 authentication
if (request.oauth2) {
try {
@@ -384,12 +394,22 @@ const runSingleRequest = async function (
let response, responseTime;
try {
- let axiosInstance = makeAxiosInstance({ requestMaxRedirects: requestMaxRedirects, disableCookies: options.disableCookies });
+ // Set timeout from request settings, default to 0 (no timeout)
+ const requestTimeout = request.settings?.timeout || 0;
+ if (requestTimeout > 0) {
+ request.timeout = requestTimeout;
+ }
+
+ let axiosInstance = makeAxiosInstance({
+ requestMaxRedirects: requestMaxRedirects,
+ disableCookies: options.disableCookies
+ });
+
if (request.ntlmConfig) {
axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults)
delete request.ntlmConfig;
}
-
+
if (request.awsv4config) {
// todo: make this happen in prepare-request.js
diff --git a/packages/bruno-converters/src/postman/postman-to-bruno.js b/packages/bruno-converters/src/postman/postman-to-bruno.js
index b40315854..e9c4f421f 100644
--- a/packages/bruno-converters/src/postman/postman-to-bruno.js
+++ b/packages/bruno-converters/src/postman/postman-to-bruno.js
@@ -387,6 +387,16 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
}
+ // Handle followRedirects setting
+ if (i.protocolProfileBehavior?.followRedirects !== undefined) {
+ settings.followRedirects = i.protocolProfileBehavior.followRedirects;
+ }
+
+ // Handle maxRedirects setting
+ if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
+ settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
+ }
+
brunoRequestItem.settings = settings;
brunoParent.items.push(brunoRequestItem);
diff --git a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
index 4e1084ec1..faef24465 100644
--- a/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
+++ b/packages/bruno-converters/tests/postman/postman-to-bruno/postman-to-bruno.spec.js
@@ -74,6 +74,88 @@ describe('postman-collection', () => {
expect(brunoCollection.root.request.vars.req).toEqual([]);
});
+ it('should correctly import protocolProfileBehavior settings from Postman requests', async () => {
+ const collectionWithSettings = {
+ info: {
+ _postman_id: 'test-settings-id',
+ name: 'Collection with Settings',
+ schema: 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json'
+ },
+ item: [
+ {
+ name: 'Request with all settings',
+ protocolProfileBehavior: {
+ maxRedirects: 10,
+ followRedirects: false,
+ disableUrlEncoding: true
+ },
+ request: {
+ method: 'GET',
+ header: [],
+ url: {
+ raw: 'https://httpbin.org/get',
+ protocol: 'https',
+ host: ['httpbin', 'org'],
+ path: ['get']
+ }
+ }
+ },
+ {
+ name: 'Request with partial settings',
+ protocolProfileBehavior: {
+ followRedirects: true
+ },
+ request: {
+ method: 'POST',
+ header: [],
+ url: {
+ raw: 'https://httpbin.org/post',
+ protocol: 'https',
+ host: ['httpbin', 'org'],
+ path: ['post']
+ }
+ }
+ },
+ {
+ name: 'Request without settings',
+ request: {
+ method: 'PUT',
+ header: [],
+ url: {
+ raw: 'https://httpbin.org/put',
+ protocol: 'https',
+ host: ['httpbin', 'org'],
+ path: ['put']
+ }
+ }
+ }
+ ]
+ };
+
+ const brunoCollection = await postmanToBruno(collectionWithSettings);
+
+ // Test request with all settings
+ const requestWithAllSettings = brunoCollection.items[0];
+ expect(requestWithAllSettings.settings).toEqual({
+ encodeUrl: false,
+ followRedirects: false,
+ maxRedirects: 10
+ });
+
+ // Test request with partial settings
+ const requestWithPartialSettings = brunoCollection.items[1];
+ expect(requestWithPartialSettings.settings).toEqual({
+ encodeUrl: true,
+ followRedirects: true
+ });
+
+ // Test request without settings
+ const requestWithoutSettings = brunoCollection.items[2];
+ expect(requestWithoutSettings.settings).toEqual({
+ encodeUrl: true
+ });
+ });
+
it('should handle collection with auth object having undefined type', async () => {
const collectionWithUndefinedAuthType = {
'info': {
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 8c090baf1..f40ed7dc2 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -18,6 +18,7 @@ const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-requ
const { prepareRequest } = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
const { makeAxiosInstance } = require('./axios-instance');
+const { resolveInheritedSettings } = require('../../utils/collection');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common');
const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem');
@@ -89,14 +90,24 @@ const configureRequest = async (
collectionPath
});
- let requestMaxRedirects = request.maxRedirects
- request.maxRedirects = 0
+ // Get followRedirects setting, default to true for backward compatibility
+ const followRedirects = request.settings?.followRedirects ?? true;
- // Set default value for requestMaxRedirects if not explicitly set
- if (requestMaxRedirects === undefined) {
+ // Get maxRedirects from request settings, fallback to request.maxRedirects, then default to 5
+ let requestMaxRedirects = request.settings?.maxRedirects ?? request.maxRedirects ?? 5;
+
+ // Ensure it's a valid number
+ if (typeof requestMaxRedirects !== 'number' || requestMaxRedirects < 0) {
requestMaxRedirects = 5; // Default to 5 redirects
}
+ // If followRedirects is disabled, set maxRedirects to 0 to disable all redirects
+ if (!followRedirects) {
+ requestMaxRedirects = 0;
+ }
+
+ request.maxRedirects = 0;
+
let { proxyMode, proxyConfig, httpsAgentRequestFields, interpolationOptions } = certsAndProxyConfig;
let axiosInstance = makeAxiosInstance({
proxyMode,
@@ -193,7 +204,9 @@ const configureRequest = async (
addDigestInterceptor(axiosInstance, request);
}
- request.timeout = preferencesUtil.getRequestTimeout();
+ // Get timeout from request settings, fallback to global preference
+ const resolvedSettings = resolveInheritedSettings(request.settings || {});
+ request.timeout = resolvedSettings.timeout;
// add cookies to request
if (preferencesUtil.shouldSendCookies()) {
@@ -276,7 +289,9 @@ const fetchGqlSchemaHandler = async (event, endpoint, environment, _request, col
const collectionRoot = get(collection, 'root', {});
const request = prepareGqlIntrospectionRequest(endpoint, resolvedVars, _request, collectionRoot);
- request.timeout = preferencesUtil.getRequestTimeout();
+ // Get timeout from request settings, resolve inheritance if needed
+ const resolvedSettings = resolveInheritedSettings(request.settings || {});
+ request.timeout = resolvedSettings.timeout;
if (!preferencesUtil.shouldVerifyTls()) {
request.httpsAgent = new https.Agent({
diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js
index 4ae8e307f..4d0fb5fac 100644
--- a/packages/bruno-electron/src/utils/collection.js
+++ b/packages/bruno-electron/src/utils/collection.js
@@ -3,6 +3,7 @@ const fs = require('fs');
const { getRequestUid } = require('../cache/requestUids');
const { uuid } = require('./common');
const os = require('os');
+const { preferencesUtil } = require('../store/preferences');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
@@ -523,6 +524,32 @@ const mergeAuth = (collection, request, requestTreePath) => {
}
};
+const resolveInheritedSettings = (settings) => {
+ const resolvedSettings = {};
+
+ // Resolve each setting individually
+ Object.keys(settings).forEach((settingKey) => {
+ const currentValue = settings[settingKey];
+
+ // If setting is inherited, fallback to preferences only for timeout setting
+ if (currentValue === 'inherit' || currentValue === undefined || currentValue === null) {
+ if (settingKey === 'timeout') {
+ resolvedSettings[settingKey] = preferencesUtil.getRequestTimeout();
+ }
+ } else {
+ // Use the current value as-is
+ resolvedSettings[settingKey] = currentValue;
+ }
+ });
+
+ // Handle missing timeout setting - if timeout is not in settings, treat it as inherited
+ if (!settings.hasOwnProperty('timeout')) {
+ resolvedSettings.timeout = preferencesUtil.getRequestTimeout();
+ }
+
+ return resolvedSettings;
+};
+
const sortByNameThenSequence = items => {
const isSeqValid = seq => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0;
@@ -585,5 +612,6 @@ module.exports = {
getAllRequestsInFolderRecursively,
getEnvVars,
getFormattedCollectionOauth2Credentials,
- sortByNameThenSequence
+ sortByNameThenSequence,
+ resolveInheritedSettings
};
\ No newline at end of file
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 7652f003d..e80daa170 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -427,20 +427,48 @@ const sem = grammar.createSemantics().addAttribute('ast', {
const keepAliveInterval = getNumFromRecord('keepAliveInterval');
- const timeout = getNumFromRecord('timeout');
+ const parsedSettings = {};
+ if (settings.followRedirects !== undefined) {
+ parsedSettings.followRedirects = typeof settings.followRedirects === 'boolean' ? settings.followRedirects : settings.followRedirects === 'true';
+ }
+
+ // Parse maxRedirects as number
+ if (settings.maxRedirects !== undefined) {
+ const maxRedirects = parseInt(settings.maxRedirects, 10);
+ if (!isNaN(maxRedirects)) {
+ parsedSettings.maxRedirects = maxRedirects;
+ }
+ }
+
+ // Parse timeout as number or inherit
+ if (settings.timeout !== undefined) {
+ if (settings.timeout === 'inherit') {
+ parsedSettings.timeout = 'inherit';
+ } else {
+ const timeout = parseInt(settings.timeout, 10);
+ if (!isNaN(timeout)) {
+ parsedSettings.timeout = timeout;
+ }
+ }
+ }
const _settings = {
- encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true'
+ encodeUrl: typeof settings.encodeUrl === 'boolean' ? settings.encodeUrl : settings.encodeUrl === 'true',
+ timeout: parsedSettings.timeout !== undefined ? parsedSettings.timeout : 0
};
+ if (parsedSettings.followRedirects !== undefined) {
+ _settings.followRedirects = parsedSettings.followRedirects;
+ }
+
+ if (parsedSettings.maxRedirects !== undefined) {
+ _settings.maxRedirects = parsedSettings.maxRedirects;
+ }
+
if (keepAliveInterval) {
_settings.keepAliveInterval = keepAliveInterval;
}
- if (timeout) {
- _settings.timeout = timeout;
- }
-
return {
settings: _settings
};
diff --git a/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.bru b/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.bru
new file mode 100644
index 000000000..df01c1f47
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.bru
@@ -0,0 +1,26 @@
+meta {
+ name: Settings All Options Test
+ type: http
+ seq: 3
+}
+
+put {
+ url: https://api.example.com/all-options
+}
+
+headers {
+ content-type: application/json
+}
+
+body:json {
+ {
+ "test": "data"
+ }
+}
+
+settings {
+ encodeUrl: true
+ followRedirects: false
+ maxRedirects: 0
+ timeout: 60000
+}
diff --git a/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.json b/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.json
new file mode 100644
index 000000000..cd81b4c21
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/settings/fixtures/settings-all-options.json
@@ -0,0 +1,27 @@
+{
+ "meta": {
+ "name": "Settings All Options Test",
+ "type": "http",
+ "seq": "3"
+ },
+ "http": {
+ "method": "put",
+ "url": "https://api.example.com/all-options"
+ },
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/json",
+ "enabled": true
+ }
+ ],
+ "body": {
+ "json": "{\n \"test\": \"data\"\n}"
+ },
+ "settings": {
+ "encodeUrl": true,
+ "followRedirects": false,
+ "maxRedirects": 0,
+ "timeout": 60000
+ }
+}
diff --git a/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.bru b/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.bru
new file mode 100644
index 000000000..c9daf67dd
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.bru
@@ -0,0 +1,14 @@
+meta {
+ name: Settings Minimal Test
+ type: http
+ seq: 2
+}
+
+post {
+ url: https://api.example.com/minimal
+}
+
+settings {
+ encodeUrl: false
+ timeout: 5000
+}
diff --git a/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.json b/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.json
new file mode 100644
index 000000000..7a88e1be4
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/settings/fixtures/settings-minimal.json
@@ -0,0 +1,15 @@
+{
+ "meta": {
+ "name": "Settings Minimal Test",
+ "type": "http",
+ "seq": "2"
+ },
+ "http": {
+ "method": "post",
+ "url": "https://api.example.com/minimal"
+ },
+ "settings": {
+ "encodeUrl": false,
+ "timeout": 5000
+ }
+}
diff --git a/packages/bruno-lang/v2/tests/settings/settings.spec.js b/packages/bruno-lang/v2/tests/settings/settings.spec.js
new file mode 100644
index 000000000..5a24471b7
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/settings/settings.spec.js
@@ -0,0 +1,58 @@
+const fs = require('fs');
+const path = require('path');
+const bruToJson = require('../../src/bruToJson');
+const jsonToBru = require('../../src/jsonToBru');
+
+describe('Settings Conversion Tests', () => {
+ const fixturesDir = path.join(__dirname, 'fixtures');
+
+ describe('parse (BRU to JSON)', () => {
+ it('should parse minimal settings from BRU to JSON', () => {
+ const input = fs.readFileSync(path.join(fixturesDir, 'settings-minimal.bru'), 'utf8');
+ const expected = require(path.join(fixturesDir, 'settings-minimal.json'));
+ const output = bruToJson(input);
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should parse all settings options from BRU to JSON', () => {
+ const input = fs.readFileSync(path.join(fixturesDir, 'settings-all-options.bru'), 'utf8');
+ const expected = require(path.join(fixturesDir, 'settings-all-options.json'));
+ const output = bruToJson(input);
+
+ expect(output).toEqual(expected);
+ });
+ });
+
+ describe('stringify (JSON to BRU)', () => {
+ it('should stringify minimal settings from JSON to BRU (with defaults)', () => {
+ const input = require(path.join(fixturesDir, 'settings-minimal.json'));
+ const expected = fs.readFileSync(path.join(fixturesDir, 'settings-minimal.bru'), 'utf8');
+ const output = jsonToBru(input);
+
+ expect(output).toEqual(expected);
+ });
+
+ it('should stringify all settings options from JSON to BRU', () => {
+ const input = require(path.join(fixturesDir, 'settings-all-options.json'));
+ const expected = fs.readFileSync(path.join(fixturesDir, 'settings-all-options.bru'), 'utf8');
+ const output = jsonToBru(input);
+
+ expect(output).toEqual(expected);
+ });
+ });
+
+ describe('round-trip conversion', () => {
+ it('should maintain data integrity through JSON -> BRU -> JSON conversion', () => {
+ const originalJson = require(path.join(fixturesDir, 'settings-all-options.json'));
+
+ // Convert JSON to BRU
+ const bru = jsonToBru(originalJson);
+
+ // Convert BRU back to JSON
+ const convertedJson = bruToJson(bru);
+
+ expect(convertedJson).toEqual(originalJson);
+ });
+ });
+});
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 1b0b82fd0..10852a3da 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -490,7 +490,10 @@ const itemSchema = Yup.object({
is: (type) => type === 'ws-request',
then: wsSettingsSchema,
otherwise: Yup.object({
- encodeUrl: Yup.boolean().nullable()
+ encodeUrl: Yup.boolean().nullable(),
+ followRedirects: Yup.boolean().nullable(),
+ maxRedirects: Yup.number().min(0).max(50).nullable(),
+ timeout: Yup.mixed().nullable(),
}).noUnknown(true)
.strict()
.nullable()
diff --git a/packages/bruno-tests/collection/request-setting/folder.bru b/packages/bruno-tests/collection/request-setting/folder.bru
new file mode 100644
index 000000000..5a6cf4d55
--- /dev/null
+++ b/packages/bruno-tests/collection/request-setting/folder.bru
@@ -0,0 +1,8 @@
+meta {
+ name: request-setting
+ seq: 14
+}
+
+auth {
+ mode: inherit
+}
diff --git a/packages/bruno-tests/collection/request-setting/follow-redirect.bru b/packages/bruno-tests/collection/request-setting/follow-redirect.bru
new file mode 100644
index 000000000..065120554
--- /dev/null
+++ b/packages/bruno-tests/collection/request-setting/follow-redirect.bru
@@ -0,0 +1,23 @@
+meta {
+ name: follow-redirect
+ type: http
+ seq: 1
+}
+
+get {
+ url: https://httpbun.com/redirect/3
+ body: none
+ auth: inherit
+}
+
+script:post-response {
+ test("body should include redirecting", function() {
+ const data = res.getBody();
+ expect(data).to.include("Redirecting...");
+ });
+}
+
+settings {
+ encodeUrl: true
+ followRedirects: false
+}
diff --git a/packages/bruno-tests/collection/request-setting/max-redirect.bru b/packages/bruno-tests/collection/request-setting/max-redirect.bru
new file mode 100644
index 000000000..20bcb5678
--- /dev/null
+++ b/packages/bruno-tests/collection/request-setting/max-redirect.bru
@@ -0,0 +1,24 @@
+meta {
+ name: max-redirect
+ type: http
+ seq: 2
+}
+
+get {
+ url: https://httpbun.com/redirect/3
+ body: none
+ auth: inherit
+}
+
+script:post-response {
+ test("body should include redirecting", function() {
+ const data = res.status;
+ expect(data).to.be.equal(200)
+ });
+}
+
+settings {
+ encodeUrl: true
+ followRedirects: true
+ maxRedirects: 5
+}
diff --git a/tests/request/settings/collection/bruno.json b/tests/request/settings/collection/bruno.json
new file mode 100644
index 000000000..abe6c3cbf
--- /dev/null
+++ b/tests/request/settings/collection/bruno.json
@@ -0,0 +1,9 @@
+{
+ "version": "1",
+ "name": "settings-test",
+ "type": "collection",
+ "ignore": [
+ "node_modules",
+ ".git"
+ ]
+}
\ No newline at end of file
diff --git a/tests/request/settings/collection/max-redirects.bru b/tests/request/settings/collection/max-redirects.bru
new file mode 100644
index 000000000..a041ff519
--- /dev/null
+++ b/tests/request/settings/collection/max-redirects.bru
@@ -0,0 +1,17 @@
+meta {
+ name: max-redirects-test
+ type: http
+ seq: 1
+}
+
+get {
+ url: https://httpbun.com/redirect/2
+ body: none
+ auth: inherit
+}
+
+settings {
+ followRedirects: true
+ maxRedirects: 1
+ timeout: 0
+}
diff --git a/tests/request/settings/collection/no-redirects.bru b/tests/request/settings/collection/no-redirects.bru
new file mode 100644
index 000000000..b5c824d43
--- /dev/null
+++ b/tests/request/settings/collection/no-redirects.bru
@@ -0,0 +1,17 @@
+meta {
+ name: no-redirects-test
+ type: http
+ seq: 2
+}
+
+get {
+ url: https://httpbun.com/redirect/2
+ body: none
+ auth: inherit
+}
+
+settings {
+ followRedirects: false
+ maxRedirects: 5
+ timeout: 0
+}
diff --git a/tests/request/settings/collection/timeout.bru b/tests/request/settings/collection/timeout.bru
new file mode 100644
index 000000000..ee40d4cae
--- /dev/null
+++ b/tests/request/settings/collection/timeout.bru
@@ -0,0 +1,17 @@
+meta {
+ name: timeout-test
+ type: http
+ seq: 3
+}
+
+get {
+ url: https://httpbun.com/redirect/2
+ body: none
+ auth: inherit
+}
+
+settings {
+ followRedirects: false
+ maxRedirects: 0
+ timeout: 5
+}
diff --git a/tests/request/settings/init-user-data/collection-security.json b/tests/request/settings/init-user-data/collection-security.json
new file mode 100644
index 000000000..f60c658a5
--- /dev/null
+++ b/tests/request/settings/init-user-data/collection-security.json
@@ -0,0 +1,10 @@
+{
+ "collections": [
+ {
+ "path": "{{projectRoot}}/tests/request/settings/collection",
+ "securityConfig": {
+ "jsSandboxMode": "safe"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/tests/request/settings/init-user-data/preferences.json b/tests/request/settings/init-user-data/preferences.json
new file mode 100644
index 000000000..f59ee0971
--- /dev/null
+++ b/tests/request/settings/init-user-data/preferences.json
@@ -0,0 +1,6 @@
+{
+ "maximized": true,
+ "lastOpenedCollections": [
+ "{{projectRoot}}/tests/request/settings/collection"
+ ]
+}
\ No newline at end of file
diff --git a/tests/request/settings/max-redirects.spec.ts b/tests/request/settings/max-redirects.spec.ts
new file mode 100644
index 000000000..22de00ad4
--- /dev/null
+++ b/tests/request/settings/max-redirects.spec.ts
@@ -0,0 +1,46 @@
+import { test, expect } from '../../../playwright';
+
+test.describe('Max Redirects Settings Tests', () => {
+ test('should configure and test max redirects settings', async ({
+ pageWithUserData: page
+ }) => {
+ // Navigate to the test collection and request
+ await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();
+
+ await page.locator('#sidebar-collection-name').getByText('settings-test').click();
+
+ // Navigate to the max-redirects request
+ await page.getByRole('complementary').getByText('max-redirects').click();
+
+ // Go to Settings tab
+ await page.getByRole('tab', { name: 'Settings' }).click();
+
+ // Test Max Redirects Settings
+ const maxRedirectsInput = page.locator('input[id="maxRedirects"]');
+ await expect(maxRedirectsInput).toBeVisible();
+
+ // Verify default value from .bru file (1)
+ await expect(maxRedirectsInput).toHaveValue('1');
+
+ // Test Follow Redirects toggle
+ const followRedirectsToggle = page.getByTestId('follow-redirects-toggle');
+ await expect(followRedirectsToggle).toBeVisible();
+ await expect(followRedirectsToggle).toBeChecked();
+
+ // Send the request
+ await page.getByTestId('send-arrow-icon').click();
+
+ await expect(page.getByTestId('response-status-code')).toContainText('302', { timeout: 15000 });
+
+ // change the max redirects to 2
+ await maxRedirectsInput.fill('2');
+ await page.getByTestId('send-arrow-icon').click();
+ await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });
+ });
+
+ test.afterEach(async ({ pageWithUserData: page }) => {
+ // Close the single open tab
+ await page.locator('.close-icon-container').click();
+ await page.locator('button:has-text("Don\'t Save")').first().click();
+ });
+});
diff --git a/tests/request/settings/no-redirects.spec.ts b/tests/request/settings/no-redirects.spec.ts
new file mode 100644
index 000000000..c9e6d828f
--- /dev/null
+++ b/tests/request/settings/no-redirects.spec.ts
@@ -0,0 +1,50 @@
+import { test, expect } from '../../../playwright';
+
+test.describe('No Redirects Settings Tests', () => {
+ test('should configure and test no redirects settings', async ({
+ pageWithUserData: page
+ }) => {
+ // Navigate to the test collection and request
+ await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();
+
+ await page.locator('#sidebar-collection-name').getByText('settings-test').click();
+
+ // Navigate to the no-redirects request
+ await page.getByRole('complementary').getByText('no-redirects').click();
+
+ // Go to Settings tab
+ await page.getByRole('tab', { name: 'Settings' }).click();
+
+ // Test No Redirects Settings
+ const maxRedirectsInput = page.locator('input[id="maxRedirects"]');
+ await expect(maxRedirectsInput).toBeVisible();
+
+ // Verify default value from .bru file (5)
+ await expect(maxRedirectsInput).toHaveValue('5');
+
+ // Test Follow Redirects toggle - should be unchecked
+ const followRedirectsToggle = page.getByTestId('follow-redirects-toggle');
+ await expect(followRedirectsToggle).toBeVisible();
+ await expect(followRedirectsToggle).not.toBeChecked();
+
+ // Send the request - should stop at first redirect (302) without following
+ await page.getByTestId('send-arrow-icon').click();
+
+ // Should get 302 because redirects are disabled, regardless of maxRedirects value
+ await expect(page.getByTestId('response-status-code')).toContainText('302', { timeout: 15000 });
+
+ // Toggle follow redirects to true
+ await followRedirectsToggle.click();
+ await expect(followRedirectsToggle).toBeChecked();
+
+ // Send request again - now should follow redirects and get 200
+ await page.getByTestId('send-arrow-icon').click();
+ await expect(page.getByTestId('response-status-code')).toContainText('200', { timeout: 15000 });
+ });
+
+ test.afterEach(async ({ pageWithUserData: page }) => {
+ // Close the single open tab
+ await page.locator('.close-icon-container').click();
+ await page.locator('button:has-text("Don\'t Save")').first().click();
+ });
+});
diff --git a/tests/request/settings/timeout.spec.ts b/tests/request/settings/timeout.spec.ts
new file mode 100644
index 000000000..8c54d2d2d
--- /dev/null
+++ b/tests/request/settings/timeout.spec.ts
@@ -0,0 +1,38 @@
+import { test, expect } from '../../../playwright';
+import { closeAllCollections } from '../../utils/page';
+
+test.describe('Timeout Settings Tests', () => {
+ test('should configure and test timeout settings', async ({
+ pageWithUserData: page
+ }) => {
+ // Navigate to the test collection and request
+ await expect(page.locator('#sidebar-collection-name').getByText('settings-test')).toBeVisible();
+
+ await page.locator('#sidebar-collection-name').getByText('settings-test').click();
+ // Navigate to thetimeout request
+ await page.getByRole('complementary').getByText('timeout-test').click();
+
+ // Go to Settings tab
+ await page.getByRole('tab', { name: 'Settings' }).click();
+
+ // Test Timeout Settings
+ const timeoutInput = page.locator('input[id="timeout"]');
+ await expect(timeoutInput).toBeVisible();
+
+ // Verify default value from .bru file (5)
+ await expect(timeoutInput).toHaveValue('5');
+
+ await page.getByTestId('send-arrow-icon').click();
+
+ const responsePane = page.locator('.response-pane');
+ await expect(responsePane).toContainText('timeout of 5ms exceeded');
+
+ // go to welcome page
+ await page.locator('.bruno-logo').click();
+ });
+
+ test.afterEach(async ({ pageWithUserData: page }) => {
+ // cleanup: close all collections
+ await closeAllCollections(page);
+ });
+});