Added new feature supports multiple request body#7133
Added new feature supports multiple request body#7133GokulMithran0302 wants to merge 2 commits intousebruno:mainfrom
Conversation
WalkthroughThis PR implements multi-body-variant support for HTTP requests. It adds a UI selector component, Redux state management for variant operations (add, switch, rename, delete), schema extensions, and serialization logic across file formats (Bru, YAML) and language parsers. Changes
Sequence DiagramsequenceDiagram
actor User
participant UI as BodyVariantSelector
participant Redux as Redux Store
participant FileSystem as File System
participant Parser as Format Parser
User->>UI: Click "Add Variant"
UI->>Redux: dispatch addBodyVariant()
Redux->>Redux: Clone current body<br/>Create new variant with uid
Redux-->>UI: Update variant list
UI->>User: Show new variant in dropdown
User->>UI: Select different variant
UI->>Redux: dispatch switchBodyVariant(uid)
Redux->>Redux: Save current body to<br/>active variant
Redux->>Redux: Load selected variant body
Redux-->>UI: Update active variant
UI->>User: Display variant body
User->>FileSystem: Save request
FileSystem->>Redux: Get request state
FileSystem->>FileSystem: Sync active variant body<br/>into bodyVariants
FileSystem->>Parser: Transform to file format
Parser->>Parser: Serialize bodyVariants<br/>+ activeBodyVariantUid
FileSystem->>User: Persist to disk
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In
`@packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/index.js`:
- Around line 112-123: The delete button is only rendered when
bodyVariants.length > 2, preventing removal when exactly two variants exist;
change the condition in the BodyVariantSelector rendering from
bodyVariants.length > 2 to bodyVariants.length > 1 so the trash IconTrash
(inside the span with className "variant-action-btn") is shown when there are at
least two variants, leaving the onDeleteVariant(variant.uid) handler and reducer
behavior intact to collapse variants when ≤1 remain.
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js`:
- Around line 1745-1782: The addBodyVariant reducer currently clones
request.body into the new variant (in addBodyVariant) but per spec the new
variant should be blank; change the new variant creation to initialize body with
an empty/default structure instead of cloneDeep(request.body) (refer to
addBodyVariant and request.bodyVariants/request.activeBodyVariantUid). Also
ensure edits to the live request.body are persisted into the active variant when
the request is saved—update saveRequest (or call from updateRequestBody) to
locate the active variant by request.activeBodyVariantUid and copy current
request.body into that variant's body before finalizing save (see
updateRequestBody, switchBodyVariant and saveRequest for where to hook this
sync).
In `@packages/bruno-app/src/utils/collections/index.js`:
- Around line 760-771: The current code updates body variants by shallow-copying
itemToSave.request.body with object spread, which can leave nested objects
shared; change the assignment inside the variants map to use
cloneDeep(itemToSave.request.body) for the variant whose uid equals
_item.request.activeBodyVariantUid, ensuring itemToSave.request.bodyVariants and
the active variant get a deep-cloned body and keep
itemToSave.request.activeBodyVariantUid unchanged; cloneDeep is already imported
so replace the shallow spread usage with cloneDeep in the function handling
_item.request.bodyVariants.
In `@packages/bruno-filestore/src/formats/bru/index.ts`:
- Around line 111-137: The fallback paths when no matching active variant is
found (inside the bodyVariants handling) set
transformedJson.request.activeBodyVariantUid to the first variant's uid but do
not update transformedJson.request.body, so the request body can be out of sync;
update both fallback branches (the branch where activeVariantName exists but
find returns undefined, and the branch where no activeVariantName is present) to
also set transformedJson.request.body = _.cloneDeep(variantsWithUids[0].body) so
the activeBodyVariantUid and the actual request body remain consistent,
operating on the same transformedJson.request and using the existing
variantsWithUids and _.cloneDeep utilities.
In `@packages/bruno-lang/v2/src/bruToJson.js`:
- Around line 151-159: The grammar's bodyvariants rule and specific variant
rules (bodyvariantjson, bodyvarianttext, bodyvariantxml, bodyvariantsparql,
bodyvariantformurlencoded, bodyvariantmultipart) omit graphql and file, causing
variants with mode "graphql" or "file" to be dropped; either add matching
variant productions (e.g., bodyvariantgraphql = "body:graphql:variant:"
variantname st* "{" nl* textblock tagend and bodyvariantfile =
"body:file:variant:" variantname st* dictionary or appropriate structure) and
update the writer in jsonToBru.js to emit those modes, or add a clear comment
near bodyvariants and in jsonToBru.js documenting this V1 limitation so
consumers know these variant types are intentionally unsupported.
In `@packages/bruno-lang/v2/src/jsonToBru.js`:
- Around line 675-698: The multipart map callback can return undefined for items
whose type is neither 'text' nor 'file', producing "undefined" in the joined
string; update the handling inside the multipartForms.map callback (the block
building the multipart-form variant where multipartForms is derived from
enabled(variantBody.multipartForm).concat(disabled(...))) to ensure every branch
returns a string—either filter out unsupported types before mapping or add an
explicit default return (e.g., return '' or return a commented/skipped marker)
for unknown item.type; keep usage of helpers getKeyString, getValueString and
preserve contentType and `@file` behavior.
🧹 Nitpick comments (5)
packages/bruno-electron/src/utils/collection.js (1)
538-549: InlinerequireforcloneDeep— prefer hoisting to the top-level imports.
cloneDeepis required inline here (line 540) while all other lodash utilities are imported at line 1. Just add it to the existing destructuring at the top of the file.Suggested fix
At the top of the file (line 1):
-const { get, each, find, compact, isString, filter } = require('lodash'); +const { get, each, find, compact, isString, filter, cloneDeep } = require('lodash');Then in the function:
if (_item.request.bodyVariants && _item.request.bodyVariants.length > 0) { - const cloneDeep = require('lodash/cloneDeep'); const variants = _item.request.bodyVariants.map((variant) => {packages/bruno-filestore/src/formats/bru/index.ts (1)
252-264:bruJson.activeBodyVariantUidstores a name, not a UID — misleading property name.At line 261,
bruJson.activeBodyVariantUidis assignedactiveVariant.name. This works becausejsonToBruwrites it as theactiveBodyVariantkey in meta, but the intermediate property name is confusing. Consider renaming it tobruJson.activeBodyVariantorbruJson.activeBodyVariantNamefor clarity.packages/bruno-lang/v2/src/jsonToBru.js (1)
31-33:activeBodyVariantUidholds a variant name here, not a UID.This is the downstream effect of the naming confusion in
stringifyBruRequest(packages/bruno-filestore/src/formats/bru/index.ts, line 261). The variable destructured at line 17 asactiveBodyVariantUidactually contains the variant name when it reaches this point. No functional bug — just confusing naming across the pipeline.packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/StyledWrapper.js (1)
35-38: Hardcoded color for.caret— should use theme prop.Lines 36-37 use
rgb(140, 140, 140)directly. Per project conventions, colors in styled-components should come from the theme prop.Suggested fix
.caret { - color: rgb(140, 140, 140); - fill: rgb(140, 140, 140); + color: ${(props) => props.theme.text.muted || 'rgb(140, 140, 140)'}; + fill: ${(props) => props.theme.text.muted || 'rgb(140, 140, 140)'}; }Based on learnings: "Use styled component's theme prop to manage CSS colors, not CSS variables, when in the context of a styled component or any React component using styled components"
packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/index.js (1)
94-138:activeBodyVariantUidinuseMemodeps is unnecessary.
activeBodyVariantUidisn't referenced in the memo's body — the menu items don't change based on which variant is active. It's only used asselectedItemIdonMenuDropdown, which is outside this memo. Removing it avoids needless recomputations.- }, [bodyVariants, activeBodyVariantUid, onSwitchVariant, onAddVariant, onDeleteVariant]); + }, [bodyVariants, onSwitchVariant, onAddVariant, onDeleteVariant]);
| {bodyVariants.length > 2 && ( | ||
| <span | ||
| className="variant-action-btn" | ||
| title="Delete" | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| onDeleteVariant(variant.uid); | ||
| }} | ||
| > | ||
| <IconTrash size={14} strokeWidth={1.5} /> | ||
| </span> | ||
| )} |
There was a problem hiding this comment.
Delete button hidden when exactly 2 variants — user can't revert to no-variants state.
bodyVariants.length > 2 means the trash icon only appears with 3+ variants. With exactly 2, there's no way to delete one and collapse back to no-variants. The reducer already handles this case correctly (collapses when ≤1 remaining). The threshold should be > 1 (i.e., always show delete when there are at least 2 variants).
Fix
- {bodyVariants.length > 2 && (
+ {bodyVariants.length > 1 && (🤖 Prompt for AI Agents
In
`@packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/index.js`
around lines 112 - 123, The delete button is only rendered when
bodyVariants.length > 2, preventing removal when exactly two variants exist;
change the condition in the BodyVariantSelector rendering from
bodyVariants.length > 2 to bodyVariants.length > 1 so the trash IconTrash
(inside the span with className "variant-action-btn") is shown when there are at
least two variants, leaving the onDeleteVariant(variant.uid) handler and reducer
behavior intact to collapse variants when ≤1 remain.
| addBodyVariant: (state, action) => { | ||
| const collection = findCollectionByUid(state.collections, action.payload.collectionUid); | ||
|
|
||
| if (collection) { | ||
| const item = findItemInCollection(collection, action.payload.itemUid); | ||
|
|
||
| if (item && isItemARequest(item)) { | ||
| if (!item.draft) { | ||
| item.draft = cloneDeep(item); | ||
| } | ||
| const request = item.draft.request; | ||
|
|
||
| if (!request.bodyVariants) { | ||
| request.bodyVariants = []; | ||
| } | ||
|
|
||
| // If this is the first variant being added, also save the current body as "Default" | ||
| if (request.bodyVariants.length === 0) { | ||
| const defaultVariantUid = uuid(); | ||
| request.bodyVariants.push({ | ||
| uid: defaultVariantUid, | ||
| name: 'Default', | ||
| body: cloneDeep(request.body) | ||
| }); | ||
| request.activeBodyVariantUid = defaultVariantUid; | ||
| } | ||
|
|
||
| // Add new variant by cloning the current body | ||
| const newVariantUid = uuid(); | ||
| const variantName = action.payload.name || `Variant ${request.bodyVariants.length + 1}`; | ||
| request.bodyVariants.push({ | ||
| uid: newVariantUid, | ||
| name: variantName, | ||
| body: cloneDeep(request.body) | ||
| }); | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
New variant body is cloned from current body, not blank.
Per the PR description: "a 'Default' variant is created from the current body plus a new blank variant." However, line 1778 clones request.body for the new variant. If the intent is a blank variant, the body should be initialized with default/empty values instead.
Additionally, edits made through updateRequestBody modify request.body directly but never sync back to the active variant's stored body. The variant's body only gets saved when switchBodyVariant is dispatched. If the user edits the body and then saves without switching, the active variant's stored snapshot will be stale. Consider syncing request.body into the active variant on save (e.g., in saveRequest) or documenting this as by-design.
Proposed fix for blank body on new variant
// Add new variant by cloning the current body
const newVariantUid = uuid();
const variantName = action.payload.name || `Variant ${request.bodyVariants.length + 1}`;
request.bodyVariants.push({
uid: newVariantUid,
name: variantName,
- body: cloneDeep(request.body)
+ body: {
+ mode: request.body.mode || 'none',
+ json: null,
+ text: null,
+ xml: null,
+ sparql: null,
+ formUrlEncoded: null,
+ multipartForm: null,
+ graphql: null,
+ file: null
+ }
});🤖 Prompt for AI Agents
In `@packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js`
around lines 1745 - 1782, The addBodyVariant reducer currently clones
request.body into the new variant (in addBodyVariant) but per spec the new
variant should be blank; change the new variant creation to initialize body with
an empty/default structure instead of cloneDeep(request.body) (refer to
addBodyVariant and request.bodyVariants/request.activeBodyVariantUid). Also
ensure edits to the live request.body are persisted into the active variant when
the request is saved—update saveRequest (or call from updateRequestBody) to
locate the active variant by request.activeBodyVariantUid and copy current
request.body into that variant's body before finalizing save (see
updateRequestBody, switchBodyVariant and saveRequest for where to hook this
sync).
| // Save body variants | ||
| if (_item.request.bodyVariants && _item.request.bodyVariants.length > 0) { | ||
| // Sync the current body back to the active variant before saving | ||
| const variants = _item.request.bodyVariants.map((variant) => { | ||
| if (variant.uid === _item.request.activeBodyVariantUid) { | ||
| return { ...variant, body: { ...itemToSave.request.body } }; | ||
| } | ||
| return variant; | ||
| }); | ||
| itemToSave.request.bodyVariants = variants; | ||
| itemToSave.request.activeBodyVariantUid = _item.request.activeBodyVariantUid; | ||
| } |
There was a problem hiding this comment.
Shallow copy vs. cloneDeep — inconsistent with the electron counterpart.
The electron version at packages/bruno-electron/src/utils/collection.js (line 543) uses cloneDeep(itemToSave.request.body), but here you're using a spread ({ ...itemToSave.request.body }). Since body can contain nested objects (e.g., formUrlEncoded, multipartForm, graphql with sub-objects), a shallow copy can lead to shared references between the variant body and the request body.
Suggested fix
- return { ...variant, body: { ...itemToSave.request.body } };
+ return { ...variant, body: cloneDeep(itemToSave.request.body) };cloneDeep is already imported from lodash at line 1.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Save body variants | |
| if (_item.request.bodyVariants && _item.request.bodyVariants.length > 0) { | |
| // Sync the current body back to the active variant before saving | |
| const variants = _item.request.bodyVariants.map((variant) => { | |
| if (variant.uid === _item.request.activeBodyVariantUid) { | |
| return { ...variant, body: { ...itemToSave.request.body } }; | |
| } | |
| return variant; | |
| }); | |
| itemToSave.request.bodyVariants = variants; | |
| itemToSave.request.activeBodyVariantUid = _item.request.activeBodyVariantUid; | |
| } | |
| // Save body variants | |
| if (_item.request.bodyVariants && _item.request.bodyVariants.length > 0) { | |
| // Sync the current body back to the active variant before saving | |
| const variants = _item.request.bodyVariants.map((variant) => { | |
| if (variant.uid === _item.request.activeBodyVariantUid) { | |
| return { ...variant, body: cloneDeep(itemToSave.request.body) }; | |
| } | |
| return variant; | |
| }); | |
| itemToSave.request.bodyVariants = variants; | |
| itemToSave.request.activeBodyVariantUid = _item.request.activeBodyVariantUid; | |
| } |
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/utils/collections/index.js` around lines 760 - 771,
The current code updates body variants by shallow-copying
itemToSave.request.body with object spread, which can leave nested objects
shared; change the assignment inside the variants map to use
cloneDeep(itemToSave.request.body) for the variant whose uid equals
_item.request.activeBodyVariantUid, ensuring itemToSave.request.bodyVariants and
the active variant get a deep-cloned body and keep
itemToSave.request.activeBodyVariantUid unchanged; cloneDeep is already imported
so replace the shallow spread usage with cloneDeep in the function handling
_item.request.bodyVariants.
|
|
||
| // Handle body variants | ||
| const bodyVariants = _.get(json, 'bodyVariants', []); | ||
| if (bodyVariants.length > 0) { | ||
| // Assign UIDs to variants (parsed from .bru files won't have UIDs) | ||
| const variantsWithUids = bodyVariants.map((variant: any) => ({ | ||
| ...variant, | ||
| uid: variant.uid || uuid() | ||
| })); | ||
| transformedJson.request.bodyVariants = variantsWithUids; | ||
|
|
||
| // Match active variant by name (names are what survive the .bru round-trip) | ||
| const activeVariantName = _.get(json, 'meta.activeBodyVariant'); | ||
| if (activeVariantName) { | ||
| const activeVariant = variantsWithUids.find((v: any) => v.name === activeVariantName); | ||
| if (activeVariant) { | ||
| transformedJson.request.activeBodyVariantUid = activeVariant.uid; | ||
| // Load the active variant's body as the request body | ||
| transformedJson.request.body = _.cloneDeep(activeVariant.body); | ||
| } else { | ||
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | ||
| } | ||
| } else { | ||
| // No active variant saved, default to first | ||
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | ||
| } | ||
| } |
There was a problem hiding this comment.
Fallback when active variant name doesn't match: body not loaded from the first variant.
When activeVariantName is set but no matching variant is found (line 130-132), activeBodyVariantUid defaults to the first variant but the request body is not overwritten with that first variant's body. The same applies to lines 133-136 (no active variant saved at all). This means the user could see a body that doesn't match the "active" variant.
If this is intentional (prefer the main body block as fallback), a brief comment would help clarify. Otherwise, consider loading the first variant's body in both fallback paths:
Suggested fix for the fallback at line 130
} else {
transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid;
+ transformedJson.request.body = _.cloneDeep(variantsWithUids[0].body);
}
} else {
// No active variant saved, default to first
transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid;
+ transformedJson.request.body = _.cloneDeep(variantsWithUids[0].body);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Handle body variants | |
| const bodyVariants = _.get(json, 'bodyVariants', []); | |
| if (bodyVariants.length > 0) { | |
| // Assign UIDs to variants (parsed from .bru files won't have UIDs) | |
| const variantsWithUids = bodyVariants.map((variant: any) => ({ | |
| ...variant, | |
| uid: variant.uid || uuid() | |
| })); | |
| transformedJson.request.bodyVariants = variantsWithUids; | |
| // Match active variant by name (names are what survive the .bru round-trip) | |
| const activeVariantName = _.get(json, 'meta.activeBodyVariant'); | |
| if (activeVariantName) { | |
| const activeVariant = variantsWithUids.find((v: any) => v.name === activeVariantName); | |
| if (activeVariant) { | |
| transformedJson.request.activeBodyVariantUid = activeVariant.uid; | |
| // Load the active variant's body as the request body | |
| transformedJson.request.body = _.cloneDeep(activeVariant.body); | |
| } else { | |
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | |
| } | |
| } else { | |
| // No active variant saved, default to first | |
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | |
| } | |
| } | |
| // Handle body variants | |
| const bodyVariants = _.get(json, 'bodyVariants', []); | |
| if (bodyVariants.length > 0) { | |
| // Assign UIDs to variants (parsed from .bru files won't have UIDs) | |
| const variantsWithUids = bodyVariants.map((variant: any) => ({ | |
| ...variant, | |
| uid: variant.uid || uuid() | |
| })); | |
| transformedJson.request.bodyVariants = variantsWithUids; | |
| // Match active variant by name (names are what survive the .bru round-trip) | |
| const activeVariantName = _.get(json, 'meta.activeBodyVariant'); | |
| if (activeVariantName) { | |
| const activeVariant = variantsWithUids.find((v: any) => v.name === activeVariantName); | |
| if (activeVariant) { | |
| transformedJson.request.activeBodyVariantUid = activeVariant.uid; | |
| // Load the active variant's body as the request body | |
| transformedJson.request.body = _.cloneDeep(activeVariant.body); | |
| } else { | |
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | |
| transformedJson.request.body = _.cloneDeep(variantsWithUids[0].body); | |
| } | |
| } else { | |
| // No active variant saved, default to first | |
| transformedJson.request.activeBodyVariantUid = variantsWithUids[0].uid; | |
| transformedJson.request.body = _.cloneDeep(variantsWithUids[0].body); | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@packages/bruno-filestore/src/formats/bru/index.ts` around lines 111 - 137,
The fallback paths when no matching active variant is found (inside the
bodyVariants handling) set transformedJson.request.activeBodyVariantUid to the
first variant's uid but do not update transformedJson.request.body, so the
request body can be out of sync; update both fallback branches (the branch where
activeVariantName exists but find returns undefined, and the branch where no
activeVariantName is present) to also set transformedJson.request.body =
_.cloneDeep(variantsWithUids[0].body) so the activeBodyVariantUid and the actual
request body remain consistent, operating on the same transformedJson.request
and using the existing variantsWithUids and _.cloneDeep utilities.
| // Body Variants - multiple named body variants per request | ||
| bodyvariants = bodyvariantjson | bodyvarianttext | bodyvariantxml | bodyvariantsparql | bodyvariantformurlencoded | bodyvariantmultipart | ||
| variantname = (~(st* "{") any)+ | ||
| bodyvariantjson = "body:json:variant:" variantname st* "{" nl* textblock tagend | ||
| bodyvarianttext = "body:text:variant:" variantname st* "{" nl* textblock tagend | ||
| bodyvariantxml = "body:xml:variant:" variantname st* "{" nl* textblock tagend | ||
| bodyvariantsparql = "body:sparql:variant:" variantname st* "{" nl* textblock tagend | ||
| bodyvariantformurlencoded = "body:form-urlencoded:variant:" variantname dictionary | ||
| bodyvariantmultipart = "body:multipart-form:variant:" variantname dictionary |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if graphql or file body variant rules exist or are mentioned
rg -n 'graphql.*variant|variant.*graphql|bodyvariantgraphql' --type=js --type=ts packages/bruno-lang/
rg -n 'file.*variant|variant.*file|bodyvariantfile' --type=js --type=ts packages/bruno-lang/Repository: usebruno/bruno
Length of output: 40
🏁 Script executed:
# Check if main body rules for graphql and file exist
rg -n 'bodygraphql\s*=|bodyfile\s*=' --type=js --type=ts packages/bruno-lang/Repository: usebruno/bruno
Length of output: 475
🏁 Script executed:
# Examine the bruToJson.js file structure around lines 151-159 and the semantic actions
sed -n '30,40p' packages/bruno-lang/v2/src/bruToJson.js
sed -n '145,170p' packages/bruno-lang/v2/src/bruToJson.jsRepository: usebruno/bruno
Length of output: 2055
🏁 Script executed:
# Check jsonToBru.js for variant handling and graphql/file mentions
rg -n 'bodyVariants|graphql|bodyfile' packages/bruno-lang/v2/src/jsonToBru.js | head -30Repository: usebruno/bruno
Length of output: 631
🏁 Script executed:
# Look for any TODOs or comments about missing variants
rg -n 'TODO|FIXME|graphql.*variant|file.*variant' packages/bruno-lang/ -iRepository: usebruno/bruno
Length of output: 394
🏁 Script executed:
# Get the full variant emission logic from jsonToBru.js
sed -n '634,700p' packages/bruno-lang/v2/src/jsonToBru.jsRepository: usebruno/bruno
Length of output: 3010
🏁 Script executed:
# Also check the semantic actions in bruToJson.js to see what modes are supported
sed -n '1080,1150p' packages/bruno-lang/v2/src/bruToJson.jsRepository: usebruno/bruno
Length of output: 1797
🏁 Script executed:
# Check if there are any graphql or file variant handlers in the semantic actions
rg -A5 'bodyvariantgraphql|bodyvariantfile' packages/bruno-lang/v2/src/bruToJson.jsRepository: usebruno/bruno
Length of output: 40
Add graphql and file body variant support or document the limitation
The grammar supports graphql and file main body types (lines 142, 149), but the variant rules (lines 151-159) only cover json, text, xml, sparql, formUrlEncoded, and multipartForm. The jsonToBru.js writer also only emits these six variant modes—if variant data with mode graphql or file exists in memory, it silently drops on save.
Either add the missing variant rules for completeness or add a comment explaining this is a known V1 limitation.
🤖 Prompt for AI Agents
In `@packages/bruno-lang/v2/src/bruToJson.js` around lines 151 - 159, The
grammar's bodyvariants rule and specific variant rules (bodyvariantjson,
bodyvarianttext, bodyvariantxml, bodyvariantsparql, bodyvariantformurlencoded,
bodyvariantmultipart) omit graphql and file, causing variants with mode
"graphql" or "file" to be dropped; either add matching variant productions
(e.g., bodyvariantgraphql = "body:graphql:variant:" variantname st* "{" nl*
textblock tagend and bodyvariantfile = "body:file:variant:" variantname st*
dictionary or appropriate structure) and update the writer in jsonToBru.js to
emit those modes, or add a clear comment near bodyvariants and in jsonToBru.js
documenting this V1 limitation so consumers know these variant types are
intentionally unsupported.
| if (mode === 'multipartForm' && variantBody.multipartForm && variantBody.multipartForm.length) { | ||
| bru += `body:multipart-form:variant:${name} {`; | ||
| const multipartForms = enabled(variantBody.multipartForm).concat(disabled(variantBody.multipartForm)); | ||
| if (multipartForms.length) { | ||
| bru += `\n${indentString( | ||
| multipartForms | ||
| .map((item) => { | ||
| const isEnabled = item.enabled ? '' : '~'; | ||
| const contentType = item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; | ||
| if (item.type === 'text') { | ||
| return `${isEnabled}${getKeyString(item.name)}: ${getValueString(item.value)}${contentType}`; | ||
| } | ||
| if (item.type === 'file') { | ||
| const filepaths = Array.isArray(item.value) ? item.value : []; | ||
| const filestr = filepaths.join('|'); | ||
| const value = `@file(${filestr})`; | ||
| return `${isEnabled}${getKeyString(item.name)}: ${value}${contentType}`; | ||
| } | ||
| }) | ||
| .join('\n') | ||
| )}`; | ||
| } | ||
| bru += '\n}\n\n'; | ||
| } |
There was a problem hiding this comment.
Missing return value in multipart .map() callback for non-text/non-file items.
The static analysis tool correctly flags line 681: if a multipart item is neither type === 'text' nor type === 'file', the callback returns undefined, which produces an undefined entry in the joined string. This is a pre-existing issue in the main multipart handler (line 531-547 same pattern), but it's been duplicated here.
Suggested fix
if (item.type === 'file') {
const filepaths = Array.isArray(item.value) ? item.value : [];
const filestr = filepaths.join('|');
const value = `@file(${filestr})`;
return `${isEnabled}${getKeyString(item.name)}: ${value}${contentType}`;
}
+ return `${isEnabled}${getKeyString(item.name)}: ${getValueString(item.value)}`;
})🧰 Tools
🪛 Biome (2.3.14)
[error] 681-681: This callback passed to map() iterable method should always return a value.
Add missing return statements so that this callback returns a value on all execution paths.
(lint/suspicious/useIterableCallbackReturn)
🤖 Prompt for AI Agents
In `@packages/bruno-lang/v2/src/jsonToBru.js` around lines 675 - 698, The
multipart map callback can return undefined for items whose type is neither
'text' nor 'file', producing "undefined" in the joined string; update the
handling inside the multipartForms.map callback (the block building the
multipart-form variant where multipartForms is derived from
enabled(variantBody.multipartForm).concat(disabled(...))) to ensure every branch
returns a string—either filter out unsupported types before mapping or add an
explicit default return (e.g., return '' or return a commented/skipped marker)
for unknown item.type; keep usage of helpers getKeyString, getValueString and
preserve contentType and `@file` behavior.
Description
Multiple Body Variants per Request
This PR adds the ability to create and manage multiple named body payloads (variants) within a single request. Users can save different body configurations (e.g., "Default", "Error Case", "Empty Payload") and quickly switch between them via a dropdown selector in the Body tab header. The currently selected variant is the one sent when the request is executed.
Why: When working with APIs, developers frequently need to test the same endpoint with different request bodies — valid payloads, edge cases, error scenarios, etc. Currently this requires manually editing the body each time or duplicating the entire request. Body variants solve this by letting users save multiple body configurations in one place.
How it works:
.bruand.yml(OpenCollection) file formatsChanges across the codebase:
packages/bruno-schema-types/src/requests/http.ts— AddedBodyVariantinterface,bodyVariantsandactiveBodyVariantUidtoHttpRequestpackages/bruno-schema/src/collections/index.js— AddedbodyVariantSchemaand new fields torequestSchemapackages/bruno-app/src/providers/ReduxStore/slices/collections/index.js— AddedaddBodyVariant,switchBodyVariant,renameBodyVariant,deleteBodyVariantreducers; updatedmergeRequestWithPreservedUidsto preserve variantspackages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/— New dropdown component for managing variantspackages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js— Added variant selector to Body tab headerpackages/bruno-app/src/utils/collections/index.jsandpackages/bruno-electron/src/utils/collection.js— Both copies oftransformRequestToSaveToFilesystemupdated to include body variantspackages/bruno-lang/v2/src/bruToJson.js— Extended ohm grammar withbody:json:variant:Namerules;packages/bruno-lang/v2/src/jsonToBru.js— Writes variant blockspackages/bruno-filestore/src/formats/yml/items/stringifyHttpRequest.tsandparseHttpRequest.ts— Added body variants read/write for OpenCollection formatpackages/bruno-filestore/src/formats/bru/index.ts— UpdatedparseBruRequestandstringifyBruRequestto handle body variants with UID assignment and active variant matching by nameIssue:
#7134
Contribution Checklist: