Command Line Reference
Rift provides Mountebank-compatible CLI options for easy migration.
The command is
rift. Docker, Homebrew, the release archives and the install script all put the server on yourPATHunder that name. The one exception iscargo install rift-http-proxy, which names the binary after the crate — substituterift-http-proxyforriftin every example below if you installed that way.
Basic Usage
# Start the server
rift
# With configuration file
rift --configfile imposters.json
# With custom port
rift --port 3525
Imposter Sources
--imposters loads imposters from one or more URIs instead of a single local path. Sources are merged in the order given, and POST /admin/reload re-fetches all of them.
# A local file (these three are identical)
rift --configfile mocks.json
rift --imposters file:mocks.json
rift --imposters mocks.json
# A document served over HTTP
rift --imposters https://config.example.com/imposters.json
# Several sources merged into one running set
rift --imposters file:base.json,https://config.example.com/team-overrides.json
--configfile <p> is sugar for --imposters file:<p> and behaves identically; passing both is an error. RIFT_IMPOSTERS is the environment-variable spelling.
Built-in schemes
| Scheme | Form | Version token |
|---|---|---|
file: | file:<path>, or a bare path | none — always re-read |
http: / https: | a full URL | the response ETag |
Embedders register their own schemes (this is how the Rift Cluster git+https:, s3: and registry: providers attach), so the table above is the built-in set, not the whole set.
How a scheme is recognised
Both scheme://rest and the shorter scheme:rest dispatch on their scheme, so s3://bucket/key and s3:bucket/key reach the same provider. The :// form is checked first, so a compound scheme such as git+https://… resolves to git+https rather than to git.
A URI without :// is read as scheme:rest only when what precedes the first colon is actually scheme-shaped — RFC 3986 §3.1 (ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )) and longer than one character. Everything else is a path, and paths are file::
rift --imposters s3:bucket/key # scheme `s3`
rift --imposters C:\mocks.json # a path — a drive letter is one character, so not a scheme
rift --imposters ./a:b.json # a path — does not start with a letter
rift --imposters my_source:key # a path — `_` is not an RFC 3986 scheme character
The one-character rule is a deliberate deviation from the RFC: single-letter schemes are legal there, but keeping Windows drive-letter paths working is worth more than reserving them.
A path whose leading segment is scheme-shaped (weird:path.json, or a Unix filename ending in a colon) is taken as a scheme and fails at startup with no imposter source is registered for the 'weird:' scheme, listing the schemes that are. Spell such a path file:weird:path.json — the file: prefix is stripped verbatim and the rest is opened as-is.
Merging
Every source contributes its imposters to one set. A port declared by two sources is a startup error naming both — the alternative, letting the last source win, silently drops an imposter the operator asked for. The optional intercept and routes blocks follow the same rule: at most one source may declare each.
Reload and ETag
POST /admin/reload re-fetches every source. An https: source sends If-None-Match with the ETag it last saw; a 304 Not Modified is served from cache without re-parsing, and when every source reports no change the reload returns without touching the running imposters at all:
{"message": "No source changed; imposters left as they are",
"created": 0, "replaced": 0, "stubPatched": 0, "deleted": 0}
When something did change, the existing incremental apply runs: unchanged imposters keep their recorded requests, scenario state and response cyclers; only changed ports are patched or replaced.
What a remote document may not do
A document fetched over the network is not written by someone who already has access to the machine running Rift, so two things a local --configfile may do are refused for https: sources:
<% include 'path' %>and<%- stringify('path') %>— both read a local file. Refused with an error naming the tag._rift.scriptfile:references — refused, as they are for admin-API-created imposters without--scripts-dir.
Any other tag the preprocessor does not evaluate is also refused, as it is for a local file, and a fetched document has no --no-parse.
<%= process.env.VAR %> is substituted for remote documents: environment is deployment configuration the operator supplied to their own process. Note the consequence — a remote source you do not control can read your process environment into an imposter response body. Point --imposters only at hosts you trust as much as the config file they replace.
Limits
| Limit | Value |
|---|---|
| Response body | 10 MB (enforced while reading, not from Content-Length) |
| Request timeout | 30 s |
| Redirects | followed only while the target stays http/https; at most 10 hops |
Adding a scheme
Embedders register their own schemes on the builder:
ServerBuilder::from_cli(cli)
.imposter_source(Arc::new(MyGitSource::new()))
.run()
.await
A source declares the schemes it claims via ImposterSource::schemes. Claiming a scheme that is already registered — including a built-in — is a startup error rather than a silent override. Providers return bytes; parsing goes through the same loader the built-ins use, so no scheme can grow its own dialect of the config format.
CLI Options
rift [OPTIONS]
Options:
--port <PORT> Admin API port [default: 2525]
--host <HOST> IP address to bind the admin API to (IPv4, or IPv6 bare `::1` or bracketed `[::1]`) [default: 0.0.0.0]
--configfile <FILE> Load imposters from a JSON/YAML file on startup (sugar for --imposters file:<FILE>)
--imposters <URI[,URI...]> Load imposters from one or more source URIs: file:<path>, a bare path, or https://… (see Imposter Sources below)
--datadir <DIR> Directory for persistent imposter storage
--scripts-dir <DIR> Root directory for admin-API `file:`/`ref:` script resolution; references that escape it are rejected (unset ⇒ file-backed scripts via the admin API are refused)
--allow-injection Enable JavaScript injection in responses (alias: --allowInjection)
--local-only Only accept connections from localhost (binds both the admin API and /metrics to loopback)
--require-admin-auth Refuse to start when the admin API would bind a non-loopback address with no --api-key (default: warn)
--loglevel <LEVEL> Log level: trace, debug, info, warn, error (an unrecognised value is refused) [default: info]
--runtime <MODE> Runtime topology: work-stealing (default) or per-core[=N] (RFC-712; experimental, Linux-first — macOS falls back with a warning, Windows rejects it)
--runtime-affinity Pin per-core worker threads to CPU cores (with --runtime per-core; effective on Linux)
--metrics-port <PORT> Prometheus metrics port [default: 9090]
--front-door <ADDR> Serve every imposter from one address, routed by host/path/header (see Features -> Front Door)
--ip-whitelist <IPS> Comma-separated allowed IPs (accepted for Mountebank compatibility; NOT enforced)
--mock Accepted for Mountebank compatibility; no effect — set `recordRequests: true` per imposter
--debug Enable debug mode (same as RIFT_DEBUG=1; also sets the log level to debug unless RUST_LOG is set)
--nologfile Do not write the --log file (stdout only)
--log <FILE> Also write logs to this file (off unless set)
--pidfile <FILE> Write the server's PID here (off unless set; stop/restart read ./rift.pid when omitted)
--origin <ORIGIN> Accepted for Mountebank compatibility; NOT implemented — the admin API sends no CORS headers
--api-key <TOKEN> Require this token in the Authorization header for all admin API requests
--rcfile <FILE> RC file with default flag values (a subset: port/host/loglevel/allowInjection/localOnly/requireAdminAuth/apiKey/datadir/configfile/noParse); one that cannot be read or applied aborts startup
--default-tls-cert <FILE> Default TLS certificate (PEM) for HTTPS imposters without their own
--default-tls-key <FILE> Default TLS private key (PEM), paired with --default-tls-cert
--no-self-signed-tls Disable the self-signed fallback; an HTTPS imposter with no cert is an error
--upstream-ca-file <FILE> Extra CA certificate(s) (PEM) trusted for outbound TLS (proxy stubs, --configfile URLs); appended to the OS trust store
--upstream-tls-skip-verify Accept any certificate on outbound TLS (development only; prefer --upstream-ca-file)
--intercept-port <PORT> Start the TLS-MITM intercept/redirect proxy on this port (epic #394); off when unset
--intercept-auth <USER:PASS> Require Proxy-Authorization: Basic on every CONNECT to the intercept proxy; open when unset
--intercept-ca-cert <FILE> PEM CA certificate for interception (with --intercept-ca-key); a CA is generated if omitted
--intercept-ca-key <FILE> PEM CA private key for interception (required with --intercept-ca-cert)
--intercept-ca-cert-pem <PEM> Inline PEM CA certificate for interception (with --intercept-ca-key-pem); mutually exclusive with file paths
--intercept-ca-key-pem <PEM> Inline PEM CA private key for interception (required with --intercept-ca-cert-pem)
--no-parse Disable EJS preprocessing of --configfile/file: sources; use it when a document contains a literal `<%` (alias: --noParse)
--formatter <NAME> Custom config formatter module (no-op; Rift auto-detects JSON/YAML)
--protofile <FILE> Custom protocol definitions file (no-op; custom protocols unsupported)
-h, --help Print help
-V, --version Print version
--no-parse disables EJS preprocessing of --configfile (<% include %> / <%= process.env.X %> expansion), which is otherwise applied on load. A tag the preprocessor does not evaluate fails the load, so use --no-parse when a document contains a literal <%. A <%= process.env.VAR %> whose variable is unset (and has no || 'default') renders empty and logs a WARN naming the variable and where the tag is; if the rendered document then fails to parse, the error names it too.
--formatter, --protofile, --ip-whitelist, --origin and --mock are accepted for Mountebank command-line compatibility but have no effect in Rift. Each logs a warning when given. --origin sets the admin API’s CORS origin in Mountebank; Rift’s admin API sends no CORS headers (an imposter’s own allowCORS is unrelated). Mountebank deprecated --mock; set recordRequests: true on each imposter instead.
--ip-whitelist does not filter anything
--ip-whitelist has never applied IP filtering in any release of Rift. It parses, and is otherwise ignored; passing it now logs a warning saying so. Earlier versions of this page advertised it as a way to “restrict access”, including a CIDR example — that syntax was never implemented either. If you were relying on it, you had no filtering.
This is deliberate rather than a gap waiting to be filled. Network-level access control belongs to the network, which sees the real peer: behind a proxy, load balancer or container NAT this process sees the hop, not the client, so an ACL enforced here would silently admit everyone unless it trusted X-Forwarded-For — and trusting a client-settable header for an ACL is a vulnerability, not a feature. Use a NetworkPolicy, security group or firewall.
What does work inside Rift:
| Goal | Use |
|---|---|
| Refuse connections from other hosts | --local-only (binds loopback; covers /metrics too since 0.17.0) |
| Require a credential on the admin API | --api-key <token> |
| Fail startup if the admin plane is exposed and keyless | --require-admin-auth |
GET /config reports "ipWhitelist": ["*"], which is accurate: every address may connect.
--intercept-port eagerly starts the intercept/TLS-MITM proxy at boot. It is no longer the only way to enable it: a server started without the flag still exposes the runtime lifecycle endpoints (POST/GET/DELETE /intercept), so intercept can be turned on at runtime over the admin API. The flag and the endpoints drive the same single listener.
A third way, and the only declarative one, is an intercept block in --configfile: it starts the listener with its rules already installed, so a container needs no post-boot admin call. The block and these --intercept-* flags are two spellings of one listener — supplying both is a startup error rather than a silent precedence guess, so pick one. (Each flag also has a RIFT_INTERCEPT_* environment variable, which counts as supplying it.)
The intercept proxy is unauthenticated unless you say otherwise
The listener has no credential by default — it is off unless asked for, and most uses are a loopback test rig where auth is pure friction. But it is a TLS-MITM proxy: anyone who can reach the port can route traffic through it and be served certificates forged by Rift’s CA, which are trusted wherever that CA is installed — and installing it is the whole point of the feature. On a shared or LAN-reachable host, set a credential:
rift --intercept-port 8888 --intercept-auth ci:s3cr3t
Every CONNECT must then carry Proxy-Authorization: Basic <base64(user:pass)>; anything else gets 407 Proxy Authentication Required. Standard clients do this for you — HTTPS_PROXY=http://ci:s3cr3t@host:8888, curl -x http://ci:s3cr3t@host:8888, or a JVM Authenticator. A value with no :, or with a blank half, is a startup error rather than a silently-disabled gate — as is --intercept-auth without --intercept-port, which would otherwise read as protection while guarding nothing.
On a shared host prefer RIFT_INTERCEPT_AUTH: a value passed on the command line is visible to anyone who can run ps.
Note this is Proxy-Authorization, not the admin --api-key. The two are different credentials on purpose: Proxy-Authorization is hop-by-hop and is consumed here, whereas Authorization is end-to-end and would be forwarded to every intercepted origin — sending your admin key onward to the very servers you are intercepting.
--require-admin-auth covers this listener too: a non-loopback intercept bind with no credential warns by default and refuses to start under that flag.
RC file (--rcfile)
--rcfile <FILE> reads defaults from a JSON object, as Mountebank’s --rcfile does. Only these keys are recognised (the snake_case spellings are accepted too):
| Key | Type | Sets |
|---|---|---|
port | integer 0–65535 | --port |
host | string | --host |
loglevel / logLevel | string | --loglevel |
allowInjection | boolean | --allow-injection |
localOnly | boolean | --local-only |
requireAdminAuth | boolean | --require-admin-auth |
apiKey | string | --api-key |
datadir | string | --datadir |
configfile | string | --configfile |
noParse | boolean | --no-parse |
{ "port": 3000, "apiKey": "s3cr3t", "requireAdminAuth": true, "datadir": "/data/mb" }
Rules:
- Lowest precedence. A key is applied only when the setting is still at its built-in default, so a flag or environment variable wins. The check is by value:
--port 2525on the command line does not stop an rcfileportfrom applying, because 2525 is the default. - Refused, not ignored. A file that cannot be read or parsed, is not a JSON object, or gives a recognised key the wrong type (
"localOnly": "yes","port": 70000) aborts startup, naming the file — and nothing from it is applied. AnapiKeyof the wrong type is reported by its type, never echoed. - Unknown keys are skipped with a warning on stderr.
- A blank
apiKeyis refused exactly as--api-key ""is (see below). - The file is read before
rift healthcheckcomputes its target, so the probe follows a port the rcfile sets.
API-key authentication
--api-key (or MB_APIKEY) requires every admin API request to carry the token in the Authorization header. Data-plane traffic — direct imposter ports and the /__rift/:port/... gateway — is not gated by this key.
A blank value is refused at startup rather than accepted as a key:
the admin API key (`--api-key` / `MB_APIKEY` / `apiKey`) is set to a blank value. …
--api-key "" (or MB_APIKEY= set-but-empty) would otherwise enable the auth gate and then match every unauthenticated request, leaving the admin API open while reporting as protected. Whitespace counts as blank. Omit the flag entirely to run the admin API explicitly unauthenticated; a key that merely contains spaces is still a valid key and is compared exactly as given.
rift --api-key s3cr3t
curl -H "Authorization: s3cr3t" http://localhost:2525/imposters
Unauthenticated admin plane on a public interface
--host defaults to 0.0.0.0, so a bare rift with no --api-key already serves the full admin API — which can create imposters and drive the TLS intercept proxy — on every interface with no authentication. Since 0.17.0 that posture is stated at startup instead of being silent:
WARN the admin API is bound to 0.0.0.0:2525, which is reachable from outside this host, with no
API key — anyone who can reach that address can create imposters and drive the TLS intercept
proxy. Set `--api-key <token>` (`MB_APIKEY`), or restrict the bind with `--local-only` or
`--host 127.0.0.1`. Set `--require-admin-auth` (`RIFT_REQUIRE_ADMIN_AUTH`) to make this a
startup failure instead of a warning.
The default is a warning, not a refusal: containers require 0.0.0.0 (binding loopback inside Docker makes the published port unreachable), so refusing would break the no-argument invocation and every keyless quickstart. Fleets that want fail-closed opt in:
# Refuse to start unless the admin plane is authenticated or loopback-only
rift --require-admin-auth # errors: 0.0.0.0 with no key
rift --require-admin-auth --api-key s3cr3t # ok — authenticated
rift --require-admin-auth --local-only # ok — not reachable off-host
--require-admin-auth gates on authentication, not on the address: a real --api-key satisfies it on any bind. Loopback (127.0.0.0/8, ::1) satisfies it with no key. 0.0.0.0 and :: are unspecified addresses, not loopback, so they are flagged — as is any specific off-host interface such as 10.0.0.5.
The same rule applies at every door onto the admin plane, so an embedded host gets it too: the C-ABI rift_serve_admin accepts "requireAdminAuth": true in its options, and an embedder building an AdminApiServer directly gets it from .with_require_admin_auth(true).
Default TLS for HTTPS imposters
An imposter declared with protocol: https terminates TLS. If it carries no cert/key, Rift falls back to --default-tls-cert / --default-tls-key when set, otherwise to a generated self-signed certificate. Pass --no-self-signed-tls to refuse such an imposter instead of silently self-signing: POST /imposters answers 400, and an imposter loaded at startup is skipped with an error log while the server still starts. See TLS/HTTPS Support.
rift \
--default-tls-cert ./certs/server.pem \
--default-tls-key ./certs/server-key.pem \
--no-self-signed-tls
Examples
# Start with custom port
rift --port 3525
# Load configuration and enable injection
rift --configfile imposters.json --allow-injection
# Debug logging
rift --loglevel debug
# Restrict access
rift --local-only
rift --api-key s3cr3t --require-admin-auth
# With persistent data directory (see "Data directory" below)
rift --datadir ./mb-data
# Seed from a config file and persist admin-API imposters
rift --configfile imposters.json --datadir ./mb-data
Data directory (--datadir)
--datadir persists imposters created or changed through the admin API: each one is written to <dir>/<port>.json, and deleting the imposter deletes the file. At startup (and on POST /admin/reload) every *.json file in the directory is loaded back. Other extensions are ignored.
A file is never rewritten in place. Each write goes to <port>.json.tmp beside it, is synced to disk, and is then renamed over <port>.json, so a crash, a full disk or a reload reading at the same moment sees either the old document or the new one, never a partial one. A write that fails leaves the old file as it was, and the admin call returns 503. A leftover <port>.json.tmp means a process died mid-write. It is never loaded, and the next start removes it with a WARN line. Because each write creates a new file, a mode or ownership set by hand on <port>.json, or a hard link to it, does not carry over to the next write.
The directory is keyed by port, so each file is held to that:
- It holds one imposter object (not an
{"imposters": [...]}wrapper), as plain JSON. EJS tags are not rendered in datadir files. - It must declare its port. A file with no
port, or"port": 0, is refused — it would be auto-assigned a port and written again as a second file on every load. - It must be named after that port.
4545.jsonmust declare"port": 4545; a file named anything else is refused with the name it should have.
At startup a file that breaks these rules — or that cannot be parsed, needs --allow-injection without it, or fails to create — is skipped: the server still starts, and one ERROR line lists every skipped file with its reason. POST /admin/reload is stricter: one bad file refuses the whole reload, naming the file, and the running imposters are left as they are. A missing directory is created, empty.
With both --configfile (or --imposters) and --datadir, config-file imposters are not written to the data directory, and POST /admin/reload re-reads both stores. When both declare the same port, the config-file imposter is served and the datadir file is skipped and named.
At startup, every imposter that declares a port — from either store — is created before any imposter that does not, so an auto-assigned port can never take a port another imposter asked for. "port": 0 means “auto-assign”, the same as omitting the port.
Environment Variables
A flag given on the command line wins over its environment variable, and the environment variable wins over the built-in default (and over an --rcfile value — see RC file).
| Variable | Description | Default |
|---|---|---|
MB_PORT | Admin API port | 2525 |
MB_HOST | Admin API bind IP address (IPv4, or IPv6 ::1 / [::1]) | 0.0.0.0 |
MB_CONFIGFILE | Imposter config file | |
RIFT_IMPOSTERS | Imposter source URIs (env alias of --imposters) | |
MB_DATADIR | Persistent storage directory | |
MB_ALLOW_INJECTION | Enable injection (true/false) | false |
MB_LOCAL_ONLY | Localhost only | false |
RIFT_REQUIRE_ADMIN_AUTH | Refuse to start on a keyless non-loopback admin bind (env alias of --require-admin-auth) | false |
MB_LOGLEVEL | Log level (env alias of --loglevel). Set-but-empty means info | info |
MB_APIKEY | Admin API authorization token (see --api-key) | |
RIFT_SCRIPTS_DIR | Root directory for admin-API file:/ref: script resolution (env alias of --scripts-dir); references escaping it are rejected | |
RIFT_DEBUG | Enable debug mode (truthy: 1/true/yes/on); same as --debug. Adds an x-rift-script-trace response header and makes response-template errors return a request-time error instead of an empty substitution | off |
RIFT_RUNTIME | Runtime topology (env alias of --runtime): work-stealing or per-core[=N] (RFC-712; experimental) | work-stealing |
RIFT_RUNTIME_AFFINITY | Pin per-core worker threads to CPU cores (env alias of --runtime-affinity) | off |
RIFT_METRICS_PORT | Prometheus metrics port | 9090 |
RIFT_FRONT_DOOR | Front-door bind address (env alias of --front-door): HOST:PORT or a bare port | off |
RIFT_DEFAULT_TLS_CERT | Default TLS certificate (PEM) for HTTPS imposters | |
RIFT_DEFAULT_TLS_KEY | Default TLS private key (PEM) | |
RIFT_NO_SELF_SIGNED_TLS | Disable self-signed TLS fallback (true/false) | false |
RIFT_UPSTREAM_CA_FILE | Extra CA certificate(s) (PEM file) trusted for outbound TLS — proxy stubs and --configfile URLs. Appended to the OS trust store | |
RIFT_UPSTREAM_TLS_SKIP_VERIFY | Accept any certificate on outbound TLS (true/false); development only | false |
RIFT_INTERCEPT_PORT | Start the intercept/TLS-MITM proxy on this port (epic #394) | |
RIFT_INTERCEPT_AUTH | user:pass required in Proxy-Authorization on every CONNECT to the intercept proxy (env alias of --intercept-auth); open when unset | |
RIFT_INTERCEPT_CA_CERT | PEM CA certificate file for interception (with RIFT_INTERCEPT_CA_KEY) | |
RIFT_INTERCEPT_CA_KEY | PEM CA private key file for interception | |
RIFT_INTERCEPT_CA_CERT_PEM | Inline PEM CA certificate (the bytes, not a path; with RIFT_INTERCEPT_CA_KEY_PEM) — mutually exclusive with the _CA_CERT/_CA_KEY file pair | |
RIFT_INTERCEPT_CA_KEY_PEM | Inline PEM CA private key for interception | |
RIFT_DISABLE_HTTP2 | Force HTTP/1-only listeners, disabling HTTP/2 & h2c auto-negotiation (truthy: 1/true/yes/on). On HTTPS listeners it also removes h2 from the ALPN offer, so a client offering both protocols negotiates http/1.1 instead of being handed an h2 the server will not speak. A client offering only h2 is refused at the handshake with no_application_protocol — a loud failure rather than a protocol mismatch | off |
RIFT_TCP_BACKLOG | Listen backlog for the accept loop (positive integer) | 1024 |
RIFT_TCP_NODELAY | TCP_NODELAY on accepted sockets; true/1/on enables, false/0/off disables (case-insensitive) | on |
RIFT_HTTP_MAX_BUF | Per-connection HTTP read/write buffer cap, in bytes (positive integer; floored at hyper’s 8 KB minimum). Bounds per-connection memory at high connection counts | 65536 |
RIFT_HTTP_HEADER_TIMEOUT | Seconds to wait for a client to finish sending request headers before closing the connection (slowloris hygiene; positive integer). Also bounds the HTTP/1-vs-HTTP/2 detection window at the start of a connection, so a client that completes the handshake and then sends nothing is closed rather than parked | 30 |
RIFT_MAX_CONNECTIONS | Cap on concurrently-served connections per listener (positive integer). Unset means unlimited; at the cap the server stops accepting until a connection closes, so overload waits in the kernel backlog rather than piling up. Applies to the intercept listener too, as of #1030 | unlimited |
RIFT_STRICT_BEHAVIORS | Force strict mode process-wide (truthy: 1/true/yes/on): a decorate/shellTransform/binary-base64-decode failure returns 500 instead of the lenient fallback body | off |
NO_COLOR | Suppress ANSI color and the decorative banner in rift-verify / rift-lint output | |
RUST_LOG | A full tracing filter (e.g. warn,rift::script=debug). When set it replaces --loglevel/--debug; a value that does not parse is refused at startup | unset |
RIFT_DISABLE_HTTP2 is an escape hatch for clients or intermediaries that mishandle HTTP/2; see HTTP/2 and h2c. RIFT_TCP_BACKLOG and RIFT_TCP_NODELAY are socket-tuning knobs covered under Performance → Runtime socket tuning.
The five tuning knobs above (RIFT_TCP_BACKLOG, RIFT_TCP_NODELAY, RIFT_HTTP_MAX_BUF, RIFT_HTTP_HEADER_TIMEOUT, RIFT_MAX_CONNECTIONS) fall back to their defaults when unset. A value that is set but cannot be used — not a number, out of range, or an unrecognised boolean spelling — also falls back, but logs a WARN naming the variable, the value and the reason, so a typo is visible in the log rather than only in behaviour that does not match the configuration. RIFT_MAX_CONNECTIONS=0 is the one exception: it reads as “no cap”, which is what it does, so it is accepted silently.
RIFT_HTTP_HEADER_TIMEOUT is applied in three places on a new connection: to the HTTP/1-vs-HTTP/2 detection window; then, if the connection resolves to HTTP/1, by HTTP/1’s own header timer, which can only start once detection has resolved; and, if it resolves to HTTP/2, as both the keep-alive ping interval and the pong timeout. So the worst case for a client that goes silent at exactly the wrong moment is up to two header timeouts before the connection is closed, not one — on either protocol. The phases are not netted against each other deliberately: subtracting elapsed time would make a very small RIFT_HTTP_HEADER_TIMEOUT behave erratically, and bounding each phase separately is easier to reason about than a shared budget.
The HTTP/2 leg needs its own bound because HTTP/2 has no equivalent of HTTP/1’s header timer: once the preface is read, a peer that never opens a stream is not waiting on a request head that a timer could bound. The keep-alive ping is the only mechanism that reaches it. A consequence worth knowing about: idle HTTP/2 connections are now pinged every RIFT_HTTP_HEADER_TIMEOUT (30s by default). A live client answers and is unaffected; the traffic is two frames per interval per idle connection.
RIFT_STRICT_BEHAVIORS and the per-imposter strictBehaviors field combine with OR — either being set enables strict mode. See Rift Extensions → Strict Behaviors for the full semantics.
Docker Example
docker run \
-e MB_PORT=2525 \
-e MB_ALLOW_INJECTION=true \
-e RUST_LOG=debug \
-p 2525:2525 \
-p 9090:9090 \
zainalpour/rift-proxy:latest
Docker Compose Example
version: '3.8'
services:
rift:
image: zainalpour/rift-proxy:latest
ports:
- "2525:2525"
- "4545:4545"
- "9090:9090"
environment:
- MB_PORT=2525
- MB_ALLOW_INJECTION=true
- RUST_LOG=info
volumes:
- ./imposters.json:/imposters.json
command: ["--configfile", "/imposters.json"]
Logging Configuration
Log Levels
# Via CLI
rift --loglevel debug
# Via environment
MB_LOGLEVEL=debug rift
| Level | Description |
|---|---|
error | Only errors |
warn | Warnings and errors (warning is also accepted) |
info | Standard operation (default) |
debug | Detailed debugging |
trace | Very verbose (development) |
Levels are case-insensitive. Any other value is refused at startup with an error listing the accepted ones — it used to fall back to info silently. An empty value (for example MB_LOGLEVEL=${LOG_LEVEL} with LOG_LEVEL unset) means info.
The filter actually used is chosen in this order:
RUST_LOG, when it is set — a fulltracingfilter. A value that does not parse, or is not valid UTF-8, is refused at startup rather than ignored. (RUST_LOG=set to the empty string is accepted.)--debug, which meansdebug.--loglevel/MB_LOGLEVEL/ the rcfileloglevelkey.info.
--loglevel is validated even when RUST_LOG overrides it, so a typo fails the same way in every environment.
Module-Specific Logging
RUST_LOG targets are matched by prefix. Rift’s own log lines use the crate module paths (rift_http_proxy::…, rift_mock_core::…) plus the explicit targets rift::script, rift::template and rift::state_ops:
# Debug everything Rift logs (the prefix `rift` covers all of the above)
RUST_LOG=rift=debug rift
# Debug script execution only
RUST_LOG=info,rift::script=debug rift
# Debug imposter handling in the engine, admin API at info
RUST_LOG=info,rift_mock_core::imposter=debug rift
Log lines are plain text on stdout (the tracing default format); --log <FILE> writes the same lines to a file as well.
Health Check
Rift provides health endpoints:
# Admin API health — answers {"status":"ok"}
curl http://localhost:2525/health
# Metrics endpoint (separate listener)
curl http://localhost:9090/metrics
With --api-key set, every admin API path — /health and / included — requires the Authorization header and answers 401 without it. The metrics listener on --metrics-port is not gated by the key.
Signal Handling
SIGTERM and SIGINT (Ctrl+C) shut the server down gracefully (issue #1155):
- it stops accepting on the admin API, the metrics listener and the front door, and gives connections already in flight on them a short, bounded grace — about three seconds at worst;
- imposter connections are closed rather than drained — a mock server has no in-flight work worth holding a shutdown for, which is also what Mountebank does;
- persisted imposters in
--datadirare left exactly as they are, so the next start serves them; - the
--pidfileit wrote is removed, and the--logfile is flushed; - the process exits
0.
kill -TERM $(pidof rift)
This holds for a rift that is a container’s PID 1, too, so docker stop and a Kubernetes pod termination are prompt, and no init process (docker run --init, init: true) is needed.
Exit Codes
| Code | Meaning |
|---|---|
0 | Success |
1 | Any runtime failure: a refused flag value, config file or rcfile, a port that cannot be bound, a failed stop/save, an unhealthy healthcheck |
2 | The command line itself could not be parsed (unknown flag, missing value, conflicting flags) |
The error is printed to stderr in every case; the exit code does not distinguish the cause further. A server stopped by SIGTERM or SIGINT exits 0: that is a clean shutdown, not a failure.
Subcommands
Rift supports several subcommands for server management:
start
Start the Rift server (default behavior when no subcommand is specified):
rift start
rift start --port 3525 --configfile imposters.json
stop
Stop a running Rift server using its PID file. A server only writes a PID file when it was started with --pidfile, so start it that way if you intend to use stop/restart:
rift --pidfile /var/run/rift.pid &
# Stop using that PID file
rift stop --pidfile /var/run/rift.pid
# Without --pidfile, stop reads ./rift.pid
rift stop
stop sends SIGTERM (taskkill /F on Windows) and waits for the process to exit before it returns — up to 5 seconds, above the server’s own shutdown bound — then removes the PID file (the server usually removes it first, which is fine). A process still running after that is reported as an error and its PID file is kept, rather than being assumed gone. A PID file whose process is already gone is treated as stale: it is removed and stop succeeds. A missing PID file, a non-positive PID, or a process rift is not permitted to signal is an error, and the file is left in place.
restart
Stop the server named by the PID file, then start a new one in this process with the other flags given:
rift restart --pidfile /var/run/rift.pid --configfile imposters.json
A missing PID file is not an error for restart — there is nothing to stop, so it just starts.
save
Save the running server’s imposters to a file for later replay. The server is found through --host/--port (so MB_HOST/MB_PORT and --rcfile apply):
# Save imposters to ./mb.json (the default, as in Mountebank)
rift save
# Save to a named file
rift save --savefile recorded.json
# Drop `proxy` responses, keeping what they recorded (stubs left with no response are dropped)
rift save --savefile mocks.json --remove-proxies
| Flag | Description | Default |
|---|---|---|
--savefile <FILE> | Output file | mb.json |
--remove-proxies | Request the removeProxies=true view | off |
A non-2xx answer (for example 401 from a server started with --api-key — save sends no key) fails the command instead of writing the error body to the file.
replay
Start a server with the saved imposters loaded:
rift replay --configfile recorded.json
This is rift --configfile recorded.json under another name: it starts a new server rather than switching a running Mountebank-style server from recording to replaying.
script
Validate and run _rift.script scripts outside a running server (no admin API, no imposter) — the authoring loop from Scripting. Two actions:
rift script check <target> — statically validate a raw script file (.rhai/.js) or a config file (JSON/YAML) with _rift.script entries: engine syntax, entrypoint presence/arity for the intended hook, and (for a config) state-used-without-flowState. Exits non-zero on any error — so a script whose entrypoint is misnamed fails here instead of at request time.
rift script check scripts/fail-twice.rhai
rift script check scripts/rate-limit.js --hook respond
rift script check imposters.yaml # every _rift.script in the config
| Flag | Description | Default |
|---|---|---|
--hook <HOOK> | Entrypoint to check a raw script against. Only respond is dispatched at request time, so any other value is rejected — for a config target too, where the flag is redundant because every _rift.script entry is respond-position | respond |
--no-parse | Load a config target verbatim, skipping EJS preprocessing, as rift --no-parse does — for a config that contains a literal <%. No effect on a raw script (alias: --noParse) | off |
rift script run <target> — execute a script against a fixture request and seeded flow state, printing the decision, the mutated flow state, captured ctx.logger output, and the execution duration. No server runs.
rift script run scripts/fail-twice.rhai --state attempts=2
rift script run scripts/echo.js --request fixtures/get-resource.json --flow-id t1
| Flag | Description | Default |
|---|---|---|
--request <FILE> | JSON file with the request-object shape scripts see ({method, path, headers, query, pathParams, body}; all fields optional). headers holds one entry per name — a name spelled twice (X-Trace and x-trace) is rejected | empty GET / |
--state <KEY=VALUE> | Seed flow state before running (repeatable); the value is parsed as JSON when it parses, else stored as a string | |
--flow-id <ID> | Flow id the seeded state and the script’s ctx.state/ctx.store calls use | cli |
--engine <ENGINE> | Script engine (rhai/js); inferred from the file extension when omitted | (from extension) |
--hook <HOOK> | Entrypoint to run. Only respond is wired across both engines, so any other value is rejected | respond |
healthcheck
Probe a running server’s admin API and exit 0 when it answers 2xx, 1 otherwise. This is what the container images run as their HEALTHCHECK — the probe is built into the binary so the image needs no shell and no curl, which is what lets the -static image be FROM scratch (see Docker).
With no arguments it probes /health on the admin API, reading --host/--port (and therefore MB_HOST/MB_PORT, and an --rcfile that sets them) exactly as the server does — so inside a container rift healthcheck needs no configuration. A bind-any host (0.0.0.0, ::) is probed on loopback, since that is where a server bound to every interface answers.
rift healthcheck # probes http://127.0.0.1:2525/health
MB_PORT=3000 rift healthcheck # follows MB_PORT
rift --rcfile /etc/rift/rc.json healthcheck # follows the port that file sets
rift healthcheck --url http://localhost:9090/metrics # probe something else
Pass the same --rcfile the server was started with, or the probe knocks on the default port (issue #1133). An rcfile the server would refuse (a missing file, a wrong-typed key) refuses the probe too, and reports unhealthy: a server started with that file would not have started either.
A keyed server needs no extra configuration (issue #1154). The probe presents the admin API key it already has — MB_APIKEY, --api-key, or an rcfile apiKey, all read exactly as the server reads them — so a container that sets MB_APIKEY keeps its built-in HEALTHCHECK healthy with no change to the image or the command. There is deliberately no key flag on the subcommand: it would duplicate the top-level one and invite putting a secret in argv, where docker inspect shows it.
The key goes only to the URL derived from --host/--port. An explicit --url is an arbitrary target — pointing it at the unauthenticated metrics listener is the common use — and attaching the admin secret to it would leak the key, so an explicit --url is never sent one. If you point --url at the admin API of a keyed server it gets 401 and reports unhealthy; drop --url instead. A 401 with no key configured says which setting to add.
| Flag | Description | Default |
|---|---|---|
--url <URL> | URL to probe instead of the admin API’s /health | (from --host/--port) |
--timeout <SECONDS> | Give up and report unhealthy after this long. Kept under the images’ HEALTHCHECK --timeout=3s so a hung server makes the probe report the verdict itself instead of being killed mid-probe | 2 |
Additional CLI Tools
Rift includes additional CLI tools for working with imposters:
rift-verify
Test imposters by making requests and verifying responses.
rift-verify [OPTIONS]
Options:
-a, --admin-url <URL> Rift admin API URL [default: http://localhost:2525]
-p, --port <PORT> Verify specific imposter port only
-c, --show-curl Show curl commands for each test
-v, --verbose Verbose output with pass/fail details
-t, --timeout <SECS> Request timeout in seconds [default: 10]
-o, --output <FMT> Output format: text (default), json
--dry-run Show what would be tested without making requests
--skip-dynamic Skip stubs with inject/proxy/script responses
--verify-dynamic Opt-in: assert dynamic stubs instead of skipping them
--status-only Only verify status codes (ignore body/headers)
--gateway Send requests through the admin port's /__rift/<port>/ gateway
--insecure Accept self-signed/invalid TLS certificates (https imposters)
--space <SPACE> Correlation value sent to imposters whose flowIdSource is a header [default: rift-verify]
--flow-id-header <FLOW_ID_HEADER> Fallback correlation header name when it cannot be discovered
--demo Run demo showing enhanced error output
-h, --help Print help
-V, --version Print version
Examples:
# Verify all imposters
rift-verify
# Verify specific imposter with curl commands
rift-verify --port 4545 --show-curl
# Dry run to see test plan
rift-verify --dry-run --verbose
# Skip dynamic stubs (proxy, inject, script)
rift-verify --skip-dynamic
# Assert dynamic stubs instead of skipping them
rift-verify --verify-dynamic
# Status-only mode for cycling responses
rift-verify --status-only
# Machine-readable summary for CI (JSON on stdout, progress on stderr)
rift-verify -o json
With -o json, rift-verify writes a single summary object to stdout — { "imposters", "stubs", "tests", "passed", "failed", "skipped" } — and routes all progress and banner output to stderr, so it pipes cleanly into other tools. Color and the decorative banner are also suppressed automatically when stdout is not a TTY (piped) or when NO_COLOR is set.
By default, rift-verify SKIPs stubs whose response is dynamic (proxy/inject/script/cycling/faults, and the repeat/decorate/copy/lookup/shellTransform behaviors unless null or an empty list) because their output isn’t a static function of the stub — --skip-dynamic makes that skip explicit. --verify-dynamic is the opt-in complement: it asserts those stubs instead of skipping them, using three mechanisms — an embedded mock upstream for proxy stubs (verifying the proxied response and, when predicateGenerators is set, the recorded-stub prepend); a _verify expectation sequence (see below) run against a freshly recreated imposter for inject/script/decorate/cycling/stateful stubs; and deterministic (probability: 1.0 or unset) _rift.fault assertions for latency/error/tcp faults. Each check runs against a throwaway imposter that is torn down afterward, so it never mutates the imposters under test. A dynamic stub with none of these assertable markers is still surfaced as a visible SKIP in the output rather than silently ignored.
See Stub Analysis for details, including the _verify annotation schema.
rift-lint
Validate imposter configuration files before loading.
rift-lint <path> [OPTIONS]
Arguments:
<path> Path to imposter file or directory (required)
Options:
-f, --fix Fix issues automatically where possible
-o, --output <FMT> Output format: text (default), json
-e, --errors-only Only show errors (hide warnings)
-s, --strict Strict mode - treat warnings as errors
--no-parse Lint files verbatim, without rendering EJS tags, as `rift --no-parse` loads them (alias: --noParse)
-h, --help Print help
-V, --version Print version
Examples:
# Lint all imposters in directory
rift-lint ./imposters/
# Strict mode for CI/CD (exits 1 on warnings)
rift-lint ./imposters/ --strict
# JSON output for tooling integration
rift-lint ./imposters/ --output json
# Auto-fix header type issues
rift-lint ./imposters/ --fix
# Only show errors, hide warnings
rift-lint ./imposters/ --errors-only
See Configuration Linting for details.