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 your PATH under that name. The one exception is cargo install rift-http-proxy, which names the binary after the crate — substitute rift-http-proxy for rift in 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.script file: 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 2525 on the command line does not stop an rcfile port from 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. An apiKey of the wrong type is reported by its type, never echoed.
  • Unknown keys are skipped with a warning on stderr.
  • A blank apiKey is refused exactly as --api-key "" is (see below).
  • The file is read before rift healthcheck computes 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.json must 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:

  1. RUST_LOG, when it is set — a full tracing filter. 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.)
  2. --debug, which means debug.
  3. --loglevel / MB_LOGLEVEL / the rcfile loglevel key.
  4. 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 --datadir are left exactly as they are, so the next start serves them;
  • the --pidfile it wrote is removed, and the --log file 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-keysave 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.