fix: improve error handling modal for postman import

This commit is contained in:
naman-bruno
2026-04-06 18:12:10 +05:30
parent fabba4d296
commit 43fdf00ed0
6 changed files with 623 additions and 406 deletions

View File

@@ -0,0 +1,86 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.import-error-content {
display: flex;
flex-direction: column;
gap: 14px;
}
.error-banner {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
padding-left: 0;
border-radius: 6px;
background-color: ${(props) => props.theme.colors.danger}11;
border: 1px solid ${(props) => props.theme.colors.danger}33;
.error-icon {
color: ${(props) => props.theme.colors.danger};
flex-shrink: 0;
margin-top: 2px;
}
.error-message {
color: ${(props) => props.theme.text};
font-size: 14px;
font-weight: 500;
line-height: 1.4;
word-break: break-word;
}
}
.error-raw {
pre {
margin: 0;
padding: 10px 12px;
border-radius: 6px;
background-color: ${(props) => props.theme.input.bg};
border: 1px solid ${(props) => props.theme.input.border};
font-size: 12px;
line-height: 1.5;
color: ${(props) => props.theme.text};
opacity: 0.8;
white-space: pre-wrap;
word-break: break-word;
max-height: 140px;
overflow-y: auto;
font-family: monospace;
}
}
.error-hint {
font-size: 12px;
color: ${(props) => props.theme.text};
opacity: 0.5;
line-height: 1.4;
}
.error-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
.action-button {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
background-color: ${(props) => props.theme.input.bg};
border: 1px solid ${(props) => props.theme.input.border};
color: ${(props) => props.theme.text};
transition: background-color 0.15s ease;
&:hover {
background-color: ${(props) => props.theme.input.border};
}
}
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,97 @@
import React, { useState, useCallback } from 'react';
import { IconAlertTriangle, IconCopy, IconCheck, IconBrandGithub } from '@tabler/icons';
import Portal from 'components/Portal';
import Modal from 'components/Modal';
import StyledWrapper from './StyledWrapper';
const GITHUB_ISSUES_URL = 'https://github.com/usebruno/bruno/issues/new';
const ImportErrorModal = ({ title = 'Import Failed', error, onClose }) => {
const [copied, setCopied] = useState(false);
const errorMessage = error?.message || 'An unknown error occurred during import.';
const rawError = error?.rawError || null;
const copyErrorToClipboard = useCallback(() => {
navigator.clipboard.writeText(rawError || errorMessage).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
});
}, [errorMessage, rawError]);
const reportOnGithub = useCallback(() => {
const body = [
'### Description',
'Postman collection import failed with the following error:',
'',
'### Error Details',
'```',
rawError || errorMessage,
'```',
'',
'### Additional Context',
'<!-- Attach your Postman collection JSON (with sensitive data redacted) if possible -->'
].join('\n');
const params = new URLSearchParams({
title: `Postman import failure: ${errorMessage.slice(0, 80)}`,
body,
labels: 'bug'
});
window.open(`${GITHUB_ISSUES_URL}?${params.toString()}`, '_blank');
}, [errorMessage, rawError]);
return (
<StyledWrapper>
<Portal>
<Modal
size="md"
title={title}
hideFooter={true}
handleCancel={onClose}
dataTestId="import-error-modal"
>
<div className="import-error-content">
<div className="error-banner">
<IconAlertTriangle size={20} className="error-icon" />
<div className="error-message">{errorMessage}</div>
</div>
{rawError && (
<div className="error-raw">
<pre>{rawError}</pre>
</div>
)}
<p className="error-hint">
Ensure your Postman collection is valid JSON and uses a supported schema (v2.0 or v2.1).
</p>
<div className="error-actions">
<button className="action-button" onClick={reportOnGithub}>
<IconBrandGithub size={14} />
<span>Report on GitHub</span>
</button>
<button className="action-button" onClick={copyErrorToClipboard}>
{copied ? (
<>
<IconCheck size={14} />
<span>Copied</span>
</>
) : (
<>
<IconCopy size={14} />
<span>Copy Error</span>
</>
)}
</button>
</div>
</div>
</Modal>
</Portal>
</StyledWrapper>
);
};
export default ImportErrorModal;

View File

@@ -25,11 +25,12 @@ const convertFileToObject = async (file) => {
const parsed = jsyaml.load(text);
if (typeof parsed !== 'object' || parsed === null) {
throw new Error();
throw new Error('File content is not a valid object');
}
return parsed;
} catch {
throw new Error('Failed to parse the file ensure it is valid JSON or YAML');
} catch (err) {
const detail = err.message || '';
throw new Error(`Failed to parse the file: ensure it is valid JSON or YAML.\n${detail}`);
}
};

