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)
  • Corrupt

    Not 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

  • Busy

    Requests are still in flight; only an immediate finish returns this.

  • WaitTimeout

    The 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)
  • CaptureLimit

    The capture exceeded its byte budget or 10,000 exchanges, or the budget is outside 1 byte to 16 MiB.

  • IoFailure(operation: FileOperation, cause: FileCause)
  • DestinationExists

    The destination exists and replace_existing was not set.

  • RecordingClosed

    The recording is finishing or finished.

  • Interrupted

    The 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

A capture in progress. Finish or abort it; stop the client separately.

pub type Recording =
  @internal 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.

✨ Search Document