From 97c700beba4d064b181276efe3867d6141ac0341 Mon Sep 17 00:00:00 2001 From: lohit Date: Sun, 4 Jan 2026 21:27:07 +0530 Subject: [PATCH] fix: update logic for checking `formdata` instances (#6643) * fix: update logic for checking formdata instance * fix: isFormData logic update * fix: review comment fix, add isFormData to @usebruno/common package * fix: review comment fix --- package-lock.json | 1 + .../bruno-cli/src/runner/interpolate-vars.js | 4 +- .../src/runner/run-single-request.js | 5 +- packages/bruno-common/package.json | 1 + .../bruno-common/src/utils/form-data.spec.ts | 53 ++++++++++++++++++- packages/bruno-common/src/utils/form-data.ts | 12 +++++ packages/bruno-common/src/utils/index.ts | 3 +- .../bruno-electron/src/ipc/network/index.js | 5 +- .../src/ipc/network/interpolate-vars.js | 4 +- packages/bruno-electron/src/utils/common.js | 4 +- 10 files changed, 78 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d4cb7c65..9cbdd6759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33081,6 +33081,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "form-data": "^4.0.0", "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index b2f614e7e..9b1d64e4d 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -1,6 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep, find } = require('lodash'); -const FormData = require('form-data'); +const { isFormData } = require('@usebruno/common').utils; const getContentType = (headers = {}) => { let contentType = ''; @@ -87,7 +87,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc })); } } else if (contentType === 'multipart/form-data') { - if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { + if (Array.isArray(request?.data) && !isFormData(request.data)) { try { request.data = request?.data?.map((d) => ({ ...d, diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index e14570d13..4022152e8 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -3,7 +3,6 @@ const chalk = require('chalk'); const decomment = require('decomment'); const fs = require('fs'); const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash'); -const FormData = require('form-data'); const prepareRequest = require('./prepare-request'); const interpolateVars = require('./interpolate-vars'); const { interpolateString } = require('./interpolate-string'); @@ -25,7 +24,7 @@ const { NtlmClient } = require('axios-ntlm'); const { addDigestInterceptor } = require('@usebruno/requests'); const { getCACertificates } = require('@usebruno/requests'); const { getOAuth2Token } = require('../utils/oauth2'); -const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables } = require('@usebruno/common').utils; +const { encodeUrl, buildFormUrlEncodedPayload, extractPromptVariables, isFormData } = require('@usebruno/common').utils; const onConsoleLog = (type, args) => { console[type](...args); @@ -429,7 +428,7 @@ const runSingleRequest = async function ( } if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') { - if (!(request?.data instanceof FormData)) { + if (!isFormData(request?.data)) { request._originalMultipartData = request.data; request.collectionPath = collectionPath; let form = createFormData(request.data, collectionPath); diff --git a/packages/bruno-common/package.json b/packages/bruno-common/package.json index c989069df..5876bf9c9 100644 --- a/packages/bruno-common/package.json +++ b/packages/bruno-common/package.json @@ -46,6 +46,7 @@ "@rollup/plugin-typescript": "^12.1.2", "@types/jest": "^29.5.14", "babel-jest": "^29.7.0", + "form-data": "^4.0.0", "is-ip": "^5.0.1", "moment": "^2.29.4", "rollup": "3.29.5", diff --git a/packages/bruno-common/src/utils/form-data.spec.ts b/packages/bruno-common/src/utils/form-data.spec.ts index 060cc4bf5..76fe581c6 100644 --- a/packages/bruno-common/src/utils/form-data.spec.ts +++ b/packages/bruno-common/src/utils/form-data.spec.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from '@jest/globals'; -import { buildFormUrlEncodedPayload } from './form-data'; +import { buildFormUrlEncodedPayload, isFormData } from './form-data'; +import FormData from 'form-data'; describe('buildFormUrlEncodedPayload', () => { it('should handle single key-value pair', () => { @@ -110,3 +111,53 @@ describe('buildFormUrlEncodedPayload', () => { expect(result).toEqual(expected); }); }); + +describe('isFormData', () => { + it('should return true for objects with FormData constructor name', () => { + const mockFormData = { + constructor: { name: 'FormData' } + }; + expect(isFormData(mockFormData)).toBe(true); + }); + + it('should return false for null', () => { + expect(isFormData(null)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(isFormData(undefined)).toBe(false); + }); + + it('should return false for plain objects', () => { + expect(isFormData({})).toBe(false); + expect(isFormData({ key: 'value' })).toBe(false); + }); + + it('should return false for arrays', () => { + expect(isFormData([])).toBe(false); + expect(isFormData([1, 2, 3])).toBe(false); + }); + + it('should return false for primitives', () => { + expect(isFormData('string')).toBe(false); + expect(isFormData(123)).toBe(false); + expect(isFormData(true)).toBe(false); + }); + + it('should return false for objects with different constructor names', () => { + class CustomClass {} + const customObj = new CustomClass(); + expect(isFormData(customObj)).toBe(false); + }); + + it('should return false for objects without constructor', () => { + const obj = Object.create(null); + expect(isFormData(obj)).toBe(false); + }); + + it('should return true for actual FormData instance from form-data library', () => { + const formData = new FormData(); + formData.append('key', 'value'); + expect(isFormData(formData)).toBe(true); + }); +}); diff --git a/packages/bruno-common/src/utils/form-data.ts b/packages/bruno-common/src/utils/form-data.ts index 91516a52f..84cd7f333 100644 --- a/packages/bruno-common/src/utils/form-data.ts +++ b/packages/bruno-common/src/utils/form-data.ts @@ -31,3 +31,15 @@ export const buildFormUrlEncodedPayload = (params: Array<{ name: string; value: return resultParams.toString(); }; + +/** + * Determines if the given object is a FormData instance. + * Supports native FormData (Node 18+, browser) and the 'form-data' npm package. + * @param obj - Object to check. + * @returns True if obj is a FormData instance, false otherwise. + */ +export const isFormData = (obj: unknown): boolean => { + // Check constructor name (works for both native FormData and form-data npm package) + // todo: checking constructor.name can produce false positives for objects that have a constructor.name property set to 'FormData', but this is rare. + return obj?.constructor?.name === 'FormData'; +}; diff --git a/packages/bruno-common/src/utils/index.ts b/packages/bruno-common/src/utils/index.ts index 62a418fe0..3a62c2a08 100644 --- a/packages/bruno-common/src/utils/index.ts +++ b/packages/bruno-common/src/utils/index.ts @@ -5,7 +5,8 @@ export { } from './url'; export { - buildFormUrlEncodedPayload + buildFormUrlEncodedPayload, + isFormData } from './form-data'; export { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 30a356f3a..e17fe4ae9 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -5,7 +5,6 @@ const qs = require('qs'); const decomment = require('decomment'); const contentDispositionParser = require('content-disposition'); const mime = require('mime-types'); -const FormData = require('form-data'); const { ipcMain } = require('electron'); const { each, get, extend, cloneDeep, merge } = require('lodash'); const { NtlmClient } = require('axios-ntlm'); @@ -36,7 +35,7 @@ const { cookiesStore } = require('../../store/cookies'); const registerGrpcEventHandlers = require('./grpc-event-handlers'); const { registerWsEventHandlers } = require('./ws-event-handlers'); const { getCertsAndProxyConfig } = require('./cert-utils'); -const { buildFormUrlEncodedPayload } = require('@usebruno/common').utils; +const { buildFormUrlEncodedPayload, isFormData } = require('@usebruno/common').utils; const ERROR_OCCURRED_WHILE_EXECUTING_REQUEST = 'Error occurred while executing the request!'; @@ -474,7 +473,7 @@ const registerNetworkIpc = (mainWindow) => { } if (contentTypeHeader && request.headers[contentTypeHeader] === 'multipart/form-data') { - if (!(request.data instanceof FormData)) { + if (!isFormData(request.data)) { request._originalMultipartData = request.data; request.collectionPath = collectionPath; let form = createFormData(request.data, collectionPath); diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index f071b6194..c8ce4dfa0 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -1,6 +1,6 @@ const { interpolate } = require('@usebruno/common'); const { each, forOwn, cloneDeep } = require('lodash'); -const FormData = require('form-data'); +const { isFormData } = require('@usebruno/common').utils; const getContentType = (headers = {}) => { let contentType = ''; @@ -132,7 +132,7 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc })); } } else if (contentType === 'multipart/form-data') { - if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { + if (Array.isArray(request?.data) && !isFormData(request.data)) { try { request.data = request?.data?.map((d) => ({ ...d, diff --git a/packages/bruno-electron/src/utils/common.js b/packages/bruno-electron/src/utils/common.js index bb8c119b4..384410ea8 100644 --- a/packages/bruno-electron/src/utils/common.js +++ b/packages/bruno-electron/src/utils/common.js @@ -1,8 +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'); +const { isFormData } = require('@usebruno/common').utils; // a customized version of nanoid without using _ and - const uuid = () => { @@ -135,7 +135,7 @@ const parseDataFromRequest = (request) => { // 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)) { + } else if (isFormData(request?.data) && Array.isArray(request._originalMultipartData)) { const boundary = request.data._boundary || 'boundary'; requestDataString = formatMultipartData(request._originalMultipartData, boundary); } else {