http_gun/cassette
Loads, saves and records HTTP exchanges as cassette files.
Record once against a live server, then replay offline:
// Recording, for example behind a flag in a test helper.
let assert Ok(cassette.Recorded(client:, recording:)) =
cassette.record(settings, "test/cassettes/orders.json", cassette.options())
let _ = http_gun.send(client, req)
let assert Ok(_path) = cassette.finish(recording, duration.seconds(5))
http_gun.stop(client)
// Playback.
let assert Ok(script) = cassette.load("test/cassettes/orders.json", 1_048_576)
let assert Ok(client) = testing.playback(script, settings)
A cassette is readable JSON ("http_gun": 2): a body or chunk that is
valid UTF-8 is stored as {"text": ..}, anything else as
{"base64": ..}. There is one schema; a file of another version fails
with UnsupportedVersion.
What a cassette stores follows the configuration’s redaction
(config.with_redaction): the credential headers by default, plus any
headers, query parameters and body rewriting you add. Bodies and queries
are otherwise stored exactly and can contain secrets.
A recording writes each exchange as it completes, within a byte budget,
and publishes the file atomically when finish succeeds. Capture
failures are separate from HTTP outcomes: the live client keeps working.
Exchanges are staged in a private <path>.http-gun-* directory next to the
destination. Success, abort, a failed finish, a capture failure, the
death of the process that called record and a crash of the recorder all
remove it. A writer stuck in a blocking file operation delays removal until
that operation returns. Nothing can remove it if the VM is killed or halted
mid-recording; such a directory is safe to delete.
Types
Why a cassette could not be loaded or parsed.
pub type CassetteError {
Missing
Io(operation: FileOperation, cause: FileCause)
Corrupt
UnsupportedVersion(version: Int)
TooLarge(limit: Int, observed: Int)
}
Constructors
-
Missing -
Io(operation: FileOperation, cause: FileCause) -
CorruptNot UTF-8 JSON in the cassette schema.
-
UnsupportedVersion(version: Int) -
TooLarge(limit: Int, observed: Int)
Why a filesystem operation failed.
pub type FileCause {
FileMissing
AlreadyExists
PermissionDenied
NoSpace
ReadOnlyFilesystem
NotDirectory
IsDirectory
CrossFilesystem
UnknownIoFailure
}
Constructors
-
FileMissing -
AlreadyExists -
PermissionDenied -
NoSpace -
ReadOnlyFilesystem -
NotDirectory -
IsDirectory -
CrossFilesystem -
UnknownIoFailure
A filesystem operation that failed.
pub type FileOperation {
OpenFile
ReadFile
WriteFile
CloseFile
CreateDirectory
SetPermissions
PublishFile
}
Constructors
-
OpenFile -
ReadFile -
WriteFile -
CloseFile -
CreateDirectory -
SetPermissions -
PublishFile
Why finish returned without a published file.
pub type FinishError {
Busy
WaitTimeout
CaptureFailed(RecordError)
}
Constructors
-
BusyRequests are still in flight; only an immediate finish returns this.
-
WaitTimeoutThe wait ended; finishing continues and a later call sees the outcome.
-
CaptureFailed(RecordError)
Why a recording could not start or failed while capturing.
pub type RecordError {
ClientFailed(http_gun.StartError)
CaptureLimit
IoFailure(operation: FileOperation, cause: FileCause)
DestinationExists
RecordingClosed
Interrupted
}
Constructors
-
ClientFailed(http_gun.StartError) -
CaptureLimitThe capture exceeded its byte budget or 10,000 exchanges, or the budget is outside 1 byte to 16 MiB.
-
IoFailure(operation: FileOperation, cause: FileCause) -
DestinationExistsThe destination exists and
replace_existingwas not set. -
RecordingClosedThe recording is finishing or finished.
-
InterruptedThe recording’s owner exited, or a writer crashed.
How a recording publishes its file.
pub opaque type RecordOptions
A started recording: a live client and the capture to finish.
pub type Recorded {
Recorded(client: http_gun.Client, recording: Recording)
}
Constructors
-
Recorded(client: http_gun.Client, recording: Recording)
Values
pub fn abort(recording: Recording) -> Result(Nil, RecordError)
Abandon the capture; the live client keeps working. The staging directory beside the destination is removed before this returns, and nothing is published. A file already published is not removed.
pub fn describe_error(problem: CassetteError) -> String
pub fn describe_finish_error(problem: FinishError) -> String
pub fn describe_record_error(problem: RecordError) -> String
pub fn encode(script: testing.Script) -> String
Encode a script as a cassette. Credential headers are never written.
pub fn finish(
recording: Recording,
wait: duration.Duration,
) -> Result(String, FinishError)
Publish the recording. Zero or less finishes now if nothing is in flight
and returns Busy otherwise. A positive wait refuses new requests, waits
up to wait for the requests in flight, then publishes. Once finished
or failed, every later call returns the same outcome.
pub fn load(
path: String,
max_bytes: Int,
) -> Result(testing.Script, CassetteError)
Read and parse a cassette file of at most max_bytes, reading no more
than max_bytes plus one byte. Never opens a network connection.
pub fn options() -> RecordOptions
Capture at most 16 MiB of encoded data and refuse to replace an existing file.
pub fn parse(
text: String,
max_bytes: Int,
) -> Result(testing.Script, CassetteError)
Parse a cassette of at most max_bytes.
pub fn record(
settings: config.Config,
path: String,
options: RecordOptions,
) -> Result(Recorded, RecordError)
Start a live client that records every exchange to path. Finish the
recording with finish and stop the client separately.
pub fn replace_existing(options: RecordOptions) -> RecordOptions
Replace the destination file if it exists.
pub fn with_max_bytes(
options: RecordOptions,
bytes: Int,
) -> RecordOptions
The capture’s byte budget, 1 byte to 16 MiB.