View File

@@ -12,9 +12,9 @@ import { convertOpenapiToBruno } from 'utils/importers/openapi-collection';
import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { processOpenCollection } from 'utils/importers/opencollection';
import { wsdlToBruno } from '@usebruno/converters';
import { toastError } from 'utils/common/error';
import { useBetaFeature, BETA_FEATURES } from 'utils/beta-features';
import Modal from 'components/Modal';
import ImportErrorModal from 'components/ImportErrorModal';
import Help from 'components/Help';
import Dropdown from 'components/Dropdown';
import StyledWrapper from './StyledWrapper';
@@ -87,7 +87,6 @@ const convertCollection = async (format, rawData, groupingType, collectionFormat
return collection;
} catch (err) {
console.error('Conversion error:', err);
toastError(err, 'Failed to convert collection');
throw err;
}
};
@@ -102,6 +101,7 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
const dispatch = useDispatch();
const [groupingType, setGroupingType] = useState('tags');
const [collectionFormat, setCollectionFormat] = useState(DEFAULT_COLLECTION_FORMAT);
const [importError, setImportError] = useState(null);
const isOpenAPISyncEnabled = useBetaFeature(BETA_FEATURES.OPENAPI_SYNC);
const [enableCheckForSpecUpdates, setEnableCheckForSpecUpdates] = useState(isOpenAPISyncEnabled);
const dropdownTippyRef = useRef();
@@ -135,7 +135,17 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
.required('Location is required')
}),
onSubmit: async (values) => {
const convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat);
let convertedCollection;
try {
convertedCollection = await convertCollection(format, rawData, groupingType, collectionFormat);
} catch (err) {
setImportError({
message: err.message || 'Failed to convert collection',
rawError: err.rawError || err.message || null
});
return;
}
const options = { format: collectionFormat };
if (showCheckForSpecUpdatesOption && enableCheckForSpecUpdates) {
@@ -345,6 +355,13 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, rawData, format, sour
)}
</form>
</Modal>
{importError && (
<ImportErrorModal
title="Import Failed"
error={importError}
onClose={() => setImportError(null)}
/>
)}
</StyledWrapper>
);
};

View File

@@ -1,5 +1,4 @@
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
import { BrunoError, formatIpcError } from 'utils/common/error';
import { safeParseJSON } from 'utils/common/index';
const readFile = (files) => {
@@ -11,13 +10,22 @@ const readFile = (files) => {
});
};
const parseConversionError = (rawMessage) => {
const full = rawMessage || 'Conversion failed';
const summary = full.split('\n')[0].replace(/^Import collection failed:\s*/, '').trim() || full;
return summary;
};
const postmanToBruno = (collection) => {
return new Promise((resolve, reject) => {
window.ipcRenderer.invoke('renderer:convert-postman-to-bruno', collection)
.then((result) => resolve(result))
.catch((err) => {
console.error('Error converting Postman to Bruno via Electron:', err);
reject(new BrunoError('Conversion failed'));
const rawError = formatIpcError(err) || err.message || 'Conversion failed';
const error = new BrunoError(parseConversionError(rawError));
error.rawError = rawError;
reject(error);
});
});
};

View File

