dream_http_client/recorder
Recorder process and state management
Manages HTTP request/response recordings using a process to store state. Supports recording, playback, and passthrough modes.
Types
Opaque recorder handle for managing HTTP request/response recordings
A Recorder is a handle to an OTP actor process that manages recording state.
Multiple HTTP requests can share the same recorder by passing the same handle.
The recorder handles saving recordings to disk, loading them for playback,
and matching requests to recorded responses.
Thread Safety
Recorders are safe to use concurrently - multiple requests can use the same recorder handle simultaneously. The internal actor processes messages sequentially.
Lifecycle
- Build with
recorder.new() |> ... |> start() - Attach to requests with
client.recorder(rec) - Optionally cleanup with
recorder.stop(rec)(recordings already saved)
Examples
import dream_http_client/recorder.{directory, mode, start}
// Record mode
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("record")
|> start()
// Use recorder with requests
client.new()
|> client.host("api.example.com")
|> client.recorder(rec)
|> client.send()
// Cleanup (optional - recordings already saved)
recorder.stop(rec)
pub opaque type Recorder
Builder for configuring and starting a recorder.
Recorder configuration uses a builder so options can evolve without adding a
combinatorial set of start_with_* functions.
import dream_http_client/recorder.{directory, mode}
let builder =
recorder.new()
|> directory("mocks/api")
|> mode("playback")
pub type RecorderBuilder {
RecorderBuilder(
mode: String,
directory: option.Option(String),
key: fn(recording.RecordedRequest) -> String,
request_transformer: fn(recording.RecordedRequest) -> recording.RecordedRequest,
response_transformer: fn(
recording.RecordedRequest,
recording.RecordedResponse,
) -> recording.RecordedResponse,
)
}
Constructors
-
RecorderBuilder( mode: String, directory: option.Option(String), key: fn(recording.RecordedRequest) -> String, request_transformer: fn(recording.RecordedRequest) -> recording.RecordedRequest, response_transformer: fn( recording.RecordedRequest, recording.RecordedResponse, ) -> recording.RecordedResponse, )
A request transformer applied before keying and persistence.
A transformer is a pure function RecordedRequest -> RecordedRequest that
runs in two places:
- Before computing the match key (so matching uses the transformed request)
- Before persisting recordings to disk (so secrets can be removed)
This is how you implement “scrub secrets and still match”: normalize the request (remove auth headers, drop query params, normalize paths, etc.), then choose a key that matches on the remaining stable fields.
Example (drop Authorization header)
import dream_http_client/recorder.{directory, mode, request_transformer}
import dream_http_client/recording
import gleam/list
fn drop_auth_header(req: recording.RecordedRequest) -> recording.RecordedRequest {
let headers =
req.headers
|> list.filter(fn(h) { h.0 != "Authorization" })
recording.RecordedRequest(..req, headers: headers)
}
let builder =
recorder.new()
|> mode("record")
|> directory("mocks")
|> request_transformer(drop_auth_header)
pub type RequestTransformer =
fn(recording.RecordedRequest) -> recording.RecordedRequest
A response transformer applied before persistence in Record mode.
This hook is for scrubbing secrets out of recorded responses (set-cookie, authorization echoes, PII, etc.) before they are written to disk.
Order of operations in Record mode:
- The request transformer runs (request normalization + request scrubbing)
- The key is computed from the transformed request
- The response transformer runs (response scrubbing)
- The scrubbed recording is stored in memory and persisted to disk
Note: this transformer runs only in Record mode. Playback returns whatever is in the saved fixtures.
pub type ResponseTransformer =
fn(recording.RecordedRequest, recording.RecordedResponse) -> recording.RecordedResponse
Values
pub fn add_recording(
recorder: Recorder,
rec: recording.Recording,
) -> Nil
Add a recording to the recorder
Manually adds a recording to the recorder’s in-memory state and saves it to disk immediately if in Record mode. This function is typically called automatically by the HTTP client when a request completes, but can be used directly for testing or manual recording creation.
Parameters
recorder: The recorder to add the recording torec: The recording (request/response pair) to add
Behavior by Mode
- Record mode: Adds to in-memory state and saves to disk immediately
- Playback mode: No-op (recordings loaded from disk at startup)
- Passthrough mode: No-op (no recording functionality)
Examples
import dream_http_client/recorder.{directory, mode, start}
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("record")
|> start()
// Manually add a recording
let manual_recording = recording.Recording(
request: create_test_request(),
response: create_test_response(),
)
recorder.add_recording(rec, manual_recording)
Notes
- Recordings are saved immediately in Record mode (no need to call
stop()) - If save fails, error is logged but recording remains in memory
- If multiple recordings share the same match key, they are all stored (playback lookup will error if that makes the key ambiguous)
pub fn directory(
builder: RecorderBuilder,
directory: String,
) -> RecorderBuilder
Set the recording directory used by "record" and "playback" modes.
The directory is required for "record" and "playback" (validated in
start()), and ignored for "passthrough".
pub fn find_recording(
recorder: Recorder,
request: recording.RecordedRequest,
) -> Result(option.Option(recording.Recording), String)
Find a matching recording for a request
Searches for a recording that matches the given request based on the recorder’s configured match key and request transformer. This is used internally by the HTTP client during playback, but can be called directly to check if a recording exists.
Parameters
recorder: The recorder to search inrequest: The request to find a matching recording for
Returns
Ok(Some(Recording)): Matching recording found (unambiguous)Ok(None): No matching recording found (or not in Playback mode)Error(String): Playback lookup was ambiguous (multiple recordings share the same key)
Examples
import dream_http_client/recorder.{directory, mode, start}
import gleam/http
import gleam/option
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("playback")
|> start()
let request = recording.RecordedRequest(
method: http.Get,
scheme: http.Https,
host: "api.example.com",
port: option.None,
path: "/users",
query: option.None,
headers: [],
body: "",
)
case recorder.find_recording(rec, request) {
Ok(option.Some(_recording)) -> io.println("Found recording")
Ok(option.None) -> io.println("No recording found")
Error(reason) -> io.println_error("Ambiguous match: " <> reason)
}
Notes
- Only works in Playback mode (returns
Nonein other modes) - Matching uses the recorder’s configured key + request transformer
- Returns
Noneif recorder doesn’t respond (safe default) - Timeout is 1 second - recorder should respond quickly
pub fn get_recordings(
recorder: Recorder,
) -> List(recording.Recording)
Get all recordings from the recorder
Retrieves all recordings currently stored in the recorder’s in-memory state. In Playback mode, this returns all recordings loaded from disk at startup. In Record mode, this returns all recordings captured so far (including unsaved ones).
Parameters
recorder: The recorder to get recordings from
Returns
List(Recording): All recordings in the recorder (empty list if none or error)
Examples
import dream_http_client/recorder.{directory, mode, start}
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("record")
|> start()
// Make some requests...
let recordings = recorder.get_recordings(rec)
io.println("Captured " <> int.to_string(list.length(recordings)) <> " recordings")
Notes
- Returns empty list if recorder doesn’t respond (safe default)
- In Record mode, includes recordings that may not yet be saved to disk
- Timeout is 1 second - recorder should respond quickly
pub fn is_record_mode(recorder: Recorder) -> Bool
Check if recorder is in Record mode
Determines whether the recorder is configured to capture and save real HTTP requests/responses. Useful for conditional logic that only applies during recording.
Parameters
recorder: The recorder to check
Returns
True: Recorder is in Record modeFalse: Recorder is in Playback or Passthrough mode
Examples
import dream_http_client/recorder.{directory, mode, start}
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("record")
|> start()
if recorder.is_record_mode(rec) {
io.println("Recording mode active")
}
Notes
- Returns
Falseif the recorder process doesn’t respond (safe default) - Timeout is 1 second - recorder should respond quickly
pub fn key(
builder: RecorderBuilder,
key: fn(recording.RecordedRequest) -> String,
) -> RecorderBuilder
Set the match key function.
The key function determines how requests are grouped for playback lookup. Keys should be stable and should generally not include secrets.
If two different recordings produce the same key, playback lookup becomes
ambiguous and find_recording() will return Error(...).
pub fn mode(
builder: RecorderBuilder,
mode: String,
) -> RecorderBuilder
Set the recorder mode string.
Valid modes (validated in start()):
"record""playback""passthrough"
Mode strings are case-sensitive.
pub fn new() -> RecorderBuilder
Create a new recorder builder with safe defaults.
Defaults:
mode:"passthrough"directory: unsetkey:matching.request_key(method: True, url: True, headers: False, body: False)request_transformer: identityresponse_transformer: identity
pub fn request_transformer(
builder: RecorderBuilder,
request_transformer: fn(recording.RecordedRequest) -> recording.RecordedRequest,
) -> RecorderBuilder
Set the request transformer.
The transformer is applied before keying and before persistence, so it’s the right place to normalize requests and scrub secrets.
pub fn response_transformer(
builder: RecorderBuilder,
response_transformer: fn(
recording.RecordedRequest,
recording.RecordedResponse,
) -> recording.RecordedResponse,
) -> RecorderBuilder
Set the response transformer.
This transformer is applied only in Record mode and runs before persistence so recorded fixtures can be safely committed/shared.
pub fn start(
builder: RecorderBuilder,
) -> Result(Recorder, String)
Start a new recorder from a builder.
Creates an OTP actor process to manage recorder state. The recorder handles saving recordings to disk (Record mode), loading them for playback (Playback mode), or passing requests through unchanged (Passthrough mode).
Parameters
builder: Recorder configuration builder
Returns
Ok(Recorder): Successfully started recorderError(String): Error message if startup fails (e.g., cannot load recordings in Playback mode)
Examples
import dream_http_client/recorder.{directory, mode, start}
let assert Ok(rec) =
recorder.new()
|> directory("mocks/api")
|> mode("playback")
|> start()
Notes
- In Playback mode, recordings are loaded from disk at startup
- In Record mode, recordings are saved immediately when captured (no need to call
stop()) - Multiple requests can share the same recorder handle safely
- The recorder process runs until
stop()is called or the VM shuts down
pub fn stop(recorder: Recorder) -> Result(Nil, String)
Stop the recorder and cleanup
Stops the recorder’s OTP actor process and releases resources. In Record mode,
recordings are already saved to disk immediately when captured, so this function
only performs cleanup. Calling stop() is optional but recommended for proper
resource management.
Parameters
recorder: The recorder to stop
Returns
Ok(Nil): Successfully stopped the recorderError(String): Error message if the recorder doesn’t respond within 5 seconds
Examples
import dream_http_client/recorder.{directory, mode, start}
let assert Ok(rec) =
recorder.new()
|> directory("mocks")
|> mode("record")
|> start()
// Use recorder...
// Cleanup (optional - recordings already saved)
case recorder.stop(rec) {
Ok(_) -> io.println("Recorder stopped")
Error(reason) -> io.println_error("Failed to stop: " <> reason)
}
Notes
- Recordings are saved immediately when captured -
stop()is not required for persistence - This function is optional but recommended for proper resource cleanup
- Timeout is 5 seconds - recorder should stop quickly
- After calling
stop(), the recorder handle is no longer valid
pub fn transform_response(
recorder: Recorder,
request: recording.RecordedRequest,
response: recording.RecordedResponse,
) -> recording.RecordedResponse
Apply the recorder’s response transformer to a response.