Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 0 additions & 76 deletions src/extension/ipc/network/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -768,15 +768,6 @@ const registerNetworkIpc = (): void => {
tags: request.tags || []
};

const scriptContentType = scriptRequest.headers['Content-Type'] || scriptRequest.headers['content-type'] || '';
const isFormUrlEncodedBefore = scriptContentType === 'application/x-www-form-urlencoded' ||
(request.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (isFormUrlEncodedBefore && Array.isArray(scriptRequest.data)) {
scriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(scriptRequest.data);
} else if (isFormUrlEncodedBefore && scriptRequest.data && typeof scriptRequest.data === 'object' && !Array.isArray(scriptRequest.data)) {
scriptRequest.data = qs.stringify(scriptRequest.data, { arrayFormat: 'repeat' });
}

const context: RequestContext = {
uid: uuidv4(),
cancelTokenUid,
Expand Down Expand Up @@ -821,14 +812,6 @@ const registerNetworkIpc = (): void => {
name: innerRequest.name || '',
tags: innerRequest.tags || []
};
const innerScriptContentType = innerScriptRequest.headers['Content-Type'] || innerScriptRequest.headers['content-type'] || '';
const innerIsFormUrlEncoded = innerScriptContentType === 'application/x-www-form-urlencoded' ||
(innerRequest.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (innerIsFormUrlEncoded && Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(innerScriptRequest.data);
} else if (innerIsFormUrlEncoded && innerScriptRequest.data && typeof innerScriptRequest.data === 'object' && !Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = qs.stringify(innerScriptRequest.data, { arrayFormat: 'repeat' });
}
const innerContext: RequestContext = {
uid: uuidv4(),
cancelTokenUid,
Expand Down Expand Up @@ -879,17 +862,6 @@ const registerNetworkIpc = (): void => {
console.error('Inner request pre-request script error:', preReqError);
}

const innerPostScriptContentType = innerScriptRequest.headers['Content-Type'] || innerScriptRequest.headers['content-type'] || '';
const innerIsFormUrlEncodedAfter = innerPostScriptContentType === 'application/x-www-form-urlencoded' ||
(innerRequest.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (innerIsFormUrlEncodedAfter && Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(innerScriptRequest.data);
} else if (innerIsFormUrlEncodedAfter && innerScriptRequest.data && typeof innerScriptRequest.data === 'object' && !Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = Object.entries(innerScriptRequest.data as Record<string, unknown>)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value ?? ''))}`)
.join('&');
}

const result = await executeRequest(innerScriptRequest as unknown as BrunoRequest, innerContext);

const innerResponse = {
Expand Down Expand Up @@ -1011,18 +983,6 @@ const registerNetworkIpc = (): void => {
};
}

// This handles cases where req.setBody() was called with an array in the script
const postScriptContentType = scriptRequest.headers['Content-Type'] || scriptRequest.headers['content-type'] || '';
const isFormUrlEncoded = postScriptContentType === 'application/x-www-form-urlencoded' ||
(scriptRequest.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (isFormUrlEncoded && Array.isArray(scriptRequest.data)) {
scriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(scriptRequest.data);
} else if (isFormUrlEncoded && scriptRequest.data && typeof scriptRequest.data === 'object' && !Array.isArray(scriptRequest.data)) {
// This properly handles nested objects, arrays, and special characters
scriptRequest.data = qs.stringify(scriptRequest.data, { arrayFormat: 'repeat' });
}
// if `data` is of string type - return as-is (assumes already encoded)

sendToWebview('main:run-request-event', {
type: 'request-sent',
requestSent: {
Expand Down Expand Up @@ -1744,13 +1704,6 @@ const registerNetworkIpc = (): void => {
tags: request.tags || []
};

const runnerScriptContentType = scriptRequest.headers['Content-Type'] || scriptRequest.headers['content-type'] || '';
const runnerIsFormUrlEncodedBefore = runnerScriptContentType === 'application/x-www-form-urlencoded' ||
(request.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (runnerIsFormUrlEncodedBefore && Array.isArray(scriptRequest.data)) {
scriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(scriptRequest.data);
}

const runnerProcessEnvVars = getProcessEnvVars(collectionUid) as Record<string, string>;

const runnerRunRequestByItemPathname = async (relativeItemPathname: string): Promise<unknown> => {
Expand Down Expand Up @@ -1781,12 +1734,6 @@ const registerNetworkIpc = (): void => {
name: innerRequest.name || '',
tags: innerRequest.tags || []
};
const runnerInnerScriptContentType = innerScriptRequest.headers['Content-Type'] || innerScriptRequest.headers['content-type'] || '';
const runnerInnerIsFormUrlEncoded = runnerInnerScriptContentType === 'application/x-www-form-urlencoded' ||
(innerRequest.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (runnerInnerIsFormUrlEncoded && Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(innerScriptRequest.data);
}
const innerContext: RequestContext = {
uid: uuidv4(),
cancelTokenUid,
Expand Down Expand Up @@ -1837,17 +1784,6 @@ const registerNetworkIpc = (): void => {
console.error('Inner request pre-request script error:', preReqError);
}

const runnerInnerPostScriptContentType = innerScriptRequest.headers['Content-Type'] || innerScriptRequest.headers['content-type'] || '';
const runnerInnerIsFormUrlEncodedAfter = runnerInnerPostScriptContentType === 'application/x-www-form-urlencoded' ||
(innerRequest.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (runnerInnerIsFormUrlEncodedAfter && Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(innerScriptRequest.data);
} else if (runnerInnerIsFormUrlEncodedAfter && innerScriptRequest.data && typeof innerScriptRequest.data === 'object' && !Array.isArray(innerScriptRequest.data)) {
innerScriptRequest.data = Object.entries(innerScriptRequest.data as Record<string, unknown>)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value ?? ''))}`)
.join('&');
}

const result = await executeRequest(innerScriptRequest as unknown as BrunoRequest, innerContext);

const innerResponse = {
Expand Down Expand Up @@ -1967,18 +1903,6 @@ const registerNetworkIpc = (): void => {
continue;
}

// This handles cases where req.setBody() was called with an array in the script
const runnerPostScriptContentType = scriptRequest.headers['Content-Type'] || scriptRequest.headers['content-type'] || '';
const runnerIsFormUrlEncodedAfter = runnerPostScriptContentType === 'application/x-www-form-urlencoded' ||
(request.body as { mode?: string } | undefined)?.mode === 'formUrlEncoded';
if (runnerIsFormUrlEncodedAfter && Array.isArray(scriptRequest.data)) {
scriptRequest.data = brunoUtils.buildFormUrlEncodedPayload(scriptRequest.data);
} else if (runnerIsFormUrlEncodedAfter && scriptRequest.data && typeof scriptRequest.data === 'object' && !Array.isArray(scriptRequest.data)) {
scriptRequest.data = Object.entries(scriptRequest.data as Record<string, unknown>)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value ?? ''))}`)
.join('&');
}

const requestSent = {
url: scriptRequest.url,
method: scriptRequest.method,
Expand Down
170 changes: 170 additions & 0 deletions src/extension/ipc/network/interpolate-vars.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { describe, test, expect } from 'vitest';
import { interpolateVars } from './interpolate-vars';

describe('interpolateVars', () => {
const baseOptions = {
envVars: { API_URL: 'https://api.example.com', TOKEN: 'secret123' },
collectionVariables: {},
folderVariables: {},
requestVariables: {},
runtimeVariables: {},
processEnvVars: { NODE_ENV: 'test' },
globalEnvironmentVariables: {}
};

describe('formUrlEncoded body interpolation', () => {
test('interpolates variables in body.formUrlEncoded values', () => {
const request = {
url: 'https://example.com',
headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded', enabled: true }],
body: {
mode: 'formUrlEncoded',
formUrlEncoded: [
{ name: 'username', value: '{{TOKEN}}', enabled: true },
{ name: 'url', value: '{{API_URL}}/auth', enabled: true },
{ name: 'static', value: 'plain-value', enabled: true }
]
},
data: [
{ name: 'username', value: '{{TOKEN}}', enabled: true },
{ name: 'url', value: '{{API_URL}}/auth', enabled: true },
{ name: 'static', value: 'plain-value', enabled: true }
]
};

const result = interpolateVars(request, baseOptions);

// body.formUrlEncoded should be interpolated
expect(result.body.formUrlEncoded[0].value).toBe('secret123');
expect(result.body.formUrlEncoded[1].value).toBe('https://api.example.com/auth');
expect(result.body.formUrlEncoded[2].value).toBe('plain-value');

// data (array format) should also be interpolated
expect(result.data[0].value).toBe('secret123');
expect(result.data[1].value).toBe('https://api.example.com/auth');
expect(result.data[2].value).toBe('plain-value');
});

test('interpolates process.env variables in formUrlEncoded values', () => {
const request = {
url: 'https://example.com',
headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded', enabled: true }],
body: {
mode: 'formUrlEncoded',
formUrlEncoded: [
{ name: 'env', value: '{{process.env.NODE_ENV}}', enabled: true }
]
},
data: [
{ name: 'env', value: '{{process.env.NODE_ENV}}', enabled: true }
]
};

const result = interpolateVars(request, baseOptions);

expect(result.body.formUrlEncoded[0].value).toBe('test');
expect(result.data[0].value).toBe('test');
});

test('interpolates variables in formUrlEncoded names', () => {
const request = {
url: 'https://example.com',
headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded', enabled: true }],
body: {
mode: 'formUrlEncoded',
formUrlEncoded: [
{ name: '{{TOKEN}}', value: 'some-value', enabled: true }
]
},
data: [
{ name: '{{TOKEN}}', value: 'some-value', enabled: true }
]
};

const result = interpolateVars(request, baseOptions);

expect(result.body.formUrlEncoded[0].name).toBe('secret123');
});

test('does NOT interpolate variables when data is a pre-encoded string (regression guard)', () => {
// This test documents the bug from issue #58:
// If buildFormUrlEncodedPayload is called BEFORE interpolation,
// {{variables}} get URL-encoded to %7B%7Bvariables%7D%7D and
// the interpolation engine can't recognize them.
const request = {
url: 'https://example.com',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
// Pre-encoded string — this is what the old code produced
data: 'username=%7B%7BTOKEN%7D%7D&url=%7B%7BAPI_URL%7D%7D%2Fauth'
};

const result = interpolateVars(request, baseOptions);

// The pre-encoded data CANNOT be interpolated — variables remain URL-encoded
expect(result.data).toContain('%7B%7BTOKEN%7D%7D');
expect(result.data).not.toContain('secret123');
});

test('correctly interpolates variables when data is kept as array (current behavior)', () => {
// This test verifies the fix: data stays as array, interpolation works
const request = {
url: 'https://example.com',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
data: [
{ name: 'username', value: '{{TOKEN}}', enabled: true },
{ name: 'url', value: '{{API_URL}}/auth', enabled: true }
]
};

const result = interpolateVars(request, baseOptions);

expect(result.data[0].value).toBe('secret123');
expect(result.data[1].value).toBe('https://api.example.com/auth');
});
});

describe('JSON body interpolation', () => {
test('interpolates variables in body.json', () => {
const request = {
url: 'https://example.com',
headers: [{ name: 'content-type', value: 'application/json', enabled: true }],
body: {
mode: 'json',
json: '{"token": "{{TOKEN}}"}'
}
};

const result = interpolateVars(request, baseOptions);

expect(result.body.json).toBe('{"token": "secret123"}');
});
});

describe('URL interpolation', () => {
test('interpolates variables in URL', () => {
const request = {
url: '{{API_URL}}/users',
headers: []
};

const result = interpolateVars(request, baseOptions);

expect(result.url).toBe('https://api.example.com/users');
});
});

describe('header interpolation', () => {
test('interpolates variables in header values (array format)', () => {
const request = {
url: 'https://example.com',
headers: [
{ name: 'Authorization', value: 'Bearer {{TOKEN}}', enabled: true }
]
};

const result = interpolateVars(request, baseOptions);

expect(result.headers[0].value).toBe('Bearer secret123');
});
});
});