From 57aedccc5f140142c0b01d550c57faff54f227aa Mon Sep 17 00:00:00 2001 From: Sarthak Sharma Date: Tue, 8 Sep 2026 00:30:32 +0530 Subject: [PATCH] fix: curl paste does not set form --- .../components/RequestPane/QueryUrl/index.js | 20 ++++--- .../bruno-app/src/utils/curl/curl-to-json.js | 6 ++- packages/bruno-app/src/utils/curl/index.js | 52 ++++++++++++++++++- .../bruno-app/src/utils/curl/index.spec.js | 15 ++++++ 4 files changed, 85 insertions(+), 8 deletions(-) diff --git a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js index 29147a22e90..ab714025d3c 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryUrl/index.js @@ -296,13 +296,21 @@ const QueryUrl = ({ item, collection, handleRun }) => { ); } } else if (bodyMode === 'formUrlEncoded' && request.body.formUrlEncoded) { - // For formUrlEncoded, we need to set each param individually - // This is a limitation - we'd need to clear existing params first - // For now, we'll set the body mode and the user can manually adjust - // TODO: Implement proper formUrlEncoded param setting + dispatch( + updateRequestBody({ + itemUid: item.uid, + collectionUid: collection.uid, + content: request.body.formUrlEncoded + }) + ); } else if (bodyMode === 'multipartForm' && request.body.multipartForm) { - // For multipartForm, similar limitation - // TODO: Implement proper multipartForm param setting + dispatch( + updateRequestBody({ + itemUid: item.uid, + collectionUid: collection.uid, + content: request.body.multipartForm + }) + ); } } diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.js b/packages/bruno-app/src/utils/curl/curl-to-json.js index aace9916a57..b8a278c3ecb 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -18,6 +18,10 @@ function getContentType(headers = {}) { return contentType ? headers[contentType] : null; } +function isMultipartFormDataContentType(contentType) { + return typeof contentType === 'string' && contentType.toLowerCase().includes('multipart/form-data'); +} + function repr(value, isKey) { return isKey ? '\'' + jsesc(value, { quotes: 'single' }) + '\'' : value; } @@ -35,7 +39,7 @@ function getDataString(request) { const contentType = getContentType(request.headers); - if (isStructuredContentType(contentType)) { + if (isStructuredContentType(contentType) || isMultipartFormDataContentType(contentType)) { return { data: request.data }; } diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index 2170757bb10..d2289754e1b 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -4,6 +4,54 @@ import { prettifyJsonString } from 'utils/common/index'; import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type'; export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { + const getMultipartBoundary = (contentType) => { + const boundaryMatch = contentType?.match(/(?:^|;)\s*boundary=(?:"([^"]+)"|([^;]+))/i); + return boundaryMatch ? boundaryMatch[1] || boundaryMatch[2]?.trim() : null; + }; + + const normalizeMultipartLineEndings = (value) => { + return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + }; + + const parseContentDispositionName = (headersText) => { + const contentDisposition = headersText + .split('\n') + .find((header) => header.toLowerCase().startsWith('content-disposition:')); + const nameMatch = contentDisposition?.match(/(?:^|;)\s*name="([^"]*)"/i); + return nameMatch ? nameMatch[1] : null; + }; + + const parseMultipartFormData = (bodyText, contentType) => { + const boundary = getMultipartBoundary(contentType); + if (!boundary || typeof bodyText !== 'string') { + return []; + } + + const normalizedBody = normalizeMultipartLineEndings(bodyText); + return normalizedBody + .split(`--${boundary}`) + .map((part) => part.replace(/^\n/, '').replace(/\n$/, '')) + .filter((part) => part && part !== '--') + .map((part) => { + const separatorIndex = part.indexOf('\n\n'); + const headersText = separatorIndex >= 0 ? part.slice(0, separatorIndex) : ''; + const value = separatorIndex >= 0 ? part.slice(separatorIndex + 2).replace(/\n--$/, '') : ''; + const name = parseContentDispositionName(headersText); + + if (name === null) { + return null; + } + + return { + name, + value, + type: 'text', + enabled: true + }; + }) + .filter(Boolean); + }; + const parseFormData = (parsedBody) => { const formData = []; forOwn(parsedBody, (value, key) => { @@ -82,7 +130,9 @@ 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 = Array.isArray(parsedBody) + ? parsedBody + : parseMultipartFormData(parsedBody, contentType); } 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..a2227e5ef78 100644 --- a/packages/bruno-app/src/utils/curl/index.spec.js +++ b/packages/bruno-app/src/utils/curl/index.spec.js @@ -20,4 +20,19 @@ describe('getRequestFromCurlCommand', () => { expect(Array.isArray(request.body.file)).toBe(true); expect(request.body.file[0].filePath).toBe('/path/to/payload.json'); }); + + it('should parse raw multipart form data from --data-raw', () => { + const curl = `curl --url 'https://example.com/apply' \ + -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryTest' \ + --data-raw $'------WebKitFormBoundaryTest\\r\\nContent-Disposition: form-data; name="first_name"\\r\\n\\r\\nAda\\r\\n------WebKitFormBoundaryTest\\r\\nContent-Disposition: form-data; name="response"\\r\\n\\r\\n[{"answer":"yes"}]\\r\\n------WebKitFormBoundaryTest\\r\\nContent-Disposition: form-data; name=""\\r\\n\\r\\n30\\r\\n------WebKitFormBoundaryTest--\\r\\n'`; + + const request = getRequestFromCurlCommand(curl); + + expect(request.body.mode).toBe('multipartForm'); + expect(request.body.multipartForm).toEqual([ + { name: 'first_name', value: 'Ada', type: 'text', enabled: true }, + { name: 'response', value: '[{"answer":"yes"}]', type: 'text', enabled: true }, + { name: '', value: '30', type: 'text', enabled: true } + ]); + }); });