Proxy record/replay (Recording)¶
imposter.startRecording(origin) fronts an imposter with a proxy to a real upstream, records the
traffic that flows through it, then swaps the proxy for the recorded stubs — so the imposter serves
the captured responses with the upstream gone. It's sugar over the engine's proxy record/replay.
Imposter proxy = rift.create(imposter("users").record());
try (Recording recording = proxy.startRecording("https://api.real-service.com")) {
// drive your SUT through proxy.uri() — requests are proxied to the origin and recorded
List<Stub> recorded = recording.stop(); // swaps the proxy for the recorded stubs
}
// proxy now serves the recorded responses; the origin is no longer touched
RecordSpec¶
startRecording(origin, spec) tunes what is captured:
RecordSpec spec = RecordSpec.builder()
.mode(RecordMode.ONCE) // ONCE (default) | ALWAYS | TRANSPARENT
.generateBy(RequestField.METHOD, RequestField.PATH) // which fields become match predicates
.addWaitBehavior(true) // capture realistic latency (default true)
.ignoreHeaders("Date", "X-Request-Id") // strip volatile headers from predicates
.build();
| Setting | Default | Meaning |
|---|---|---|
mode |
ONCE |
ONCE = proxy first request per predicate, then replay (proxyOnce); ALWAYS = always proxy, recording each unique request (proxyAlways); TRANSPARENT = forward without recording (proxyTransparent). |
generateBy(RequestField...) |
METHOD, PATH |
The request fields turned into the recorded stub's match predicates. |
addWaitBehavior(boolean) |
true |
Capture the upstream's observed latency as a wait behavior. |
ignoreHeaders(String...) |
none | When generateBy includes HEADERS, drop these (case-insensitive) volatile headers from the generated predicates so recordings are stable run-to-run. From rift 0.22.0 the engine drops them while recording, so requests that differ only in them reach the origin once and record one stub; the exported stubs are also stripped, which covers older engines. |
Recording¶
Recording is AutoCloseable, so a try-with-resources block stops it on exit.
| Method | Effect |
|---|---|
List<Stub> stop() |
Fetch the recorded stubs (proxy removed), swap them onto the imposter, and return them. Idempotent. |
List<Stub> snapshot() |
Return what's recorded so far; the proxy stays in place (keep recording). |
void persist(Path file) |
stop(), then write the replayable imposter JSON to file. |
void close() |
stop(). |
From rift 0.23.0, an ALWAYS (proxyAlways) recording's responses also carry _proxyResponseTime,
the origin's response time (rift#1519). The model keeps it through IsResponse.extra, so it survives
persist and a replay. Imposter.clearProxyResponses() then also strips the responses proxyAlways
appended to stubs you declared, and restarts every remaining stub's response cycle.
persist(file) writes the engine's replayable imposter definition — the same format loadable by
rift --configfile, rift-node, and rift-scala — so a captured golden file is portable across SDKs.
To replay it from rift-java, read the file yourself and hand the JSON to the engine:
Imposter replayed = rift.create(Files.readString(file));
rift-java has no configFile option of its own: the engine's --configfile (and its noParse
switch) is for the CLI, where the file may carry EJS templates. Config the SDK loads is never
run through EJS, so a literal <% in a recorded body is safe as-is.
Recording an origin behind a private CA¶
The engine verifies an origin's certificate against the OS trust store. An origin issued by a
private or corporate CA fails with invalid peer certificate: UnknownIssuer unless the engine is
told to trust that CA. Set upstreamTrust on the options for the engine you start (rift ≥ 0.18.0):
// Embedded: a PEM file, or the same certificate inline
Rift rift = Rift.embedded(EmbeddedOptions.builder()
.upstreamTrust(new UpstreamTrust.CaFile(Path.of("/etc/pki/corp-ca.pem")))
.build());
// Spawned: a PEM file (the rift CLI has no inline form, so UpstreamTrust.CaPem is rejected)
Rift rift = Rift.spawn(SpawnOptions.builder()
.upstreamTrust(new UpstreamTrust.CaFile(Path.of("/etc/pki/corp-ca.pem")))
.build());
// Testcontainers: a PEM file or an inline PEM, written into the container by the transport
RiftContainer rift = new RiftContainer()
.withUpstreamTrust(new UpstreamTrust.CaPem(corpCaPem));
UpstreamTrust |
Engine option | Transports |
|---|---|---|
CaFile(Path) |
upstreamCaFile / --upstream-ca-file (RIFT_UPSTREAM_CA_FILE) |
embedded, spawn, testcontainers |
CaPem(String) |
upstreamCaPem; in a container, a file the transport writes |
embedded, testcontainers |
SkipVerify() |
upstreamTlsSkipVerify / --upstream-tls-skip-verify (RIFT_UPSTREAM_TLS_SKIP_VERIFY) |
embedded, spawn, testcontainers |
- The CA is appended to the OS trust store, so public origins keep working. Don't reach for
SSL_CERT_FILEinstead: the engine honours it, but it replaces the trust store, so pointing it at a lone private CA quietly breaks every public origin. SkipVerifyis for development only. A recording proxy that accepts any certificate will faithfully record a man-in-the-middle's traffic. The SDK logs a warning whenever it is used.- One engine, one policy. Trust is process-wide for that engine, and it covers
proxystubs and the intercept listener's origin leg. - Older engines are refused, not ignored. An embedded engine must advertise the option in its
serveOptions(Rift.info().serveOptions()), orRift.embeddedfails withEngineUnavailable. A spawned engine is checked againstSpawnOptions.versionwhen the options are built, and a container against its image tag whenwithUpstreamTrustis called (a tag that is not a version, such aslatest, cannot be checked). - In a container, the CA is read when the container starts, copied to
/etc/rift/upstream-ca.pem, and named to the engine through its environment; an unreadableCaFilefails the start. - A connected engine (
Rift.connect) is configured by whoever started it: pass--upstream-ca-filetoriftthere.