@@ -363,34 +363,103 @@ export const processAuth = (auth, requestObject, isCollection = false) => {
}
};
const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap) => {
const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false } = {}, scriptMap, parentPath = '') => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
item.forEach((i, index) => {
if (isItemAFolder(i)) {
const baseFolderName = i.name || 'Untitled Folder';
let folderName = baseFolderName;
let count = 1;
const itemName = (i && i.name) || `Item ${index + 1}`;
const itemPath = parentPath ? `${parentPath} / ${itemName}` : itemName;
while (folderMap[folderName]) {
folderName = `${baseFolderName}_${count}`;
count++;
}
try {
if (isItemAFolder(i)) {
const baseFolderName = i.name || 'Untitled Folder';
let folderName = baseFolderName;
let count = 1;
const brunoFolderItem = {
uid: uuid(),
name: folderName,
type: 'folder',
items: [],
seq: index + 1,
root: {
docs: transformDescription(i.description),
meta: {
name: folderName
},
while (folderMap[folderName]) {
folderName = `${baseFolderName}_${count}`;
count++;
}
const brunoFolderItem = {
uid: uuid(),
name: folderName,
type: 'folder',
items: [],
seq: index + 1,
root: {
docs: transformDescription(i.description),
meta: {
name: folderName
},
request: {
auth: {
mode: 'inherit',
basic: null,
bearer: null,
awsv4: null,
apikey: null,
oauth1: null,
oauth2: null,
digest: null
},
headers: [],
script: {},
tests: '',
vars: {}
}
}
};
brunoParent.items.push(brunoFolderItem);
// Folder level auth
processAuth(i.auth, brunoFolderItem.root.request);
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap, itemPath);
}
if (i.event) {
if (useWorkers) {
scriptMap.set(brunoFolderItem.uid, {
events: i.event,
request: brunoFolderItem.root.request
});
} else {
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
}
}
folderMap[folderName] = brunoFolderItem;
} else if (i.request) {
const method = i?.request?.method?.toUpperCase();
if (!method || typeof method !== 'string' || !method.trim()) {
console.warn('Missing or invalid request.method', method);
return;
}
const baseRequestName = i.name || 'Untitled Request';
let requestName = baseRequestName;
let count = 1;
while (requestMap[requestName]) {
requestName = `${baseRequestName}_${count}`;
count++;
}
const url = constructUrl(i.request.url);
const brunoRequestItem = {
uid: uuid(),
name: requestName,
type: 'http-request',
seq: index + 1,
request: {
url: url,
method: method,
auth: {
mode: 'inherit',
basic: null,
@@ -402,406 +471,345 @@ const importPostmanV2CollectionItem = (brunoParent, item, { useWorkers = false }
digest: null
},
headers: [],
script: {},
tests: '',
vars: {}
}
}
};
brunoParent.items.push(brunoFolderItem);
// Folder level auth
processAuth(i.auth, brunoFolderItem.root.request);
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item, { useWorkers }, scriptMap);
}
if (i.event) {
if (useWorkers) {
scriptMap.set(brunoFolderItem.uid, {
events: i.event,
request: brunoFolderItem.root.request
});
} else {
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
}
}
folderMap[folderName] = brunoFolderItem;
} else if (i.request) {
const method = i?.request?.method?.toUpperCase();
if (!method || typeof method !== 'string' || !method.trim()) {
console.warn('Missing or invalid request.method', method);
return;
}
const baseRequestName = i.name || 'Untitled Request';
let requestName = baseRequestName;
let count = 1;
while (requestMap[requestName]) {
requestName = `${baseRequestName}_${count}`;
count++;
}
const url = constructUrl(i.request.url);
const brunoRequestItem = {
uid: uuid(),
name: requestName,
type: 'http-request',
seq: index + 1,
request: {
url: url,
method: method,
auth: {
mode: 'inherit',
basic: null,
bearer: null,
awsv4: null,
apikey: null,
oauth1: null,
oauth2: null,
digest: null
},
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
},
docs: transformDescription(i.request.description)
}
};
const settings = {
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
};
// Handle followRedirects setting
if (i.protocolProfileBehavior?.followRedirects !== undefined) {
settings.followRedirects = i.protocolProfileBehavior.followRedirects;
}
// Handle maxRedirects setting
if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
}
brunoRequestItem.settings = settings;
brunoParent.items.push(brunoRequestItem);
if (i.event) {
if (useWorkers) {
scriptMap.set(brunoRequestItem.uid, {
events: i.event,
request: brunoRequestItem.request
});
} else {
i.event.forEach((event) => {
if (event.listen === 'prerequest' && event.script && event.script.exec) {
if (!brunoRequestItem.request?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.req = '';
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
if (event.listen === 'test' && event.script && event.script.exec) {
if (!brunoRequestItem.request?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.res = '';
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
});
}
}
const bodyMode = get(i, 'request.body.mode');
if (bodyMode) {
if (bodyMode === 'formdata') {
brunoRequestItem.request.body.mode = 'multipartForm';
each(i.request.body.formdata, (param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
if (bodyMode === 'urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(i.request.body.urlencoded, (param) => {
if (param.key == null && param.value == null) return;
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
if (bodyMode === 'raw') {
let language = get(i, 'request.body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(i.request.header);
}
if (language === 'json') {
brunoRequestItem.request.body.mode = 'json';
brunoRequestItem.request.body.json = i.request.body.raw;
} else if (language === 'xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = i.request.body.raw;
} else {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = i.request.body.raw;
}
}
}
if (bodyMode === 'graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(normalizeHeaders(i.request.header), (header) => {
if (header.key == null && header.value == null) return;
brunoRequestItem.request.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
// Request-level auth
processAuth(i.request.auth, brunoRequestItem.request);
each(get(i, 'request.url.query'), (param) => {
if (param.key == null && param.value == null) {
return;
}
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
each(get(i, 'request.url.variable', []), (param) => {
if (!param.key) {
// If no key, skip this iteration and discard the param
return;
}
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
// Handle Postman examples (responses)
if (i.response && Array.isArray(i.response)) {
brunoRequestItem.examples = [];
i.response.forEach((response, responseIndex) => {
const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
const exampleName = sanitized || `Example ${responseIndex + 1}`;
// Convert originalRequest to Bruno request format
const originalRequest = response.originalRequest || {};
const exampleUrl = constructUrl(originalRequest.url);
const exampleMethod = originalRequest.method?.toUpperCase() || method;
const example = {
uid: uuid(),
itemUid: brunoRequestItem.uid,
name: exampleName,
description: '',
type: 'http-request',
request: {
url: exampleUrl,
method: exampleMethod,
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
}
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
},
response: {
status: response.code || null,
statusText: response.status || '',
headers: [],
body: {
type: getBodyTypeFromContentTypeHeader(response.header),
content: response.body || ''
}
}
};
docs: transformDescription(i.request.description)
}
};
// Convert original request headers
if (originalRequest.header) {
normalizeHeaders(originalRequest.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.request.headers.push({
const settings = {
encodeUrl: i.protocolProfileBehavior?.disableUrlEncoding !== true
};
// Handle followRedirects setting
if (i.protocolProfileBehavior?.followRedirects !== undefined) {
settings.followRedirects = i.protocolProfileBehavior.followRedirects;
}
// Handle maxRedirects setting
if (i.protocolProfileBehavior?.maxRedirects !== undefined) {
settings.maxRedirects = i.protocolProfileBehavior.maxRedirects;
}
brunoRequestItem.settings = settings;
brunoParent.items.push(brunoRequestItem);
if (i.event) {
if (useWorkers) {
scriptMap.set(brunoRequestItem.uid, {
events: i.event,
request: brunoRequestItem.request
});
} else {
i.event.forEach((event) => {
if (event.listen === 'prerequest' && event.script && event.script.exec) {
if (!brunoRequestItem.request?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.req = '';
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
if (event.listen === 'test' && event.script && event.script.exec) {
if (!brunoRequestItem.request?.script) {
brunoRequestItem.request.script = {};
}
if (event.script.exec && event.script.exec.length > 0) {
brunoRequestItem.request.script.res = postmanTranslation(event.script.exec);
} else {
brunoRequestItem.request.script.res = '';
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
}
});
}
}
const bodyMode = get(i, 'request.body.mode');
if (bodyMode) {
if (bodyMode === 'formdata') {
brunoRequestItem.request.body.mode = 'multipartForm';
each(i.request.body.formdata, (param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
brunoRequestItem.request.body.multipartForm.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
// Convert original request query parameters
if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
originalRequest.url.query.forEach((param) => {
if (param.key == null && param.value == null) {
return;
}
example.request.params.push({
if (bodyMode === 'urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(i.request.body.urlencoded, (param) => {
if (param.key == null && param.value == null) return;
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
}
if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
originalRequest.url.variable.forEach((param) => {
if (!param.key) return;
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
// Convert original request body
if (originalRequest.body) {
const bodyMode = originalRequest.body.mode;
if (bodyMode === 'formdata') {
example.request.body.mode = 'multipartForm';
if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
originalRequest.body.formdata.forEach((param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
example.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
} else if (bodyMode === 'urlencoded') {
example.request.body.mode = 'formUrlEncoded';
if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
originalRequest.body.urlencoded.forEach((param) => {
if (param.key == null && param.value == null) return;
example.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
} else if (bodyMode === 'raw') {
let language = get(originalRequest, 'body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(originalRequest.header || []);
}
if (language === 'json') {
example.request.body.mode = 'json';
example.request.body.json = originalRequest.body.raw;
} else if (language === 'xml') {
example.request.body.mode = 'xml';
example.request.body.xml = originalRequest.body.raw;
} else {
example.request.body.mode = 'text';
example.request.body.text = originalRequest.body.raw;
}
if (bodyMode === 'raw') {
let language = get(i, 'request.body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(i.request.header);
}
if (language === 'json') {
brunoRequestItem.request.body.mode = 'json';
brunoRequestItem.request.body.json = i.request.body.raw;
} else if (language === 'xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = i.request.body.raw;
} else {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = i.request.body.raw;
}
}
}
// Convert response headers
if (response.header) {
normalizeHeaders(response.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.response.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: true
});
});
if (bodyMode === 'graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql);
}
each(normalizeHeaders(i.request.header), (header) => {
if (header.key == null && header.value == null) return;
brunoRequestItem.request.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
// Request-level auth
processAuth(i.request.auth, brunoRequestItem.request);
each(get(i, 'request.url.query'), (param) => {
if (param.key == null && param.value == null) {
return;
}
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
each(get(i, 'request.url.variable', []), (param) => {
if (!param.key) {
// If no key, skip this iteration and discard the param
return;
}
brunoRequestItem.examples.push(example);
brunoRequestItem.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
requestMap[requestName] = brunoRequestItem;
// Handle Postman examples (responses)
if (i.response && Array.isArray(i.response)) {
brunoRequestItem.examples = [];
i.response.forEach((response, responseIndex) => {
const sanitized = String(response.name ?? '').replace(/\r?\n/g, ' ').trim();
const exampleName = sanitized || `Example ${responseIndex + 1}`;
// Convert originalRequest to Bruno request format
const originalRequest = response.originalRequest || {};
const exampleUrl = constructUrl(originalRequest.url);
const exampleMethod = originalRequest.method?.toUpperCase() || method;
const example = {
uid: uuid(),
itemUid: brunoRequestItem.uid,
name: exampleName,
description: '',
type: 'http-request',
request: {
url: exampleUrl,
method: exampleMethod,
headers: [],
params: [],
body: {
mode: 'none',
json: null,
text: null,
xml: null,
formUrlEncoded: [],
multipartForm: []
}
},
response: {
status: response.code || null,
statusText: response.status || '',
headers: [],
body: {
type: getBodyTypeFromContentTypeHeader(response.header),
content: response.body || ''
}
}
};
// Convert original request headers
if (originalRequest.header) {
normalizeHeaders(originalRequest.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.request.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: !header.disabled
});
});
}
// Convert original request query parameters
if (originalRequest.url && originalRequest.url.query && Array.isArray(originalRequest.url.query)) {
originalRequest.url.query.forEach((param) => {
if (param.key == null && param.value == null) {
return;
}
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'query',
enabled: !param.disabled
});
});
}
if (originalRequest.url && originalRequest.url.variable && Array.isArray(originalRequest.url.variable)) {
originalRequest.url.variable.forEach((param) => {
if (!param.key) return;
example.request.params.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
type: 'path',
enabled: true
});
});
}
// Convert original request body
if (originalRequest.body) {
const bodyMode = originalRequest.body.mode;
if (bodyMode === 'formdata') {
example.request.body.mode = 'multipartForm';
if (originalRequest.body.formdata && Array.isArray(originalRequest.body.formdata)) {
originalRequest.body.formdata.forEach((param) => {
if (param.key == null && param.value == null) return;
const isFile = param.type === 'file' || (param.type === 'default' && param.src);
const value = isFile
? (Array.isArray(param.src) ? param.src : param.src ? [param.src] : [])
: (Array.isArray(param.value) ? param.value.join('') : ensureString(param.value));
example.request.body.multipartForm.push({
uid: uuid(),
type: isFile ? 'file' : 'text',
name: ensureString(param.key),
value,
description: transformDescription(param.description),
enabled: !param.disabled,
...(param.contentType && { contentType: param.contentType })
});
});
}
} else if (bodyMode === 'urlencoded') {
example.request.body.mode = 'formUrlEncoded';
if (originalRequest.body.urlencoded && Array.isArray(originalRequest.body.urlencoded)) {
originalRequest.body.urlencoded.forEach((param) => {
if (param.key == null && param.value == null) return;
example.request.body.formUrlEncoded.push({
uid: uuid(),
name: ensureString(param.key),
value: ensureString(param.value),
description: transformDescription(param.description),
enabled: !param.disabled
});
});
}
} else if (bodyMode === 'raw') {
let language = get(originalRequest, 'body.options.raw.language');
if (!language) {
language = searchLanguageByHeader(originalRequest.header || []);
}
if (language === 'json') {
example.request.body.mode = 'json';
example.request.body.json = originalRequest.body.raw;
} else if (language === 'xml') {
example.request.body.mode = 'xml';
example.request.body.xml = originalRequest.body.raw;
} else {
example.request.body.mode = 'text';
example.request.body.text = originalRequest.body.raw;
}
}
}
// Convert response headers
if (response.header) {
normalizeHeaders(response.header).forEach((header) => {
if (header.key == null && header.value == null) return;
example.response.headers.push({
uid: uuid(),
name: ensureString(header.key),
value: ensureString(header.value),
description: transformDescription(header.description),
enabled: true
});
});
}
brunoRequestItem.examples.push(example);
});
}
requestMap[requestName] = brunoRequestItem;
}
} catch (err) {
const contextMsg = `Error processing item "${itemName}" at path: ${itemPath}`;
throw new Error(`${contextMsg}\n Reason: ${err.message}`);
}
});
};