Skip to content

Added new feature supports multiple request body#7133

Open
GokulMithran0302 wants to merge 2 commits intousebruno:mainfrom
GokulMithran0302:Added-new-feature--Supports-Multiple-Request-Body
Open

Added new feature supports multiple request body#7133
GokulMithran0302 wants to merge 2 commits intousebruno:mainfrom
GokulMithran0302:Added-new-feature--Supports-Multiple-Request-Body

Conversation

@GokulMithran0302
Copy link

@GokulMithran0302 GokulMithran0302 commented Feb 13, 2026

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:

  • Click the + icon next to the body mode selector to create the first variant
  • A "Default" variant is created from your current body, plus a new blank variant
  • Use the dropdown to switch between variants — the active one is sent on "Send"
  • Rename variants with the pencil icon, delete with the trash icon
  • Variants are persisted to both .bru and .yml (OpenCollection) file formats
  • Fully backward compatible — requests without variants work exactly as before

Changes across the codebase:

Layer Files Changed
Schema Types packages/bruno-schema-types/src/requests/http.ts — Added BodyVariant interface, bodyVariants and activeBodyVariantUid to HttpRequest
Schema Validation packages/bruno-schema/src/collections/index.js — Added bodyVariantSchema and new fields to requestSchema
Redux State packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js — Added addBodyVariant, switchBodyVariant, renameBodyVariant, deleteBodyVariant reducers; updated mergeRequestWithPreservedUids to preserve variants
UI Component packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/ — New dropdown component for managing variants
UI Integration packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js — Added variant selector to Body tab header
Save Transform packages/bruno-app/src/utils/collections/index.js and packages/bruno-electron/src/utils/collection.js — Both copies of transformRequestToSaveToFilesystem updated to include body variants
.bru Format packages/bruno-lang/v2/src/bruToJson.js — Extended ohm grammar with body:json:variant:Name rules; packages/bruno-lang/v2/src/jsonToBru.js — Writes variant blocks
.yml Format packages/bruno-filestore/src/formats/yml/items/stringifyHttpRequest.ts and parseHttpRequest.ts — Added body variants read/write for OpenCollection format
Filestore packages/bruno-filestore/src/formats/bru/index.ts — Updated parseBruRequest and stringifyBruRequest to handle body variants with UID assignment and active variant matching by name

Issue:
#7134

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.
bruno1 image

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Walkthrough

This 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

Cohort / File(s) Summary
UI Components
packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js, packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/index.js, packages/bruno-app/src/components/RequestPane/RequestBody/BodyVariantSelector/StyledWrapper.js
New BodyVariantSelector component with dropdown menu for managing variants; integrates into RequestPane with flex layout. Handles variant creation, renaming, deletion, and switching via menu actions and keyboard shortcuts.
Redux State Management
packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
Four new actions (addBodyVariant, switchBodyVariant, renameBodyVariant, deleteBodyVariant) to manage variant lifecycle. Enhanced request merging to preserve bodyVariants and activeBodyVariantUid across state updates.
File Format Support
packages/bruno-filestore/src/formats/bru/index.ts, packages/bruno-filestore/src/formats/yml/items/parseHttpRequest.ts, packages/bruno-filestore/src/formats/yml/items/stringifyHttpRequest.ts
Extended Bru and YAML parsers/stringifiers to serialize/deserialize bodyVariants and activeBodyVariantUid; uuid generation for variant identification and round-trip preservation.
Language Grammar
packages/bruno-lang/v2/src/bruToJson.js, packages/bruno-lang/v2/src/jsonToBru.js
Added grammar rules for variant-specific body blocks (json, text, xml, sparql, form-urlencoded, multipart); bidirectional conversion between JSON and Bru syntax for variant definitions.
Schema & Type Definitions
packages/bruno-schema-types/src/requests/http.ts, packages/bruno-schema-types/src/requests/index.ts, packages/bruno-schema/src/collections/index.js
Introduced BodyVariant interface with uid, name, body; extended HttpRequest with bodyVariants array and activeBodyVariantUid field; updated validation schema.
Serialization Utilities
packages/bruno-app/src/utils/collections/index.js, packages/bruno-electron/src/utils/collection.js
Enhanced transformRequestToSaveToFilesystem to sync active variant body before persisting; ensures active variant reflects latest request body state.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • PR #6329: Implements and wires up multi-variant request bodies across import/export/conversion paths in parallel.
  • PR #6335: Modifies OpenCollection YAML HTTP parsing/stringifying paths for related request schema adaptations.

Suggested labels

size/XXL

Suggested reviewers

  • lohit-bruno
  • helloanoop
  • naman-bruno

Poem

🔄 ✨ Bodies multiply in structured grace,
Variants dance in their own space,
Add, switch, rename with flair,
Multiple choices, handled with care!

🚥 Pre-merge checks | ✅ 2 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (34 files):

