From 924bc2e79eab86590b40510c0b88b8aaf57528f5 Mon Sep 17 00:00:00 2001 From: "Siddharth Gelera (reaper)" Date: Thu, 9 Oct 2025 18:25:28 +0530 Subject: [PATCH] Merge pull request #5713 from barelyhuman/fix/form-values-seq-5237 fix: reimplement payload serialization for `x-www-form-encoded` --- .../bruno-cli/src/runner/interpolate-vars.js | 11 +++++------ .../bruno-cli/src/runner/prepare-request.js | 3 +-- .../bruno-cli/src/runner/run-single-request.js | 6 +++--- packages/bruno-cli/src/utils/form-data.js | 18 ++++++------------ .../bruno-electron/src/ipc/network/index.js | 4 ++-- .../src/ipc/network/interpolate-vars.js | 11 +++++------ .../src/ipc/network/prepare-request.js | 2 +- packages/bruno-electron/src/utils/form-data.js | 18 ++++++------------ .../tests/network/prepare-request.spec.js | 8 ++++---- 9 files changed, 33 insertions(+), 48 deletions(-) diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index d0c385743..7e19265b1 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -80,12 +80,11 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } } } else if (contentType === 'application/x-www-form-urlencoded') { - if (typeof request.data === 'object') { - try { - forOwn(request?.data, (value, key) => { - request.data[key] = _interpolate(value); - }); - } catch (err) {} + if (request.data && Array.isArray(request.data)) { + request.data = request.data.map((d) => ({ + ...d, + value: _interpolate(d?.value) + })); } } else if (contentType === 'multipart/form-data') { if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index c2b1d8b43..6c6758ab2 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -6,7 +6,6 @@ const decomment = require('decomment'); const crypto = require('node:crypto'); const fs = require('node:fs'); const { mergeHeaders, mergeScripts, mergeVars, mergeAuth, getTreePathFromCollectionToItem } = require('../utils/collection'); -const { buildFormUrlEncodedPayload } = require('../utils/form-data'); const path = require('node:path'); const { isLargeFile } = require('../utils/filesystem'); const { getFormattedOauth2Credentials } = require('../utils/oauth2'); @@ -356,7 +355,7 @@ const prepareRequest = async (item = {}, collection = {}) => { axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; } const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled); - axiosRequest.data = buildFormUrlEncodedPayload(enabledParams); + axiosRequest.data = enabledParams; } if (request.body.mode === 'multipartForm') { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index 95f4787ab..a196cf8a7 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,7 +19,7 @@ const { shouldUseProxy, PatchedHttpsProxyAgent, getSystemProxyEnvVariables } = r const path = require('path'); const { parseDataFromResponse } = require('../utils/common'); const { getCookieStringForUrl, saveCookies } = require('../utils/cookies'); -const { createFormData } = require('../utils/form-data'); +const { createFormData, buildFormUrlEncodedPayload } = require('../utils/form-data'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const { NtlmClient } = require('axios-ntlm'); const { addDigestInterceptor } = require('@usebruno/requests'); @@ -333,7 +333,7 @@ const runSingleRequest = async function ( name => name.toLowerCase() === 'content-type' ); if (contentTypeHeader && request.headers[contentTypeHeader] === 'application/x-www-form-urlencoded') { - request.data = qs.stringify(request.data, { arrayFormat: 'repeat' }); + request.data = buildFormUrlEncodedPayload(request.data); } if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') { @@ -406,7 +406,7 @@ const runSingleRequest = async function ( }); if (request.ntlmConfig) { - axiosInstance=NtlmClient(request.ntlmConfig,axiosInstance.defaults) + axiosInstance = NtlmClient(request.ntlmConfig, axiosInstance.defaults); delete request.ntlmConfig; } diff --git a/packages/bruno-cli/src/utils/form-data.js b/packages/bruno-cli/src/utils/form-data.js index 7bb00ba81..180651c96 100644 --- a/packages/bruno-cli/src/utils/form-data.js +++ b/packages/bruno-cli/src/utils/form-data.js @@ -5,20 +5,14 @@ const path = require('path'); /** * @param {Array.} params The request body Array - * @returns {object} Returns an obj with repeating key as an array of values - * {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4} + * @returns {string} Returns a order respecting standard compliant string of form encoded values */ const buildFormUrlEncodedPayload = (params) => { - return params.reduce((acc, p) => { - if (!acc[p.name]) { - acc[p.name] = p.value; - } else if (Array.isArray(acc[p.name])) { - acc[p.name].push(p.value); - } else { - acc[p.name] = [acc[p.name], p.value]; - } - return acc; - }, {}); + const resultParams = new URLSearchParams(); + for (const param of params) { + resultParams.append(param.name, param.value); + } + return resultParams.toString(); }; diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index f40ed7dc2..a8f736b08 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -23,7 +23,7 @@ const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../util const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseDataFromRequest } = require('../../utils/common'); const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); -const { createFormData } = require('../../utils/form-data'); +const { createFormData, buildFormUrlEncodedPayload } = require('../../utils/form-data'); const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant, updateCollectionOauth2Credentials } = require('../../utils/oauth2'); const { preferencesUtil } = require('../../store/preferences'); @@ -424,7 +424,7 @@ const registerNetworkIpc = (mainWindow) => { // stringify the request url encoded params if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { - request.data = qs.stringify(request.data, { arrayFormat: 'repeat' }); + request.data = buildFormUrlEncodedPayload(request.data); } if (request.headers['content-type'] === 'multipart/form-data') { diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index a3b869156..569b06f7d 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -104,12 +104,11 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } catch (err) {} } } else if (contentType === 'application/x-www-form-urlencoded') { - if (typeof request.data === 'object') { - try { - forOwn(request?.data, (value, key) => { - request.data[key] = _interpolate(value); - }); - } catch (err) {} + if (request.data && Array.isArray(request.data)) { + request.data = request.data.map((d) => ({ + ...d, + value: _interpolate(d?.value) + })); } } else if (contentType === 'multipart/form-data') { if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index 663d52e10..01c11a27e 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -422,7 +422,7 @@ const prepareRequest = async (item, collection = {}, abortController) => { axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded'; } const enabledParams = filter(request.body.formUrlEncoded, (p) => p.enabled); - axiosRequest.data = buildFormUrlEncodedPayload(enabledParams); + axiosRequest.data = enabledParams; } if (request.body.mode === 'multipartForm') { diff --git a/packages/bruno-electron/src/utils/form-data.js b/packages/bruno-electron/src/utils/form-data.js index dc27b2577..5e50a76f4 100644 --- a/packages/bruno-electron/src/utils/form-data.js +++ b/packages/bruno-electron/src/utils/form-data.js @@ -5,20 +5,14 @@ const path = require('path'); /** * @param {Array.} params The request body Array - * @returns {object} Returns an obj with repeating key as an array of values - * {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4} + * @returns {string} Returns a order respecting standard compliant string of form encoded values */ const buildFormUrlEncodedPayload = (params) => { - return params.reduce((acc, p) => { - if (!acc[p.name]) { - acc[p.name] = p.value; - } else if (Array.isArray(acc[p.name])) { - acc[p.name].push(p.value); - } else { - acc[p.name] = [acc[p.name], p.value]; - } - return acc; - }, {}); + const resultParams = new URLSearchParams(); + for (const param of params) { + resultParams.append(param.name, param.value); + } + return resultParams.toString(); }; diff --git a/packages/bruno-electron/tests/network/prepare-request.spec.js b/packages/bruno-electron/tests/network/prepare-request.spec.js index f8c014869..a0cef78e2 100644 --- a/packages/bruno-electron/tests/network/prepare-request.spec.js +++ b/packages/bruno-electron/tests/network/prepare-request.spec.js @@ -23,7 +23,7 @@ describe('prepare-request: prepareRequest', () => { it('should handle single key-value pair', () => { const requestObj = [{ name: 'item', value: 2 }]; - const expected = { item: 2 }; + const expected = 'item=2'; const result = buildFormUrlEncodedPayload(requestObj); expect(result).toEqual(expected); }); @@ -33,7 +33,7 @@ describe('prepare-request: prepareRequest', () => { { name: 'item1', value: 2 }, { name: 'item2', value: 3 } ]; - const expected = { item1: 2, item2: 3 }; + const expected = 'item1=2&item2=3'; const result = buildFormUrlEncodedPayload(requestObj); expect(result).toEqual(expected); }); @@ -43,7 +43,7 @@ describe('prepare-request: prepareRequest', () => { { name: 'item', value: 2 }, { name: 'item', value: 3 } ]; - const expected = { item: [2, 3] }; + const expected = 'item=2&item=3'; const result = buildFormUrlEncodedPayload(requestObj); expect(result).toEqual(expected); }); @@ -54,7 +54,7 @@ describe('prepare-request: prepareRequest', () => { { name: 'item2', value: 3 }, { name: 'item1', value: 4 } ]; - const expected = { item1: [2, 4], item2: 3 }; + const expected = 'item1=2&item2=3&item1=4'; const result = buildFormUrlEncodedPayload(requestObj); expect(result).toEqual(expected); });