In the world of modern web applications, secure communication between clients and servers is...
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:
(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
returnat 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):
// Collection pre-request script
const authToken = await generateToken();
// Request pre-request script
req.setHeader('Authorization', `Bearer ${authToken}`); // ✅ Worked - shared scope
After (v3 — 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.
// 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):
// 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):
// 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):
// 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):
// 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 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:
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);
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()));
const helpers = require('./lib/helpers.js');
const token = helpers.generateAuthToken();
🔄 Potential edge cases
- Module resolution paths
NodeVM uses a stricter module resolution. If you have modules in non-standard locations, addadditionalContextRootsto yourbruno.json:bruno.json{ "scripting": { "additionalContextRoots": ["./shared-libs", "../common-utils"] } } - 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:
// 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
const fs = require('fs');
const config = JSON.parse(fs.readFileSync('./config.json', 'utf8'));
bru.setVar('apiKey', config.apiKey);
Error:
Error: require is not defined
2) System commands
const { execSync } = require('child_process');
const gitCommit = execSync('git rev-parse HEAD').toString().trim();
req.setHeader('X-Git-Commit', gitCommit);
3) Native Node modules
const crypto = require('crypto'); // Native module - not available
✅ Scripts that work in Safe Mode
Safe Mode supports a curated set of libraries via 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:
# 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):
- name: Run Bruno Tests
run: |
npx @usebruno/cli run folder --env ci
GitHub Actions — Developer Mode (trusted collections only):
- 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'), orrequire('crypto')? - Update CI/CD pipelines: add
--sandbox=developerif needed - Security note: only use
--sandbox=developerwith collections you trust