Skip to content

Bruno v2 → v3: Breaking Changes

Bruno v3 was our biggest release ever and introduced massive improvements to UI/UX, the support of YAML as a storage language, Local Workspaces, and more. This post is intentionally narrow: only the breaking changes you might hit when upgrading.


Breaking Change #1: Script Execution Scoping Change

What changed

In Bruno v3, scripts (pre-request, post-response, and tests) are no longer concatenated into a single execution block. Each script level now runs inside its own isolated async IIFE (Immediately Invoked Function Expression).

The implementation wraps each script in:

Execution wrapper (v3)
(async () => {
  // script code
})();

What was impacted (before v3)

Previously, Bruno merged all scripts (collection → folder(s) → request) into one big script block that shared:

  • Variable scope — variables declared at collection level were visible in folder and request scripts
  • Control flow — a return at collection level would stop all subsequent scripts from running

Breaking patterns (what no longer works)

❌ Pattern 1: Sharing variables across script levels

Before (v2 — worked):

v2 — shared scope (worked)
// Collection pre-request script
const authToken = await generateToken();

// Request pre-request script
req.setHeader('Authorization', `Bearer ${authToken}`);  // ✅ Worked - shared scope

After (v3 — broken):

v3 — isolated scope (broken)
// Collection pre-request script
const authToken = await generateToken();  // Scoped to this IIFE only

// Request pre-request script
req.setHeader('Authorization', `Bearer ${authToken}`);  // ❌ ReferenceError: authToken is not defined

Fix: Use bru.setVar() / bru.getVar() to share data between script levels.

Fix — share via vars
// Collection pre-request script
const authToken = await generateToken();
bru.setVar('authToken', authToken);

// Request pre-request script
const authToken = bru.getVar('authToken');
req.setHeader('Authorization', `Bearer ${authToken}`);

❌ Pattern 2: Guard clauses / early returns affecting other scripts

Before (v2 — problematic behavior):

v2 — return stops everything
// Collection pre-request script
if (bru.getEnvName() === 'test') {
  return;  // This would STOP folder and request scripts from running!
}

// Request pre-request script
console.log('Setting up request...');  // ❌ Never ran when env was 'test'

After (v3 — fixed):

v3 — return only exits this level
// Collection pre-request script
if (bru.getEnvName() === 'test') {
  return;  // Only exits the collection script's IIFE
}

// Request pre-request script
console.log('Setting up request...');  // ✅ Now runs regardless of collection return

❌ Pattern 3: Re-declaring variables with the same name

Before (v2 — error):

v2 — redeclare fails
// Collection pre-request script
const response = await fetch('...');

// Request pre-request script
const response = await fetch('...');  // ❌ SyntaxError: Identifier 'response' has already been declared

After (v3 — works):

v3 — redeclare works (isolated scopes)
// Collection pre-request script
const response = await fetch('...');  // Scoped to collection IIFE

// Request pre-request script
const response = await fetch('...');  // ✅ Works - separate scope

Summary table

Aspect v2 Behavior v3 Behavior
Variable declarations Shared across all levels Isolated per level
return statements Stops ALL scripts Only exits current level
Same variable names Causes re-declaration error Allowed (separate scopes)
Data sharing method Direct variable access Must use bru.setVar() / bru.getVar()

Why this change was made

