Internal architecture and design decisions for Fuzzbox.
- Zero runtime dependencies - Only use Node.js built-ins
- Minimal overhead - When chaos isn't triggered, impact should be <1ms
- Framework agnostic core - Chaos logic separated from adapters
- Mutable state - Dashboard needs to modify config at runtime
- Fail-safe - TypeScript strict mode, comprehensive error handling
fuzzbox/
├── src/
│ ├── core.ts # Central chaos engine
│ ├── types.ts # TypeScript interfaces
│ ├── adapters/
│ │ ├── express.ts # Express-specific middleware
│ │ └── next.ts # Next.js-specific middleware
│ ├── mutators/
│ │ ├── bodyMutator.ts # JSON corruption logic
│ │ ├── headerMutator.ts # Header manipulation
│ │ └── zombieMode.ts # Slow stream implementation
│ └── dashboard/
│ └── template.ts # Self-contained HTML/CSS/JS
└── tests/
├── core.test.js # Core logic unit tests
├── mutators.test.js # Mutator unit tests
└── integration.test.js # End-to-end tests
Responsibilities:
- Merge user config with defaults
- Probability calculation
- Route matching logic
- Chaos action selection
- ANSI-colored logging
- State management for dashboard
Key Functions:
// Deep merge user config with sane defaults
mergeConfig(userConfig: FuzzboxConfig)
// Create mutable state object
createState(): FuzzboxState
// Check if route should be fuzzed based on patterns
shouldFuzzRoute(path, includeRoutes, excludeRoutes): boolean
// Decide if chaos should happen (probability + spike mode)
shouldInjectChaos(state: FuzzboxState): boolean
// Select which chaos action to apply
selectChaosAction(config): ChaosAction
// Check rate limiting (fake, for testing only)
checkRateLimit(state, config, clientId): boolean
// Create colorized logger
createLogger(config): LoggerDesign Choices:
-
Mutable State: Dashboard needs to modify probability/enabled at runtime. Using a shared state object instead of closures allows this.
-
Random Selection: Chaos actions are selected randomly from enabled behaviors using
Math.random(). No seedable RNG needed (this isn't for reproducible tests). -
ANSI Escape Codes: Hand-coded color codes instead of chalk dependency. Keeps bundle size minimal.
Integration Points:
- Express middleware signature:
(req, res, next) => void - Intercepts
res.send(),res.json(),res.end()for mutations - Uses async
sleep()for latency (doesn't block event loop)
Execution Flow:
1. Dashboard route check → Serve UI or API
2. Request tracking (state.requestCount++)
3. Route matching → shouldFuzzRoute()
4. Rate limit check → checkRateLimit()
5. Probability check → shouldInjectChaos()
6. Action selection → selectChaosAction()
7. Execute chaos:
- Latency → await sleep() then next()
- Error → res.status().json()
- Timeout → do nothing (hang forever)
- BodyMutation → wrap res.send()
- ZombieMode → wrap res.end()
- HeaderHavoc → wrap res.json()
8. Call next() or return response
Response Interception:
For body mutation and zombie mode, we hijack Express response methods:
const originalSend = res.send.bind(res);
res.send = function(body) {
// Mutate body here
return originalSend(mutatedBody);
};
next(); // Continue to actual handlerThis allows chaos to happen after the handler runs but before the response is sent.
Two Modes:
-
App Router Middleware (
fuzzboxNext)- Edge Runtime compatible (mostly)
- Limited streaming/header manipulation
- Uses standard
ResponseAPI
-
Pages Router Wrapper (
fuzzboxApiRoute)- Wraps individual API route handlers
- Full feature support (Node.js runtime)
- Intercepts
res.json()for mutations
Edge Runtime Limitations:
Zombie mode and advanced header manipulation don't work in Edge Runtime because:
- No access to Node.js
streammodule - Limited control over response streaming
- Can't hold connections open indefinitely
For these features, use Pages Router or deploy to Node.js runtime.
Algorithm:
mutateBody(value, fieldProbability):
if value is primitive:
if random() < fieldProbability:
return corrupted value (undefined, -999, !bool)
if value is array:
return value.map(item => mutateBody(item))
if value is object:
return { k: mutateBody(v) for k,v in object }
Recursively walks JSON structures. Each field has fieldProbability chance of corruption.
Why This Works:
Frontend code often doesn't handle:
- Missing fields (
undefinedinstead of string) - Negative numbers where positive expected
- Boolean flips breaking conditional logic
This mutation strategy exposes those bugs quickly.
Actions:
-
Delete: Remove a header entirely
- Breaks CORS if
Access-Control-Allow-Origindeleted
- Breaks CORS if
-
Scramble: Reverse header value
Content-Type: application/json→nosj/noitacilppa
-
Alter: Change header to wrong value
Content-Type: application/json→text/plain
Random Selection:
If targetHeaders is empty, picks a random header from the response. Otherwise, randomly picks from the target list.
Implementation:
class ZombieStream extends Writable {
private buffer: Buffer[];
async drip(targetStream) {
const fullBuffer = Buffer.concat(this.buffer);
for (let i = 0; i < fullBuffer.length; i++) {
targetStream.write(Buffer.from([fullBuffer[i]]));
await sleep(1000 / bytesPerSecond);
}
targetStream.end();
}
}Collects the full response in memory, then streams it one byte at a time with delays.
Trade-offs:
- Memory: Buffers entire response (bad for large payloads)
- Simplicity: Easy to implement and understand
- Effectiveness: Forces client timeout logic to trigger
For production chaos engineering (e.g., Chaos Monkey), you'd want chunked streaming without buffering. For dev testing, this is fine.
Architecture:
Single HTML file with embedded CSS and vanilla JavaScript. No build step required.
Components:
- Stats Display: Shows request count, chaos count, chaos rate
- Controls: Toggle enable/disable, probability slider, spike mode button
- State Sync: Polls
/__fuzzbox/api/stateevery 2 seconds
Why Vanilla JS:
Zero build dependencies. The dashboard can be updated without reinstalling packages or rebuilding. It's truly zero-dependency at runtime.
State Management:
let state = { enabled, probability, spikeMode, ... };
async function fetchState() {
const res = await fetch('/__fuzzbox/api/state');
state = await res.json();
updateUI();
}
async function updateState(changes) {
await fetch('/__fuzzbox/api/state', {
method: 'POST',
body: JSON.stringify(changes),
});
// Server returns updated state
}Simple polling architecture. For a production monitoring tool, you'd use WebSockets. For a dev tool, polling every 2s is fine.
HTTP Request
↓
Fuzzbox Middleware
↓
[1] Dashboard check → Return HTML/API
↓
[2] Route matching → Skip if excluded
↓
[3] Rate limit check → Return 429 if exceeded
↓
[4] Probability check → Skip if no chaos
↓
[5] Select chaos action
↓
[6] Execute chaos:
├─ Latency: await sleep() → next()
├─ Error: return error response
├─ Timeout: hang forever
├─ BodyMutation: intercept res.send() → next()
├─ ZombieMode: intercept res.end() → next()
└─ HeaderHavoc: intercept res.json() → next()
↓
next() → Your Handler
↓
Response (possibly mutated)
↓
Client
User clicks "Spike Mode"
↓
JavaScript: POST /__fuzzbox/api/state { spikeMode: true }
↓
Fuzzbox Middleware: Intercepts POST
↓
Update state.spikeMode = true
Set state.spikeModeExpiry = Date.now() + 30000
↓
Return updated state as JSON
↓
JavaScript: Update UI to show "SPIKE MODE ACTIVE"
↓
Next request: shouldInjectChaos() uses 80% probability
↓
After 30 seconds: Expiry check clears spike mode
When Chaos is NOT Triggered:
Request → shouldFuzzRoute() → shouldInjectChaos() → next()
(string comparison) (Math.random() < prob)
~0.1ms ~0.1ms
Total overhead: <0.5ms per request
When Chaos is Triggered:
| Action | Overhead |
|---|---|
| Latency | 100-3000ms (intentional delay) |
| Error | 1ms (immediate response) |
| Timeout | ∞ (intentional hang) |
| Body Mutation | 1-10ms (JSON parse/stringify) |
| Zombie Mode | Minutes (intentional slow drip) |
| Header Havoc | <1ms (string manipulation) |
| Rate Limit | <1ms (Map lookup) |
Baseline: ~1MB (middleware + state object)
Per Request:
- Normal: ~0KB (no allocations)
- Body Mutation: ~size of JSON response (cloned and mutated)
- Zombie Mode: ~size of full response (buffered)
- Dashboard: ~6KB HTML served
State Growth:
- Rate limit counter: ~100 bytes per unique client IP
- Auto-cleared after
windowMsexpires
Negligible. All chaos is either:
- Async delays (no CPU)
- String/JSON manipulation (minimal CPU)
- Random number generation (trivial)
No cryptographic operations, no complex algorithms.
Threat Model: Fuzzbox assumes:
- Developer installing it is trusted
- Environment is development/staging (not production)
- Network is private (no external attackers)
Attack Surface:
-
Dashboard: Unauthenticated by default
- Mitigation: Disable with
dashboardPath: null
- Mitigation: Disable with
-
Data Corruption: Body mutation can break auth/session tokens
- Mitigation: Use
excludeRoutesfor sensitive endpoints
- Mitigation: Use
-
DoS: Timeout/zombie mode can exhaust connections
- Mitigation: Keep
probabilitylow, disable dangerous modes
- Mitigation: Keep
Not Designed For:
- Protection against real attackers
- Production security
- Preventing abuse
Is Designed For:
- Testing frontend resilience
- Finding client-side bugs
- Chaos engineering in safe environments
-
Add config interface (
types.ts):interface NewBehaviorConfig { enabled?: boolean; setting?: number; }
-
Update default config (
core.ts):newBehavior: { enabled: true, setting: 100 }
-
Add to chaos action selector (
core.ts):if (config.behaviors.newBehavior.enabled) { enabledBehaviors.push(() => ({ type: 'newBehavior' })); }
-
Implement in adapters (
adapters/*.ts):case 'newBehavior': // Implementation break;
-
Document in API.md and update README
To support other frameworks (Fastify, Koa, etc.):
import { mergeConfig, createState, shouldFuzzRoute, ... } from 'fuzzbox/core';
export function fuzzboxFastify(userConfig) {
const config = mergeConfig(userConfig);
const state = createState();
return async (req, reply) => {
// Implement chaos logic using framework-specific APIs
};
}The core logic is framework-agnostic. Adapters just translate to framework-specific request/response handling.
Test pure functions in core.ts:
mergeConfig()- Config merging logicshouldFuzzRoute()- Route matchingshouldInjectChaos()- Probability calculationselectChaosAction()- Action selection
Test mutator functions:
mutateBody()- JSON corruptionapplyHeaderHavoc()- Header manipulationZombieStream- Slow drip behavior
Test full middleware with mock Express/Next.js apps:
- Request/response interception
- Dashboard API
- Multi-behavior chaos scenarios
TypeScript Source (src/)
↓
tsup (build tool)
↓
├─ CommonJS (dist/index.js)
├─ ESM (dist/index.mjs)
└─ Type Definitions (dist/index.d.ts)
Why tsup:
- Zero config for dual ESM/CJS builds
- Fast (uses esbuild internally)
- Handles TypeScript declarations automatically
Output Formats:
- CJS: For older Node.js projects using
require() - ESM: For modern projects using
import - TypeScript defs: For type safety in TypeScript projects
Fuzzbox follows these versioning rules:
- Patch (1.0.X): Bug fixes, no API changes
- Minor (1.X.0): New features, backward compatible
- Major (X.0.0): Breaking API changes
Given the nature of the tool (chaos), "breaking changes" are interpreted loosely. If a new chaos behavior is added and enabled by default, that's considered minor (you can disable it).
Potential future features (not implemented yet):
- Request Body Fuzzing: Mutate incoming JSON (not just responses)
- Network Simulation: Introduce packet loss, reordering
- Time Travel: Mess with Date.now() and setTimeout
- Memory Leaks: Intentionally leak memory to test monitoring
- CPU Spikes: Busy-loop to simulate high load
- Distributed Chaos: Coordinate chaos across multiple services
These would require more dependencies or OS-level access, so they conflict with the "zero dependency" principle. Possible as separate packages or opt-in features.
| Decision | Rationale |
|---|---|
| Zero dependencies | Minimize supply chain risk, reduce bundle size |
| Mutable state | Dashboard needs runtime config changes |
| Middleware pattern | Integrates naturally with Express/Next.js |
| Vanilla JS dashboard | No build step, no dependencies |
| TypeScript strict | Catch bugs at compile time |
| Random chaos | Simpler than deterministic, good enough for dev testing |
| ANSI colors | No chalk dependency, works in all terminals |
| Single HTML dashboard | Easy to maintain, self-contained |
The entire architecture optimizes for:
- Simplicity: Easy to understand and modify
- Zero dependencies: No supply chain risk
- Developer ergonomics: Works out of the box, minimal config
- Effectiveness: Actually finds real bugs in frontends
Not optimized for:
- Performance (chaos is slow by design)
- Production use (this is a dev tool)
- Extensibility (simple is better than flexible)
Built for developers who need to test resilience without learning a complex framework.