| Crates.io | lowdown |
| lib.rs | lowdown |
| version | 0.1.0 |
| created_at | 2025-11-16 23:28:28.840417+00 |
| updated_at | 2025-11-16 23:28:28.840417+00 |
| description | An unobtrusive reverse HTTP proxy that injects faults between a client and backend service. |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1936010 |
| size | 131,013 |
This is a Rust reimplementation (inspired by
ivarref/mikkmokk-proxy) of an
unobtrusive reverse HTTP proxy that can inject faults between a client and a
backend service.
You can use it to explore and harden the resiliency of clients and backends by simulating:
All behavior is controlled through HTTP headers, environment variables, and a small admin API.
Note: this project is a clean-room rewrite in Rust, inspired by the original Clojure implementation and its behavior / docs. The original project is licensed under the Eclipse Public License 2.0; if you copy or combine code between the two, ensure you comply with that license.
cargo run --release
By default this starts:
127.0.0.1:8080127.0.0.1:7070You must either:
set a default destination URL via environment:
export DESTINATION_URL=http://example.com
cargo run --release
or use the path-based forwarding endpoints
(/lowdown-forward-http/..., see below).
Build:
docker build -t lowdown .
Run (simple example, proxying to http://example.com):
docker run --rm --name lowdown \
-e DESTINATION_URL=http://example.com \
-e PROXY_BIND=0.0.0.0 \
-e PROXY_PORT=8080 \
-e ADMIN_BIND=0.0.0.0 \
-e ADMIN_PORT=7070 \
-p 8080:8080 \
-p 7070:7070 \
lowdown
Now:
http://localhost:8080http://localhost:7070 (admin API)High-level call flow:
fail-before)fail-after)Fault injection is probabilistic. Each *-percentage setting is interpreted as
the percentage chance in [0, 100] that the corresponding behavior activates
for a matching request.
There are three layers of configuration, applied in this order:
x-lowdown-* headers)At request time, a snapshot of the effective settings is built by merging these layers. Additionally, one-off rules can consume themselves the first time a matching request is seen (see below).
These are the built-in defaults (before env/admin/headers are applied):
| Setting key | Default |
|---|---|
delay-after-ms |
0 |
delay-after-percentage |
0 |
delay-before-ms |
0 |
delay-before-percentage |
0 |
destination-url |
nil |
duplicate-percentage |
0 |
fail-after-code |
502 |
fail-after-percentage |
0 |
fail-before-code |
503 |
fail-before-percentage |
0 |
match-header-name |
* |
match-header-value |
* |
match-host |
* |
match-method |
* |
match-uri |
* |
match-uri-regex |
* |
match-uri-starts-with |
* |
Semantics:
* means "match everything".destination-url of nil means "no default backend"; you must provide one
via env, admin update, or per-request header.x-lowdown-*)When sending a request through the proxy, you can control its behavior using headers:
x-lowdown-<setting-name><setting-name> is one of the keys above (e.g. fail-before-percentage)Examples:
Always fail before reaching the backend:
curl -v \
-H 'x-lowdown-destination-url: http://example.com' \
-H 'x-lowdown-fail-before-percentage: 100' \
http://localhost:8080/
Inject a fixed delay before calling the backend:
curl -v \
-H 'x-lowdown-destination-url: http://example.com' \
-H 'x-lowdown-delay-before-percentage: 100' \
-H 'x-lowdown-delay-before-ms: 3000' \
http://localhost:8080/
Send duplicate requests:
curl -v \
-H 'x-lowdown-destination-url: http://example.com' \
-H 'x-lowdown-duplicate-percentage: 100' \
http://localhost:8080/
Fault injection only applies if the request "matches" according to the following settings (after merging env/admin/header/one-off layers):
match-uri: exact match with the request path (e.g. /foo/bar)match-uri-starts-with: prefix match on the request pathmatch-uri-regex: full regex match against the request path,
e.g. /api/uuid/([a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12})match-method: HTTP method (e.g. GET, POST), case-insensitivematch-host: backend host name (e.g. example.org), matched against
the destination's host portionmatch-header-name / match-header-value:
*, all requests matchmatch-header-name and whose value equals match-header-valueOnly if all matchers succeed will any *-percentage settings be considered.
For each percentage field (e.g. fail-before-percentage), when a request
matches:
[0, 99] is drawnpercentage > random_value, the behavior is triggeredThis is intentionally equivalent to "percentage chance out of 100".
Each setting key can also be provided via an environment variable:
For example:
destination-url → DESTINATION_URLfail-before-percentage → FAIL_BEFORE_PERCENTAGEmatch-uri-starts-with → MATCH_URI_STARTS_WITHThese environment defaults are merged on top of the built-in defaults and beneath admin/headers/one-off overrides.
Special non-behavior env vars:
PROXY_BIND: IP/host to bind the proxy server (default 127.0.0.1)PROXY_PORT: proxy port (default 8080)ADMIN_BIND: IP/host to bind the admin server (default 127.0.0.1)ADMIN_PORT: admin port (default 7070)LOWDOWN_DEVELOPMENT: if set to true, JSON responses include a trailing
newline to make terminal output nicerTZ: timezone for timestamps in logs (e.g. Europe/Oslo), depends on
system supportYou do not need a dedicated instance per backend. Instead, you can route to arbitrary hosts using special path prefixes:
GET /lowdown-forward-http/{host} → forwards to http://{host}/GET /lowdown-forward-http/{host}/{path...} → forwards to
http://{host}/{path...}GET /lowdown-forward-https/{host}/{path...} → forwards to
https://{host}/{path...}Examples:
# Plain HTTP
curl http://localhost:8080/lowdown-forward-http/example.org/
# HTTPS with path
curl http://localhost:8080/lowdown-forward-https/example.org/api/health
Internally, the proxy converts these paths into a x-lowdown-destination-url
header and a normalized request URI, so they behave exactly like explicit
x-lowdown-destination-url usage.
When forwarding to the backend, the proxy adjusts:
Host header:
Origin header:
scheme://host[:port] of the destinationWhen returning the backend's response, if the backend sets
Access-Control-Allow-Origin and the client sent an Origin, the proxy
rewrites Access-Control-Allow-Origin to match the client's original Origin.
This matches the behavior of the original Clojure implementation and helps with CORS-sensitive frontends.
The admin API runs on the ADMIN_BIND:ADMIN_PORT address (default
127.0.0.1:7070). It provides:
POST /api/v1/updateMerge new defaults into the current admin settings, using the same
x-lowdown-* header schema.
Example:
curl -XPOST \
-H 'x-lowdown-fail-before-percentage: 20' \
-H 'x-lowdown-destination-url: http://example.com' \
http://localhost:7070/api/v1/update
Returns the full effective settings (default + env + admin) as JSON.
POST /api/v1/resetReset admin settings to an empty override layer, optionally seeding new values from headers in this request.
Example:
curl -XPOST http://localhost:7070/api/v1/reset
Response is the same shape as /api/v1/update.
GET /api/v1/listReturn the current admin override layer as JSON (merged with defaults/env).
curl http://localhost:7070/api/v1/list
POST /api/v1/one-offCreate a one-off rule: a settings snapshot that will be applied to the next matching request only, then discarded.
Example: fail the next request before reaching the backend:
curl -XPOST \
-H 'x-lowdown-fail-before-percentage: 100' \
http://localhost:7070/api/v1/one-off
fail-before.Matching uses the same match-* semantics as regular requests, and
destination-url inside the one-off is derived from the current effective
settings at the time the rule is consumed.
POST /api/v1/list-headersLog all incoming headers (splitting x-lowdown-* and non-lowdown headers)
and return a JSON array of header names. This is useful for introspecting what
your gateway or client is actually sending.
curl -XPOST -H 'X-Foo: Bar' http://localhost:7070/api/v1/list-headers
GET / → {"service":"lowdown"}GET /health and GET /healthcheck → {"service":"lowdown","status":"healthy"}These are primarily for simple health and discovery checks.
Logging is handled via tracing and tracing-subscriber.
Configure via RUST_LOG, e.g.:
RUST_LOG=info,lowdown=debug
You will see logs for:
before-delay, delay-after)/api/v1/list-headersIf TZ is set appropriately in the container/host, timestamps will respect the
requested timezone (subject to OS support).
Build a release binary:
cargo build --release
Run tests:
cargo test
Tests are written as integration-style tests around the axum routers with a
stub HttpClient, so they do not require external services. They verify:
update/reset plumbingThese limitations mirror the original project:
*-percentage should be in [0, 100]*-code should be a valid HTTP status code ([200, 600))ivarref/mikkmokk-proxy