⚔️ .gitignore (content)
⚔️ package-lock.json (content)
⚔️ package.json (content)
⚔️ packages/bruno-app/src/components/EnvironmentVariablesTable/index.js (content)
⚔️ packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js (content)
⚔️ packages/bruno-app/src/components/Sidebar/ImportCollection/FileTab.js (content)
⚔️ packages/bruno-app/src/components/Sidebar/ImportCollection/index.js (content)
⚔️ packages/bruno-app/src/components/Sidebar/Sections/CollectionsSection/index.js (content)
⚔️ packages/bruno-app/src/components/WorkspaceHome/WorkspaceOverview/index.js (content)
⚔️ packages/bruno-app/src/providers/App/useIpcEvents.js (content)
⚔️ packages/bruno-app/src/providers/ReduxStore/slices/app.js (content)
⚔️ packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js (content)
⚔️ packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js (content)
⚔️ packages/bruno-app/src/utils/collections/index.js (content)
⚔️ packages/bruno-app/src/utils/exporters/bruno-environment.js (content)
⚔️ packages/bruno-app/src/utils/importers/common.js (content)
⚔️ packages/bruno-cli/src/utils/collection.js (content)
⚔️ packages/bruno-electron/package.json (content)
⚔️ packages/bruno-electron/src/index.js (content)
⚔️ packages/bruno-electron/src/ipc/collection.js (content)
⚔️ packages/bruno-electron/src/ipc/preferences.js (content)
⚔️ packages/bruno-electron/src/utils/collection-import.js (content)
⚔️ packages/bruno-electron/src/utils/collection.js (content)
⚔️ packages/bruno-electron/src/utils/filesystem.js (content)
⚔️ packages/bruno-filestore/src/formats/bru/index.ts (content)
⚔️ packages/bruno-filestore/src/formats/yml/items/parseHttpRequest.ts (content)
⚔️ packages/bruno-filestore/src/formats/yml/items/stringifyHttpRequest.ts (content)
⚔️ packages/bruno-lang/v2/src/bruToJson.js (content)
⚔️ packages/bruno-lang/v2/src/jsonToBru.js (content)
⚔️ packages/bruno-schema-types/src/requests/http.ts (content)
⚔️ packages/bruno-schema-types/src/requests/index.ts (content)
⚔️ packages/bruno-schema/src/collections/index.js (content)
⚔️ tests/environments/multiline-variables/write-multiline-variable.spec.ts (content)
⚔️ tests/request/headers/header-validation.spec.ts (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
Title check ❓ Inconclusive The title mentions supporting multiple request body, which aligns with the core feature, but contains a grammatical issue ('Added new feature supports' has extra spacing and awkward phrasing) and is slightly vague. Refine the title to be more precise and grammatically correct. Consider: 'Add support for multiple request body variants' or 'Support multiple named request bodies via body variants.'
✅ Passed checks (2 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
⚔️ Resolve merge conflicts (beta)
  • Auto-commit resolved conflicts to branch Added-new-feature--Supports-Multiple-Request-Body
  • Post resolved changes as copyable diffs in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: Inline require for cloneDeep — prefer hoisting to the top-level imports.

cloneDeep is 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.activeBodyVariantUid stores a name, not a UID — misleading property name.

At line 261, bruJson.activeBodyVariantUid is assigned activeVariant.name. This works because jsonToBru writes it as the activeBodyVariant key in meta, but the intermediate property name is confusing. Consider renaming it to bruJson.activeBodyVariant or bruJson.activeBodyVariantName for clarity.

packages/bruno-lang/v2/src/jsonToBru.js (1)

31-33: activeBodyVariantUid holds 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 as activeBodyVariantUid actually 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: activeBodyVariantUid in useMemo deps is unnecessary.

activeBodyVariantUid isn't referenced in the memo's body — the menu items don't change based on which variant is active. It's only used as selectedItemId on MenuDropdown, which is outside this memo. Removing it avoids needless recomputations.

-  }, [bodyVariants, activeBodyVariantUid, onSwitchVariant, onAddVariant, onDeleteVariant]);
+  }, [bodyVariants, onSwitchVariant, onAddVariant, onDeleteVariant]);

Comment on lines +112 to +123
{bodyVariants.length > 2 && (
<span
className="variant-action-btn"
title="Delete"
onClick={(e) => {
e.stopPropagation();
onDeleteVariant(variant.uid);
}}
>
<IconTrash size={14} strokeWidth={1.5} />
</span>
)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1745 to +1782
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)
});
}
}
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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).

Comment on lines +760 to +771
// 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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
// 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.

Comment on lines +111 to +137

// 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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 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.

Comment on lines +151 to +159
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.js

Repository: 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 -30

Repository: 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/ -i

Repository: 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.js

Repository: 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.js

Repository: 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.js

Repository: 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.

Comment on lines +675 to +698
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';
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments