From 5784b041295d2c99424631dd6ad9ea8838b21d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dawid=20G=C3=B3ra?= Date: Thu, 13 Nov 2025 08:36:36 +0100 Subject: [PATCH] bugfix(#5939): curl import fails for custom content-types --- .../bruno-app/src/utils/curl/content-type.js | 29 ++++++++++++++++ .../bruno-app/src/utils/curl/curl-to-json.js | 3 +- .../src/utils/curl/curl-to-json.spec.js | 33 +++++++++++++++++++ packages/bruno-app/src/utils/curl/index.js | 15 +++++---- 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 packages/bruno-app/src/utils/curl/content-type.js diff --git a/packages/bruno-app/src/utils/curl/content-type.js b/packages/bruno-app/src/utils/curl/content-type.js new file mode 100644 index 000000000..0a1e54610 --- /dev/null +++ b/packages/bruno-app/src/utils/curl/content-type.js @@ -0,0 +1,29 @@ +const normalizeContentType = (contentType) => { + if (!contentType || typeof contentType !== 'string') { + return ''; + } + + return contentType.toLowerCase(); +}; + +export const isJsonLikeContentType = (contentType) => { + const normalized = normalizeContentType(contentType); + + return normalized.includes('application/json') || normalized.includes('+json'); +}; + +export const isXmlLikeContentType = (contentType) => { + const normalized = normalizeContentType(contentType); + + return normalized.includes('application/xml') || normalized.includes('+xml') || normalized.includes('text/xml'); +}; + +export const isPlainTextContentType = (contentType) => { + const normalized = normalizeContentType(contentType); + + return normalized.includes('text/plain'); +}; + +export const isStructuredContentType = (contentType) => { + return isJsonLikeContentType(contentType) || isXmlLikeContentType(contentType) || isPlainTextContentType(contentType); +}; 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 21daf8283..24269f9a9 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.js @@ -10,6 +10,7 @@ import parseCurlCommand from './parse-curl'; import * as querystring from 'query-string'; import * as jsesc from 'jsesc'; import { buildQueryString } from '@usebruno/common/utils'; +import { isStructuredContentType } from './content-type'; function getContentType(headers = {}) { const contentType = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type'); @@ -34,7 +35,7 @@ function getDataString(request) { const contentType = getContentType(request.headers); - if (contentType && (contentType.includes('application/json') || contentType.includes('application/xml') || contentType.includes('text/plain'))) { + if (isStructuredContentType(contentType)) { return { data: request.data }; } diff --git a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js index 058064391..c4133a3e0 100644 --- a/packages/bruno-app/src/utils/curl/curl-to-json.spec.js +++ b/packages/bruno-app/src/utils/curl/curl-to-json.spec.js @@ -120,4 +120,37 @@ describe('curlToJson', () => { ] }); }); + + it('should parse custom json content-types', () => { + const curlCommand = `curl 'https://api.example.com/test' + -H 'content-type: application/x.custom+json;version=1' + --data-raw '{"test":"data"}' + `; + + const result = curlToJson(curlCommand); + + expect(result).toEqual({ + url: 'https://api.example.com/test', + raw_url: 'https://api.example.com/test', + method: 'post', + headers: { + 'content-type': 'application/x.custom+json;version=1' + }, + data: '{"test":"data"}' + }); + }); + + it('should parse vendor tree json content-types', () => { + const curlCommand = `curl --request POST \\ + --url https://api.example.com/orders/42/preferences \\ + --header 'accept: */*' \\ + --header 'content-type: application/vnd.vendor+json' \\ + --data '{\\n "data": {\\n "type": "order-preferences",\\n "attributes": {\\n "notes": "Leave at door",\\n "priority": true\\n }\\n }\\n}'`; + + const result = curlToJson(curlCommand); + expect(result.data).toContain('"type": "order-preferences"'); + expect(result.data).toContain('"notes": "Leave at door"'); + expect(result.data).toContain('"priority": true'); + expect(result.headers['content-type']).toBe('application/vnd.vendor+json'); + }); }); diff --git a/packages/bruno-app/src/utils/curl/index.js b/packages/bruno-app/src/utils/curl/index.js index 3fa30a95f..866df7b32 100644 --- a/packages/bruno-app/src/utils/curl/index.js +++ b/packages/bruno-app/src/utils/curl/index.js @@ -1,6 +1,7 @@ import { forOwn } from 'lodash'; import curlToJson from './curl-to-json'; import { prettifyJsonString } from 'utils/common/index'; +import { isJsonLikeContentType, isPlainTextContentType, isXmlLikeContentType } from './content-type'; export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-request') => { const parseFormData = (parsedBody) => { @@ -59,25 +60,27 @@ export const getRequestFromCurlCommand = (curlCommand, requestType = 'http-reque }; if (parsedBody && contentType && typeof contentType === 'string') { - if (requestType === 'graphql-request' && (contentType.includes('application/json') || contentType.includes('application/graphql'))) { + const normalizedContentType = contentType.toLowerCase(); + + if (requestType === 'graphql-request' && (isJsonLikeContentType(contentType) || normalizedContentType.includes('application/graphql'))) { body.mode = 'graphql'; body.graphql = parseGraphQL(parsedBody); } else if (requestType === 'http-request' && request.isDataBinary) { body.mode = 'file'; body.file = parsedBody; - }else if (contentType.includes('application/json')) { + } else if (isJsonLikeContentType(contentType)) { body.mode = 'json'; body.json = prettifyJsonString(parsedBody); - } else if (contentType.includes('xml')) { + } else if (isXmlLikeContentType(contentType) || normalizedContentType.includes('xml')) { body.mode = 'xml'; body.xml = parsedBody; - } else if (contentType.includes('application/x-www-form-urlencoded')) { + } else if (normalizedContentType.includes('application/x-www-form-urlencoded')) { body.mode = 'formUrlEncoded'; body.formUrlEncoded = parseFormData(parsedBody); - } else if (contentType.includes('multipart/form-data')) { + } else if (normalizedContentType.includes('multipart/form-data')) { body.mode = 'multipartForm'; body.multipartForm = parsedBody; - } else if (contentType.includes('text/plain')) { + } else if (isPlainTextContentType(contentType)) { body.mode = 'text'; body.text = parsedBody; }