This was done to fix a real-world bug (#6130) where users’ collection-level guard clauses were unexpectedly preventing request-level scripts from running. The fix (PR #6229) wraps each script in an isolated async IIFE to ensure proper scoping and independent execution.


Breaking Change #2: Scripting Runtime Change (vm2 → NodeVM)

What changed

Bruno’s Developer Mode scripting runtime has moved from vm2 to NodeVM (Node.js’s built-in vm module with enhanced security).

Mode v2 Runtime v3 Runtime
Safe Mode QuickJS (WASM sandbox) QuickJS (unchanged)
Developer Mode vm2 NodeVM

Why this change was made

vm2 was deprecated in July 2023 due to critical security vulnerabilities that could not be properly addressed (Issue #533). The npm package also warns that it should not be used for production.

npm deprecation warning
npm WARN deprecated vm2@3.9.19: The library contains critical security issues
and should not be used for production! The maintenance of the project has been
discontinued. Consider migrating your code to isolated-vm.

Bruno chose NodeVM (using Node’s native vm.runInNewContext()) over isolated-vm for better compatibility with existing scripts.

What’s different in NodeVM

✅ What still works

Most existing scripts will continue to work as before:

Post-response script (crypto)
const crypto = require('crypto');

const responseBody = res.getBody();
const hash = crypto
  .createHash('sha256')
  .update(JSON.stringify(responseBody))
  .digest('hex');

bru.setVar('responseHash', hash);
console.log('Response hash:', hash);
Pre-request script (url + path)
const url = require('url');
const path = require('path');

const apiUrl = 'https://echo.usebruno.com';
const parsed = new url.URL(apiUrl);

console.log('Host:', parsed.hostname);
console.log('Protocol:', parsed.protocol);

req.setHeader('X-Request-Path', path.basename(req.getUrl()));
Local module loading
const helpers = require('./lib/helpers.js');
const token = helpers.generateAuthToken();

🔄 Potential edge cases

  1. Module resolution paths
    NodeVM uses a stricter module resolution. If you have modules in non-standard locations, add additionalContextRoots to your bruno.json:
    bruno.json
    {
      "scripting": {
        "additionalContextRoots": ["./shared-libs", "../common-utils"]
      }
    }
  2. Error stack traces
    Error messages now show the actual script filename for better debugging:
    Stack trace differences
    // Before (vm2)
    Error: Cannot read property 'x' of undefined
       at Script.runInContext (vm2/lib/main.js:123)
    
    // After (NodeVM)
    Error: Cannot read property 'x' of undefined
       at /path/to/collection/script.js:15:10

Available context in NodeVM (Developer Mode)

The NodeVM runtime provides these globals:

Globals in Developer Mode
// Bruno API objects
bru, req, res, test, expect, assert

// Node.js globals
Buffer, process, setTimeout, setInterval, clearTimeout,
clearInterval, setImmediate, clearImmediate

// Error types
Error, TypeError, ReferenceError, SyntaxError, RangeError

// TypedArrays
Uint8Array, Uint16Array, Uint32Array, Int8Array, Int16Array,
Int32Array, Float32Array, Float64Array, ArrayBuffer, DataView

// Module loading
require()  // with security restrictions

Migration checklist

  • Ensure npm dependencies are installed in the collection directory
  • Check for any reliance on vm2-specific behaviors
  • Test scripts that use require() with relative paths
  • Verify filesystem access is enabled if using fs

Breaking Change #3: Bruno CLI Sandbox Defaults

What changed

The Bruno CLI now defaults to Safe Mode (--sandbox=safe) for script execution, rather than Developer Mode.

Version Default Sandbox Runtime Used
v2 CLI Developer Mode vm2
v3 CLI Safe Mode QuickJS

Why this change was made

Safe Mode provides a secure sandbox (QuickJS/WASM) that:

  • Cannot access the filesystem
  • Cannot execute system commands
  • Cannot access sensitive system information
  • Prevents malicious scripts from causing harm

This is especially important for CI/CD pipelines where you may run collections from external sources.

What’s impacted

❌ Scripts that will fail in Safe Mode

1) Filesystem access
Fails in Safe Mode (fs)
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
bru.setVar('apiKey', config.apiKey);

Error:

Safe Mode error
Error: require is not defined
2) System commands
Fails in Safe Mode (child_process)
const { execSync } = require('child_process');
const gitCommit = execSync('git rev-parse HEAD').toString().trim();
req.setHeader('X-Git-Commit', gitCommit);
3) Native Node modules
Fails in Safe Mode (crypto)
const crypto = require('crypto');  // Native module - not available

✅ Scripts that work in Safe Mode

Safe Mode supports a curated set of libraries via shims:

Works in Safe Mode (shims)
const uuid = require('uuid');
const axios = require('axios');
const _ = require('lodash');
const moment = require('moment');
const CryptoJS = require('crypto-js');
const jwt = require('jsonwebtoken');
const { nanoid } = require('nanoid');

// Bruno API - fully available
bru.setVar('requestId', uuid.v4());
bru.setEnvVar('timestamp', Date.now());
req.setHeader('Authorization', `Bearer ${bru.getVar('token')}`);

How to use Developer Mode in CLI

If your scripts require filesystem access, system commands, or native modules, pass the --sandbox=developer flag:

CLI
# Default - Safe Mode (QuickJS sandbox)
bru run --env production

# Explicit - Developer Mode (NodeVM with full access)
bru run --env production --sandbox=developer

CI/CD examples

GitHub Actions — Safe Mode (default, recommended for untrusted collections):

GitHub Actions (Safe Mode)
- name: Run Bruno Tests
  run: |
    npx @usebruno/cli run folder --env ci

GitHub Actions — Developer Mode (trusted collections only):

GitHub Actions (Developer Mode)
- name: Run Bruno Tests (Developer Mode)
  run: |
    npx @usebruno/cli run folder --env ci --sandbox=developer

Migration checklist for CLI users

  • Audit scripts: do any use require('fs'), require('child_process'), or require('crypto')?
  • Update CI/CD pipelines: add --sandbox=developer if needed
  • Security note: only use --sandbox=developer with collections you trust