From bb0096eb386d44c299d0b2869dcb36a305a40ce3 Mon Sep 17 00:00:00 2001 From: Sanjai Kumar <161328623+sanjaikumar-bruno@users.noreply.github.com> Date: Thu, 27 Nov 2025 18:12:20 +0530 Subject: [PATCH] feat: added multipart data formatting in timeline (#6185) refactor: remove escapeHeaderValue function and enhance formatMultipartData utility --- packages/bruno-electron/src/utils/common.js | 15 ++++- .../bruno-electron/src/utils/form-data.js | 58 ++++++++++++++++++- .../bruno-electron/tests/utils/common.spec.js | 25 +++++++- .../tests/utils/form-data.spec.js | 46 +++++++++++++++ 4 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 packages/bruno-electron/tests/utils/form-data.spec.js diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index a855e5523..396f40752 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -1,6 +1,8 @@ const { customAlphabet } = require('nanoid'); const iconv = require('iconv-lite'); const { cloneDeep } = require('lodash'); +const FormData = require('form-data'); +const { formatMultipartData } = require('./form-data'); // a customized version of nanoid without using _ and - const uuid = () => { @@ -128,7 +130,18 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => }; const parseDataFromRequest = (request) => { - const requestDataString = request.mode == 'file'? "": (typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data)); + let requestDataString; + + // File uploads are redacted, multipart FormData is formatted from original data for readability, and other types are stringified as-is. + if (request.mode === 'file') { + requestDataString = ''; + } else if (request?.data instanceof FormData && Array.isArray(request._originalMultipartData)) { + const boundary = request.data._boundary || 'boundary'; + requestDataString = formatMultipartData(request._originalMultipartData, boundary); + } else { + requestDataString = typeof request?.data === 'string' ? request?.data : safeStringifyJSON(request?.data); + } + const requestCopy = cloneDeep(request); if (!requestCopy.data) { return { data: null, dataBuffer: null }; diff --git a/packages/bruno-electron/src/utils/form-data.js b/packages/bruno-electron/src/utils/form-data.js index 9bff10442..540ee273e 100644 --- a/packages/bruno-electron/src/utils/form-data.js +++ b/packages/bruno-electron/src/utils/form-data.js @@ -3,6 +3,61 @@ const FormData = require('form-data'); const fs = require('fs'); const path = require('path'); +const formatMultipartData = (multipartData, boundary) => { + if (!Array.isArray(multipartData) || multipartData.length === 0) { + return ''; + } + + const normalizeBoundary = (b) => { + const value = b || 'boundary'; + return value.replace(/^--+/, '').replace(/--+$/, ''); + }; + + const getFileName = (filePath) => { + if (typeof filePath === 'string' && filePath.trim()) { + return path.basename(filePath) || 'file'; + } + return 'file'; + }; + + const formatValue = (value) => { + if (Array.isArray(value)) { + return value.map((v) => String(v ?? '')).join(', '); + } + return String(value ?? ''); + }; + + const boundaryValue = normalizeBoundary(boundary); + const parts = []; + + multipartData.forEach((field) => { + if (!field || !field.name) return; + + parts.push(`----${boundaryValue}`); + parts.push('Content-Disposition: form-data'); + + if (field.type === 'file') { + const filePaths = Array.isArray(field.value) ? field.value : (field.value ? [field.value] : ['']); + filePaths.forEach((filePath) => { + parts.push(`----${boundaryValue}`); + parts.push('Content-Disposition: form-data'); + const fileName = getFileName(filePath); + parts.push(`name: ${field.name}`); + parts.push(`value: [File: ${fileName}]`); + parts.push(''); + }); + } else { + const value = formatValue(field.value); + parts.push(`name: ${field.name}`); + parts.push(`value: ${value}`); + parts.push(''); + } + }); + + parts.push(`----${boundaryValue}--`); + return parts.join('\n'); +}; + const createFormData = (data, collectionPath) => { // make axios work in node using form data // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 @@ -38,5 +93,6 @@ const createFormData = (data, collectionPath) => { }; module.exports = { - createFormData + createFormData, + formatMultipartData }; diff --git a/packages/bruno-electron/tests/utils/common.spec.js b/packages/bruno-electron/tests/utils/common.spec.js index 077aac16d..ded1dbebb 100644 --- a/packages/bruno-electron/tests/utils/common.spec.js +++ b/packages/bruno-electron/tests/utils/common.spec.js @@ -1,4 +1,5 @@ -const { flattenDataForDotNotation } = require('../../src/utils/common'); +const { flattenDataForDotNotation, parseDataFromRequest } = require('../../src/utils/common'); +const FormData = require('form-data'); describe('utils: flattenDataForDotNotation', () => { test('Flatten a simple object with dot notation', () => { @@ -82,4 +83,24 @@ describe('utils: flattenDataForDotNotation', () => { expect(flattenDataForDotNotation(input)).toEqual(expectedOutput); }); -}); \ No newline at end of file +}); + +describe('utils: parseDataFromRequest', () => { + test('should format multipart FormData', () => { + const formData = new FormData(); + formData._boundary = 'boundary123'; + const request = { + data: formData, + _originalMultipartData: [ + { name: 'description', type: 'text', value: 'dfv' }, + { name: 'file', type: 'file', value: ['Dumy.xml'] } + ], + headers: {} + }; + + const result = parseDataFromRequest(request); + expect(result.data).toContain('name: description'); + expect(result.data).toContain('value: dfv'); + expect(result.data).toContain('value: [File: Dumy.xml]'); + }); +}); diff --git a/packages/bruno-electron/tests/utils/form-data.spec.js b/packages/bruno-electron/tests/utils/form-data.spec.js new file mode 100644 index 000000000..8d7443ef7 --- /dev/null +++ b/packages/bruno-electron/tests/utils/form-data.spec.js @@ -0,0 +1,46 @@ +const { formatMultipartData } = require('../../src/utils/form-data'); + +describe('utils: formatMultipartData', () => { + test('should format text field', () => { + const data = [{ name: 'description', type: 'text', value: 'dfv' }]; + const result = formatMultipartData(data, 'boundary'); + + expect(result).toContain('----boundary'); + expect(result).toContain('Content-Disposition: form-data'); + expect(result).toContain('name: description'); + expect(result).toContain('value: dfv'); + expect(result).toContain('----boundary--'); + }); + + test('should format file field', () => { + const data = [{ name: 'file', type: 'file', value: ['Dumy.xml'] }]; + const result = formatMultipartData(data, 'boundary'); + + expect(result).toContain('name: file'); + expect(result).toContain('value: [File: Dumy.xml]'); + }); + + test('should format multiple fields', () => { + const data = [ + { name: 'description', type: 'text', value: 'dfv' }, + { name: 'file', type: 'file', value: ['Dumy.xml'] } + ]; + const result = formatMultipartData(data, 'boundary'); + + expect(result).toContain('name: description'); + expect(result).toContain('value: dfv'); + expect(result).toContain('name: file'); + expect(result).toContain('value: [File: Dumy.xml]'); + }); + + test('should return empty string for invalid input', () => { + expect(formatMultipartData([], 'boundary')).toBe(''); + expect(formatMultipartData(null, 'boundary')).toBe(''); + }); + + test('should normalize boundary', () => { + const data = [{ name: 'field', type: 'text', value: 'value' }]; + expect(formatMultipartData(data, '--boundary')).toContain('----boundary'); + expect(formatMultipartData(data, 'boundary--')).toContain('----boundary'); + }); +});