Part of the fetch-kit ecosystem - production-ready fetch utilities and chaos-testing tools.
chaos-proxy-go is a Go port of fetch-kit/chaos-proxy: a proxy server for injecting configurable network chaos (latency, failures, connection drops, rate-limiting, etc.) into any HTTP or HTTPS traffic. Use it via CLI or programmatically to apply ordered middleware (global and per-route) and forward requests to your target server, preserving method, path, headers, query, and body.
-
Download from GitHub Releases or build from source:
go build -o chaos-proxy-go . -
Create a minimal
chaos.yaml:target: "http://localhost:4000" port: 5000 global: - failRandomly: rate: 0.1 status: 503
-
Run the proxy:
./chaos-proxy-go --config chaos.yaml --verbose
All traffic to http://localhost:5000 is now forwarded to http://localhost:4000 with 10% random 503 failures injected.
- Middleware reference — all 11 built-in primitives with config tables
- Observability — OpenTelemetry tracing, span attributes, connecting to a collector
- Hot reload — runtime config reload, endpoint spec, edge cases
Ready-made scenarios in the presets/ folder:
| Preset | Simulates |
|---|---|
flaky-backend.yaml |
Unstable upstream: latency jitter, 5% 503s, 2% connection drops |
mobile-3g.yaml |
Mobile 3G: 100–300ms latency, 50 KB/s bandwidth, 1% drops |
burst-errors.yaml |
Error bursts: every 5th fails with 500, plus 10% random 503s |
timeout-storm.yaml |
Timeout storm: 1–8s delays, 10% drops, 15% instant 504s |
Run a preset directly:
./chaos-proxy-go --config presets/flaky-backend.yaml- Simple configuration via a single
chaos.yamlfile - Programmatic API and CLI usage
- Built-in middleware primitives: latency, latencyRange, fail, failRandomly, failFirstN, failNth, dropConnection, rateLimit, cors, throttle, headerTransform, bodyTransformJSON
- Extensible registry for custom middleware
- Supports both request and response interception/modification
- Method+path route support (e.g.,
GET /api/users) - Robust short-circuiting: middlewares halt further processing when sending a response or dropping a connection
- Runtime config reload via
POST /reloadwithout process restart
Download the latest release from GitHub Releases or build from source:
go build -o chaos-proxy-go .Requires Go 1.21 or later.
./chaos-proxy-go --config chaos.yaml [--verbose]--config <path>: YAML config file (default./chaos.yaml)--verbose: emit structuredkey=valueobservability logs for startup, each request (verbose.request.begin/verbose.request.end), config reloads, proxy errors, and shutdown. Sensitive query-string values (e.g.token,secret,password,api_key) are redacted and control characters are stripped.INFO/DEBUGlines go to stdout;WARN/ERRORto stderr.
import (
"log"
"chaos-proxy-go/internal/config"
"chaos-proxy-go/internal/proxy"
)
cfg, err := config.Load("chaos.yaml")
if err != nil {
log.Fatalf("failed to load config: %v", err)
}
server, err := proxy.New(cfg, false)
if err != nil {
log.Fatalf("failed to create server: %v", err)
}
if err := server.Start(); err != nil {
log.Fatalf("server error: %v", err)
}Chaos Proxy supports full runtime reloads without process restart.
- Endpoint:
POST /reload - Content-Type:
application/json - Payload: full config snapshot (same shape as
chaos.yaml, but JSON) - Behavior: build-then-swap — all-or-nothing, the active state is never partially updated
- Body size limit: 1 MB
curl -X POST http://localhost:5000/reload \
-H "Content-Type: application/json" \
-d '{
"target": "http://localhost:4000",
"port": 5000,
"global": [
{ "latency": { "ms": 120 } },
{ "failRandomly": { "rate": 0.05, "status": 503 } }
],
"routes": {
"GET /users/:id": [
{ "failNth": { "n": 3, "status": 500 } }
]
}
}'{
"ok": true,
"version": 2,
"reload_ms": 3
}| Status | Reason |
|---|---|
400 |
Invalid or unparseable config (active state is unchanged) |
409 |
Reload already in progress |
415 |
Wrong Content-Type (must be application/json) |
{
"ok": false,
"error": "target is required",
"version": 1,
"reload_ms": 0
}proxy.New(...) returns a *Server with a ReloadConfig method:
result := server.ReloadConfig(newCfg)
if !result.OK {
log.Printf("reload failed: %s", result.Error)
} else {
log.Printf("reloaded to version %d in %dms", result.Version, result.ReloadMs)
}- In-flight requests are deterministic: they run on the snapshot captured at the moment the request arrived, immune to concurrent reloads.
- New requests after a successful swap immediately use the new snapshot.
- All-or-nothing: if parse, validate, or middleware-build fails, the active state is unchanged.
- Middleware state resets on reload (e.g., rate-limit and failNth counters start fresh).
- Concurrent reloads are rejected with
409; the second caller must retry.
target: "http://localhost:4000" # required
port: 5000 # default: 5000
# Optional: OpenTelemetry tracing
otel:
serviceName: "my-service" # required if otel is set
endpoint: "http://localhost:4318" # required if otel is set
flushIntervalMs: 5000 # default
maxBatchSize: 100 # default
maxQueueSize: 1000 # default
headers: # optional OTLP request headers
x-api-key: "secret"
# Global middleware — applied to every proxied request
global:
- latencyRange:
minMs: 20
maxMs: 100
- failRandomly:
rate: 0.05
status: 503
# Route-specific middleware — "METHOD /path" format
routes:
GET /api/users:
- latency:
ms: 500
POST /api/orders:
- failNth:
n: 3
status: 500See docs/middlewares.md for the full middleware reference and docs/observability.md for otel options.
latency(ms)— delay every requestlatencyRange(minMs, maxMs, seed?)— random delay (deterministic whenseedis set)fail({ status, body })— always failfailRandomly({ rate, status, body, seed? })— fail with probability (deterministic whenseedis set)failNth({ n, status, body })— fail every nth requestdropConnection({ prob, seed? })— randomly drop connection (probdefaults to1.0if omitted; deterministic whenseedis set)rateLimit({ limit, windowMs, key })— rate limiting (by header key if configured, otherwise by client remote address in ip:port format)cors({ origin, methods, headers })— enable and configure CORS headersthrottle({ rate, chunkSize, burst })— throttles bandwidth per request (rateis bytes/second)headerTransform({ request: { set, delete }, response: { set, delete } })— mutate request/response headersbodyTransformJSON({ request: { set, delete }, response: { set, delete } })— mutate JSON request/response bodies
Register custom middleware in Go. See the internal/middleware package for examples.
Run the deterministic test suite, static checks, and dependency scan:
go test ./...
go vet ./...
golangci-lint run
govulncheck ./...The repository includes native Go fuzz targets for configuration, middleware state, routing, proxy bodies, reloads, and telemetry. The normal test command runs every target's committed seed corpus, so regressions remain part of regular CI:
go test ./...Coverage-guided input generation runs one target at a time. List the available targets, then select a package and target with a bounded duration:
go test ./... -list '^Fuzz'
go test ./internal/config -run '^$' -fuzz '^FuzzParseJSONConfiguration$' -fuzztime=30s- Proxy forwards all headers; be careful with sensitive tokens.
- Intended for local/dev/test only.
- HTTPS pass-through requires TLS termination; not supported out-of-the-box.
- Not intended for stress testing; connection limits apply.
- Middleware execution order is nondeterministic when multiple middlewares are in the same YAML map element. For example:
For deterministic order, use separate map elements:
global: - latency: { ms: 100 } fail: { status: 500 } # Order vs latency is not guaranteed
global: - latency: { ms: 100 } - fail: { status: 500 } # Always runs after latency
MIT
Go port of fetch-kit/chaos-proxy.
This is a Go port of fetch-kit/chaos-proxy.