From c1ca291b8c98181fb2292fdf923cdf3ed035ea1d Mon Sep 17 00:00:00 2001 From: Wahyu Akbar Wibowo Date: Tue, 8 Sep 2026 15:07:52 +0700 Subject: [PATCH 1/2] fix(curl): import form-data params from postman curl commands Postman quotes every --form value (name="john", file=@"/path"), and the form field parser mistook any '@' anywhere in the field for a file upload while emitting the file path as a string. jsonToBru only serializes a multipart file param when its value is an array, so those params were written as an empty @file() and lost on import. Detect a file only by a leading '@', strip the surrounding quotes, emit file paths as an array, and give each param a uid so the multipart body matches the shape the rest of the app expects. --- packages/bruno-app/src/utils/curl/index.js | 12 ++++++++-- .../bruno-app/src/utils/curl/index.spec.js | 12 ++++++++++ .../bruno-app/src/utils/curl/parse-curl.js | 24 +++++++++++++------ .../src/utils/curl/parse-curl.spec.js | 14 ++++++++++- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index 2170757bb10..a51b4055c18 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -1,6 +1,6 @@ import { forOwn } from 'lodash'; import curlToJson from './curl-to-json'; -import { prettifyJsonString } from 'utils/common/index'; +import { prettifyJsonString, uuid } from 'utils/common/index'; import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type'; export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { @@ -13,6 +13,14 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque return formData; }; + const parseMultipartForm = (parsedBody) => { + if (!Array.isArray(parsedBody)) { + return []; + } + + return parsedBody.map((param) => ({ uid: uuid(), ...param })); + }; + const parseGraphQL = (text) => { try { const graphql = JSON.parse(text); @@ -82,7 +90,7 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque body.formUrlEncoded = parseFormData(parsedBody); } else if (normalizedContentType.includes('multipart/form-data')) { body.mode = 'multipartForm'; - body.multipartForm = parsedBody; + body.multipartForm = parseMultipartForm(parsedBody); } else if (isPlainTextContentType(contentType)) { body.mode = 'text'; body.text = parsedBody; diff --git a/packages/bruno-app/src/utils/curl/index.spec.js b/packages/bruno-app/src/utils/curl/index.spec.js index 1a7623223c5..15dcc9f2c97 100644 --- a/packages/bruno-app/src/utils/curl/index.spec.js +++ b/packages/bruno-app/src/utils/curl/index.spec.js @@ -20,4 +20,16 @@ describe('getRequestFromCurlCommand', () => { expect(Array.isArray(request.body.file)).toBe(true); expect(request.body.file[0].filePath).toBe('/path/to/payload.json'); }); + + it('should map postman form-data into a multipart body', () => { + const curl = 'curl --location \'https://example.com/upload\' --form \'name="John"\' --form \'file=@"/path/to/file.txt"\''; + + const request = getRequestFromCurlCommand(curl); + + expect(request.body.mode).toBe('multipartForm'); + expect(request.body.multipartForm).toEqual([ + { uid: expect.any(String), name: 'name', value: 'John', type: 'text', enabled: true }, + { uid: expect.any(String), name: 'file', value: ['/path/to/file.txt'], type: 'file', enabled: true } + ]); + }); }); diff --git a/packages/bruno-app/src/utils/curl/parse-curl.js b/packages/bruno-app/src/utils/curl/parse-curl.js index 3dd81fa8153..04ebc8cbff9 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.js @@ -321,19 +321,29 @@ const setFormData = (request, value) => { /** * Parse a single form field * Handles text fields, quoted values, and file uploads (@path) + * Postman quotes every form value, eg: name="john" / file=@"/path/to/file.png" */ const parseFormField = (field) => { - const match = field.match(/^([^=]+)=(?:@?"([^"]*)"|@([^@]*)|([^@]*))?$/); + if (typeof field !== 'string') return null; - if (!match) return null; + const separatorIndex = field.indexOf('='); + if (separatorIndex < 0) return null; - const fieldName = match[1]; - const fieldValue = match[2] || match[3] || match[4] || ''; - const isFile = field.includes('@'); + const name = field.slice(0, separatorIndex); + let value = field.slice(separatorIndex + 1); + + // only a leading `@` marks a file upload, an `@` inside the value (eg an email) does not + const isFile = value.startsWith('@'); + if (isFile) { + value = value.slice(1); + } + + value = value.replace(/^"([\s\S]*)"$/, '$1'); return { - name: fieldName, - value: fieldValue, + name, + // multipart file params hold a list of file paths + value: isFile ? [value] : value, type: isFile ? 'file' : 'text', enabled: true }; diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js index 33d61242d1b..92be35b76c1 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.spec.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -410,12 +410,24 @@ describe('parseCurlCommand', () => { expect(result).toEqual({ method: 'post', multipartUploads: [ - { name: 'file', value: '/path/to/file.txt', type: 'file', enabled: true } + { name: 'file', value: ['/path/to/file.txt'], type: 'file', enabled: true } ], url: 'https://api.example.com/upload', urlWithoutQuery: 'https://api.example.com/upload' }); }); + + it('should parse the quoted form data postman generates', () => { + const result = parseCurlCommand( + 'curl --location \'https://api.example.com/upload\' --form \'name="John"\' --form \'email="john@example.com"\' --form \'file=@"/path/to/file.txt"\'' + ); + + expect(result.multipartUploads).toEqual([ + { name: 'name', value: 'John', type: 'text', enabled: true }, + { name: 'email', value: 'john@example.com', type: 'text', enabled: true }, + { name: 'file', value: ['/path/to/file.txt'], type: 'file', enabled: true } + ]); + }); }); describe('Cookie', () => { From ff7dd405fabd2133fb2b2f54ed7cbe41b3d33554 Mon Sep 17 00:00:00 2001 From: Wahyu Akbar Wibowo Date: Fri, 18 Sep 2026 08:48:06 +0700 Subject: [PATCH 2/2] test(curl): cover form fields without a name=value pair Guards against a parser change turning a malformed --form value into an empty multipart param. --- packages/bruno-app/src/utils/curl/parse-curl.spec.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/bruno-app/src/utils/curl/parse-curl.spec.js b/packages/bruno-app/src/utils/curl/parse-curl.spec.js index 92be35b76c1..a022c7d6c24 100644 --- a/packages/bruno-app/src/utils/curl/parse-curl.spec.js +++ b/packages/bruno-app/src/utils/curl/parse-curl.spec.js @@ -428,6 +428,14 @@ describe('parseCurlCommand', () => { { name: 'file', value: ['/path/to/file.txt'], type: 'file', enabled: true } ]); }); + + it('should skip form fields without a name=value pair', () => { + const result = parseCurlCommand(` + curl --form "invalid" https://api.example.com/upload + `); + + expect(result.multipartUploads).toEqual([]); + }); }); describe('Cookie', () => {