Embeddable Server
The rift binary is a thin wrapper around library entry points in rift-http-proxy. A Rust host can run the same server in-process — optionally around its own ImposterManager — and bind the admin and metrics planes to addresses of its choosing.
The server composition lives in the rift_http_proxy::server module (Cli, Commands, ServerBuilder, RunningServer, admin_bind_addr, run_metrics_server, bind_metrics_server, RunningMetrics); none of these are re-exported at the crate root. The crate root re-exports the rift-mock-core modules plus TcpFaultKind, tcp_fault_carrier, default_flow_store_backends and install_default_crypto_provider.
ServerBuilder
ServerBuilder composes the standard admin API, imposter listeners, and (optionally) the metrics server, then serves them.
use rift_http_proxy::server::{Cli, ServerBuilder};
use clap::Parser;
// Build from parsed CLI options (same flags as the `rift` binary):
let builder = ServerBuilder::from_cli(Cli::parse());
| Method | Signature | Purpose |
|---|---|---|
from_cli | fn from_cli(cli: Cli) -> Self | Seed the builder from CLI options (port, host, configfile, datadir, TLS defaults, metrics port, …). |
manager | fn manager(self, manager: Arc<ImposterManager>) -> Self | The embedding seam — inject a pre-built ImposterManager (e.g. one wired with custom SPI backends) instead of letting the builder construct the default one. Skips internal construction, including --datadir write-through and TLS defaults. |
imposter_source | fn imposter_source(self, source: Arc<dyn ImposterSource>) -> Self | Register an --imposters URI scheme (repeatable). file:/https: are built in; claiming a scheme already taken is a startup error. |
accept_runtimes | fn accept_runtimes(self, runtimes: Vec<tokio::runtime::Handle>) -> Self | Fan imposter accept loops out across per-core runtimes (issue #745). Applies only to the builder-constructed manager; an injected one uses ImposterManager::with_accept_runtimes. Empty keeps the single-listener topology. |
admin_authorizer | fn admin_authorizer(self, authorizer: Arc<dyn AdminAuthorizer>) -> Self | Per-request admin authorization hook (issue #854) — see AdminAuthorizer. |
reported_admin_port | fn reported_admin_port(self, port: u16) -> Self | The port GET /config reports, when the admin plane binds somewhere other than where clients reach it (issue #1135). See AdminApiServer::with_reported_admin_port below. |
run | async fn run(self) -> anyhow::Result<()> | Load configs, bind, and serve forever (returns only on error/shutdown). |
start | async fn start(self) -> anyhow::Result<RunningServer> | Same, but returns a RunningServer handle once bound — supports ephemeral (:0) ports and programmatic shutdown. |
RunningServer
Returned by start(); lets the host discover bound addresses and control lifecycle.
| Method | Signature | Purpose |
|---|---|---|
admin_addr | fn admin_addr(&self) -> SocketAddr | The bound admin API address (resolve an ephemeral :0 to the real port). |
metrics_addr | fn metrics_addr(&self) -> Option<SocketAddr> | The bound metrics address, or None if metrics weren’t started. |
intercept_addr | fn intercept_addr(&self) -> Option<SocketAddr> | The bound intercept proxy address, whether started by --intercept-port or later by POST /intercept; None when none is running. |
front_door_addr | fn front_door_addr(&self) -> Option<SocketAddr> | The bound front door address; None unless --front-door was given. |
join | async fn join(self) -> anyhow::Result<()> | Await the server until it exits, consuming it. |
wait | async fn wait(&self) -> anyhow::Result<()> | Await the server until it exits without consuming it — so you can race it against your own shutdown signal. |
shutdown | async fn shutdown(&self) | Trigger a graceful shutdown. |
use rift_http_proxy::server::{Cli, ServerBuilder};
use clap::Parser;
let server = ServerBuilder::from_cli(Cli::parse()).start().await?;
println!("admin listening on {}", server.admin_addr());
// ... run your test suite against server.admin_addr() ...
server.shutdown().await;
Racing the server against a shutdown signal
join moves the server, so a select! arm that wins against it can no longer reach shutdown. wait borrows instead, which is what lets a host own the shutdown policy — run its own teardown between the signal and the server stopping, and still surface an admin-plane failure if the server dies on its own first:
tokio::select! {
result = server.wait() => return result, // the admin plane exited — surface why
() = termination_signal() => {} // asked to stop — fall through
}
// your own teardown here (drain a cluster, deregister from a load balancer, ...)
server.shutdown().await;
The accept loop’s error is delivered to the first caller of wait/join; later calls return Ok(()) (anyhow::Error is not Clone). shutdown takes &self, so the server can also be held in an Arc and stopped from another task.
Injecting a custom ImposterManager
ImposterManager (from rift-mock-core) is the injection point for every SPI backend (see Extension Points). Build it, wire your backends, then hand it to the server:
use std::sync::Arc;
use rift_mock_core::imposter::ImposterManager;
use rift_http_proxy::server::{Cli, ServerBuilder};
use clap::Parser;
let manager = Arc::new(
ImposterManager::new()
.with_flow_store_provider(my_flow_store_provider)
.with_request_journal(my_journal),
);
ServerBuilder::from_cli(Cli::parse())
.manager(manager)
.run()
.await?;
Bindable admin & metrics servers
For finer control — running only the admin plane, or binding each plane independently — use the lower-level bind APIs. Both follow the same pattern: pass :0 to get an OS-assigned port and read the actual address back from the returned handle.
AdminApiServer
use std::sync::Arc;
use rift_http_proxy::admin_api::AdminApiServer;
let running = AdminApiServer::new(addr, manager, api_key) // api_key: Option<String>
.with_config_source(config_source) // ConfigSource, optional
.with_allow_injection(true) // enable JS inject, optional
.bind()
.await?;
println!("admin bound to {}", running.local_addr());
| Item | Signature | Purpose |
|---|---|---|
AdminApiServer::new | fn new(addr: SocketAddr, manager: Arc<ImposterManager>, api_key: Option<String>) -> Self | Construct the admin server; api_key (when Some) gates the admin API via the Authorization header. |
with_config_source | fn with_config_source(self, source: ConfigSource) -> Self | Retain the load source so POST /admin/reload can re-read it. |
with_imposter_sources | fn with_imposter_sources(self, sources: Arc<SourceSet>, datadir: Option<PathBuf>) -> Self | Retain an --imposters source set, and the --datadir loaded beside it, so POST /admin/reload re-reads both and applies them as one set. Source imposters are applied as Persistence::Ephemeral and never written to the datadir (issue #1122). ImposterManager::apply_config keeps a running imposter in the store it is in: it persists what it creates, and changes to imposters already in the datadir. |
with_allow_injection | fn with_allow_injection(self, allow: bool) -> Self | Admit scripted configs (inject, decorate, shellTransform, _rift.script, …) submitted through this admin API, and report the setting from GET /config. The embedder spelling of --allowInjection; default false. |
with_local_only | fn with_local_only(self, local_only: bool) -> Self | Record --local-only for GET /config to report (issue #879). Stated explicitly, not inferred from the bind address. |
with_admin_authorizer | fn with_admin_authorizer(self, authorizer: Arc<dyn AdminAuthorizer>) -> Self | Install the per-request authorization hook (issue #854); ServerBuilder::admin_authorizer is the builder spelling. |
with_intercept | fn with_intercept(self, control: InterceptControl) -> Self | Serve the /intercept* routes against this shared control slot. Without it every /intercept* route answers 404. See Intercept proxy. |
with_scripts_dir | fn with_scripts_dir(self, dir: PathBuf) -> Self | Root that _rift.script file: references resolve under for imposters created through this API (issue #356). Without it such references are rejected. |
with_reported_admin_port | fn with_reported_admin_port(self, port: u16) -> Self | Report port from GET /config instead of the port this server bound (issue #1135). For a host that fronts the admin API with its own public listener and binds the core to an ephemeral loopback port — without it, options.port advertises the private port to Mountebank-compat clients that read it to build URLs. Unset reports the bound port; 0 is a configured 0, not “unset”. ServerBuilder::reported_admin_port is the builder spelling. |
with_require_admin_auth | fn with_require_admin_auth(self, require: bool) -> Self | Make bind fail when this server would be reachable off-host with no api_key, instead of warning (issue #863). The embedder spelling of --require-admin-auth. |
bind | async fn bind(self) -> anyhow::Result<RunningAdminApi> | Bind and start serving; returns once bound. |
run | async fn run(self) -> anyhow::Result<()> | Bind and serve until the accept loop exits. |
bind reports the authentication posture either way. When addr is not loopback and api_key is None, it logs a warning naming the address and the remedies — the admin API can create imposters and drive the TLS intercept proxy, so an unauthenticated off-host bind is worth stating out loud. with_require_admin_auth(true) turns that warning into a startup error; the check runs before the listener binds, so a refusal never leaves a socket behind. It gates on authentication, not on the address: a real api_key satisfies it on any bind, and loopback satisfies it with none.
RunningAdminApi: local_addr(&self) -> SocketAddr, shutdown(&self), join(self) -> anyhow::Result<()>, wait(&self) -> anyhow::Result<()> (the non-consuming form of join, as above).
Computing addr the way the CLI does
If your binary parses rift’s Cli, derive the admin address with server::admin_bind_addr rather than reading --host/--port yourself — --local-only pins loopback and outranks --host, and a private copy of that rule can disagree with the exposure check about which address is being judged (issue #1131):
use rift_http_proxy::server::admin_bind_addr;
let addr = admin_bind_addr(&cli)?; // --local-only pins 127.0.0.1, else --host, on --port
let running = AdminApiServer::new(addr, manager, cli.api_key.clone())
.with_require_admin_auth(cli.require_admin_auth)
.with_local_only(cli.local_only)
.bind()
.await?;
Do not call check_admin_exposure yourself here: bind already runs it on addr, so a hand-rolled call in front of it judges the same address twice and logs the warning twice. Thread --require-admin-auth through with_require_admin_auth instead — that is what turns the warning into a refusal. (ServerBuilder::start does check before binding, but only because it has a metrics listener it must not have to unwind; an embedder binding the admin plane alone does not.)
| Item | Signature | Purpose |
|---|---|---|
admin_bind_addr | fn admin_bind_addr(cli: &Cli) -> anyhow::Result<SocketAddr> | The address the admin plane binds under this CLI: --local-only pins loopback, otherwise --host, on --port. The same value the exposure check is handed. ServerBuilder::start calls it too, so the rule has one definition. Accepts an IPv4 literal or an IPv6 literal, bare (::1) or bracketed ([::1]); errors when --host is not an IP literal (a DNS name is not resolved). |
ConfigSource (from rift-http-proxy) is either File { path, no_parse } (a single --configfile, with optional EJS preprocessing) or Dir(PathBuf) (a --datadir of one-imposter-per-file configs).
Metrics server
use rift_http_proxy::server::bind_metrics_server;
let metrics = bind_metrics_server(addr).await?; // addr may be `:0`
println!("metrics bound to {}", metrics.local_addr());
metrics.shutdown().await;
| Function / method | Signature | Purpose |
|---|---|---|
run_metrics_server | async fn run_metrics_server(addr: SocketAddr) -> anyhow::Result<()> | Serve metrics forever on a fixed address. |
bind_metrics_server | async fn bind_metrics_server(addr: SocketAddr) -> anyhow::Result<RunningMetrics> | Bind (supports :0) and return a handle. |
RunningMetrics::local_addr | fn local_addr(&self) -> SocketAddr | The bound metrics address. |
RunningMetrics::shutdown | async fn shutdown(&self) | Stop the metrics server. |
RunningMetrics::join | async fn join(self) -> anyhow::Result<()> | Await until it exits. |
Testing against a dead admin plane (test-util)
RunningServer::wait() reports the admin accept loop dying, which an embedder typically propagates to process exit. Provoking that failure for real means breaking the listener, so the test-util feature exposes constructors whose “accept loop” is a future you supply (issue #825):
[dev-dependencies]
rift-http-proxy = { version = "*", features = ["test-util"] }
use rift_http_proxy::server::RunningServer;
let server = RunningServer::with_admin_accept_task(async {
Err(anyhow::anyhow!("admin plane died"))
});
let err = server.wait().await.expect_err("the embedder must see the failure");
RunningAdminApi::with_accept_task is the same seam one layer down. Neither binds a listener (admin_addr() reports 127.0.0.1:0), and both are test scaffolding — do not enable test-util in a production build.
Bootstrap helpers
ServerBuilder composes the running server, but a binary also has bootstrap concerns around it: applying an rcfile’s defaults, stopping a server by PID file, and saving a running server’s imposters. These live in rift_http_proxy::bootstrap so an alternative binary keeps CLI parity with rift instead of reimplementing them (issue #807).
| Function | Signature | Purpose |
|---|---|---|
apply_rcfile_defaults | fn apply_rcfile_defaults(cli: &mut Cli, rcfile: &Path) -> anyhow::Result<()> | Fill CLI fields still at their clap defaults from a Mountebank-compatible JSON rcfile. An explicitly-supplied flag always wins; unrecognised keys are logged with warn! and ignored. An Err means nothing was applied; the rift binary treats it as fatal. |
apply_rcfile_defaults_reporting | fn apply_rcfile_defaults_reporting(cli: &mut Cli, rcfile: &Path) -> anyhow::Result<Vec<String>> | The same, returning the unrecognised keys instead of logging them — for a caller that applies the rcfile before it installs a log subscriber. |
stop_for_restart | fn stop_for_restart(pidfile: &Path) -> anyhow::Result<()> | stop_server, except a missing PID file is a satisfied precondition (nothing to stop) rather than an error — the restart semantic. |
log_filter | fn log_filter(cli: &Cli) -> anyhow::Result<EnvFilter> | The tracing filter this CLI asks for: RUST_LOG when set, otherwise --debug, otherwise --loglevel. Errors on a level that does not exist, and on a RUST_LOG that is set but does not parse — an unset RUST_LOG is an absence, not a failure (issue #1134). |
log_filter_with | fn log_filter_with(cli: &Cli, rust_log: Option<&str>) -> anyhow::Result<EnvFilter> | The same rules with the RUST_LOG value supplied rather than read (None = unset), for a host that resolves it some other way — or wants to test without mutating the process environment. |
DEFAULT_PIDFILE | pub const DEFAULT_PIDFILE: &str | The rift.pid fallback stop/restart apply when --pidfile is absent. Applied at the dispatch site so a plain start never writes a PID file it wasn’t asked to. |
stop_server | fn stop_server(pidfile: &Path) -> anyhow::Result<()> | Signal the process named in pidfile (SIGTERM on unix, taskkill /F on Windows), then remove the file. A stale pidfile (process already gone) is cleaned up as Ok; a denied or failed signal is an error and the pidfile is kept. On unix it waits up to five seconds for the process to exit; one still running after that is an error and the pidfile is kept. |
stop_server_within | fn stop_server_within(pidfile: &Path, ceiling: Duration) -> anyhow::Result<()> | stop_server with the exit wait chosen by the caller — for an embedder whose server outlives five seconds on SIGTERM (a drain window, a cluster departure). |
save_imposters_async | async fn save_imposters_async(host: &str, port: u16, savefile: &Path, remove_proxies: bool, api_key: Option<&str>) -> anyhow::Result<()> | Fetch GET /imposters?replayable=true from a running admin API and write it to savefile. The async form — call it from an embedder’s own runtime. api_key is sent as the raw Authorization value; pass None for an unkeyed server (issue #1154). A non-2xx admin response is an error; nothing is written to savefile. |
save_imposters | fn save_imposters(host: &str, port: u16, savefile: &Path, remove_proxies: bool, api_key: Option<&str>) -> anyhow::Result<()> | Blocking wrapper over save_imposters_async for the sync save subcommand path. |
Supported rcfile keys: port, host, logLevel/loglevel, allowInjection/allow_injection, localOnly/local_only, requireAdminAuth/require_admin_auth, apiKey/api_key, datadir, configfile, noParse/no_parse. Each must have its type — the flags are JSON booleans, host, logLevel, apiKey, datadir and configfile are strings, and port is an integer from 0 to 65535 — or the whole rcfile is refused and nothing is applied.
apiKey sets the admin credential, like --api-key/MB_APIKEY, and like every other key it defers to an explicitly-given flag (issue #1132). A blank value is refused by validate_admin_api_key at startup exactly as a blank --api-key is — an rcfile is a normal place to keep the credential, so keep it readable only by the user the server runs as.
use rift_http_proxy::bootstrap;
use rift_http_proxy::server::{Cli, Commands};
use clap::Parser;
let mut cli = Cli::parse();
if let Some(rcfile) = cli.rcfile.clone() {
bootstrap::apply_rcfile_defaults(&mut cli, &rcfile)?;
}
match &cli.command {
// `--pidfile` is a single global binding (issue #827): it parses before or after the
// subcommand, and the stop/restart default lives here, not on the flag.
Some(Commands::Stop) => return bootstrap::stop_server(&pidfile_or_default(&cli)),
// `restart` is `stop` followed by the normal start path — but a missing PID file means
// "nothing to stop", not an error, so it uses the restart-specific seam.
Some(Commands::Restart) => bootstrap::stop_for_restart(&pidfile_or_default(&cli))?,
Some(Commands::Save { savefile, remove_proxies }) => {
return bootstrap::save_imposters(
&cli.host,
cli.port,
savefile,
*remove_proxies,
cli.api_key.as_deref(),
);
}
_ => {}
}
The rift binary writes --pidfile only on the serving path (inside run_mountebank_mode), never before subcommand dispatch — otherwise rift --pidfile p restart would record its own PID and then signal itself, and a transient save/healthcheck would clobber a running server’s file (issue #827). An alternative binary should keep that ordering.
save_imposters builds its own tokio runtime and blocks on it (it is a sync subcommand path), so do not call it from inside a running async runtime — it would panic starting a nested runtime. From async code call save_imposters_async directly instead; it awaits rather than driving its own runtime, so it is safe on an async worker thread.
TLS: install the crypto provider
Before serving any HTTPS imposter from an embedding host, install the default rustls (ring) crypto provider once:
rift_http_proxy::install_default_crypto_provider();
It is idempotent, so calling it more than once is safe. The rift binary does this for you; an embedding host must call it itself if it serves TLS.