cassette

package
v0.18.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 17, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package cassette implements recording and replaying of HTTP traffic captured via CDP Fetch interception, for deterministic browser-automation runs.

A cassette is a versioned JSON file holding (request, response) pairs. The match key for serving a request from a cassette is the tuple (method, url, body-hash). Querystring is part of the URL and matters. Headers are NOT part of the match key (they vary too much for replay to be useful). Bodies are stored base64 — they may be binary.

Forward-compat: Version is bumped on any breaking format change. Readers should reject cassettes with a higher Version than they understand.

Index

Constants

View Source
const DefaultBodyCapBytes = 5 * 1024 * 1024

DefaultBodyCapBytes is the per-response body cap when not opted out. Responses larger than this are truncated; a stderr warning is emitted by the recording layer.

View Source
const FormatVersion = 1

FormatVersion is the cassette format version. Bump on breaking changes.

Variables

This section is empty.

Functions

func DecodeBody

func DecodeBody(s string) ([]byte, error)

DecodeBody base64-decodes a stored body. Returns nil for empty input.

func EncodeBody

func EncodeBody(b []byte) string

EncodeBody base64-encodes a body for on-disk storage.

func IsValidMode

func IsValidMode(m RecordMode) bool

IsValidMode reports whether m is a recognized record mode.

func Save

func Save(c *Cassette, path string) error

Save writes the cassette to path with pretty-printed JSON. The parent directory is created if it does not exist (so callers can supply a path like "fixtures/foo.cassette.json" without a separate mkdir).

Types

type Cassette

type Cassette struct {
	Version    int       `json:"version"`
	RecordedAt time.Time `json:"recorded_at"`
	BrzVersion string    `json:"brz_version,omitempty"`
	Workflow   string    `json:"workflow,omitempty"`
	Entries    []*Entry  `json:"entries"`
}

Cassette is the on-disk format. Entries is intentionally a slice (not map) to preserve recording order — useful for human inspection and diffs.

func Load

func Load(path string) (*Cassette, error)

Load reads and parses a cassette file. Rejects unknown future versions.

func LoadCassetteForRecord

func LoadCassetteForRecord(path string) (*Cassette, error)

LoadCassetteForRecord loads an existing cassette for ModeNew seeding. Returns (nil, nil) if the file does not exist (clean start). All other errors — corrupt JSON, future format version — are surfaced so the caller does not silently overwrite a recoverable cassette on save.

type Entry

type Entry struct {
	Request       Request  `json:"request"`
	Response      Response `json:"response"`
	MatchedCount  int      `json:"matched_count"`
	BodyTruncated bool     `json:"body_truncated,omitempty"`
}

Entry is a single (request, response) pair plus bookkeeping.

type Index

type Index struct {
	// contains filtered or unexported fields
}

Index is an in-memory match-key → entries map built from a Cassette. On hash collision (same key, multiple entries), entries are returned in recording order — a round-robin across calls.

func NewIndex

func NewIndex(c *Cassette) *Index

NewIndex builds an Index from a Cassette by hashing each entry's request.

func (*Index) Cassette

func (i *Index) Cassette() *Cassette

Cassette returns the underlying cassette (read-only callers).

func (*Index) Lookup

func (i *Index) Lookup(k Key) (*Entry, bool)

Lookup returns the next entry matching k, advancing a per-key cursor so repeated identical requests hit successive recorded responses (when the cassette has multiple entries for the same key). When the cursor exceeds the entry count, we wrap and continue serving the last entry — a request the page makes a 4th time gets the same recording as the 3rd.

type Key

type Key struct {
	Method   string
	URL      string
	BodyHash string
}

Key is the match-key tuple used to look up entries in replay mode. URL and method are normalized; bodyHash is sha256 hex of the raw body bytes.

func MakeKey

func MakeKey(method, rawURL string, body []byte) Key

MakeKey builds a Key from method, URL, and raw body bytes. Method is uppercased. URL is canonicalized (host lowercased, query keys sorted, fragment stripped) so cosmetically-different URLs match.

func (Key) String

func (k Key) String() string

String renders the key for logging/error messages.

type RecordMode

type RecordMode string

RecordMode controls record-mode behavior when a request is already in the cassette (only relevant for ModeNew).

const (
	// ModeAll records every intercepted request, even if the same key is
	// already in the cassette. The cassette grows on each run.
	ModeAll RecordMode = "all"
	// ModeNew passes through to the network for already-recorded requests
	// but does not append a new entry. New requests get appended.
	ModeNew RecordMode = "new"
)

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder hooks rod.Browser.HijackRequests, fetches the upstream response via Go's http.Client, and appends entries to a Cassette. Call Stop() before saving the cassette — Stop drains in-flight handlers via a WaitGroup so concurrent intercepts cannot race the save.

func NewRecorder

func NewRecorder(existing *Cassette, opts RecorderOptions) *Recorder

NewRecorder constructs a Recorder. existing may be nil; if non-nil, its entries seed the recorder so ModeNew can deduplicate against prior runs.

func (*Recorder) Attach

func (r *Recorder) Attach(browser *rod.Browser) error

Attach starts the interceptor on the given browser. Must be called after the browser is connected via CDP. Returns an error if Fetch.enable fails.

func (*Recorder) Cassette

func (r *Recorder) Cassette() *Cassette

Cassette returns the current cassette state.

func (*Recorder) Stop

func (r *Recorder) Stop() error

Stop tears down the interceptor and waits for any in-flight handlers to finish writing to the cassette. It is safe to call Cassette() after Stop().

type RecorderOptions

type RecorderOptions struct {
	Mode RecordMode
	// BodyCapBytes caps each captured response body. 0 = use DefaultBodyCapBytes;
	// negative = no cap. The cap is enforced while reading the response (LimitReader),
	// not after, so a 500MB response will not be fully buffered into memory.
	BodyCapBytes int
	Stderr       io.Writer
	Workflow     string
	BrzVersion   string
}

RecorderOptions configures the Recorder.

type Replayer

type Replayer struct {
	// contains filtered or unexported fields
}

Replayer serves browser requests from a cassette index. Unmatched requests either pass through (default) or fail (strict). Strict mode is suitable for CI: a regression in workflow ordering surfaces as a clear failure.

func NewReplayer

func NewReplayer(idx *Index, opts ReplayerOptions) *Replayer

NewReplayer constructs a Replayer over the given cassette index.

func (*Replayer) Attach

func (r *Replayer) Attach(browser *rod.Browser) error

Attach starts the interceptor on the given browser.

func (*Replayer) Misses

func (r *Replayer) Misses() []Key

Misses returns the keys that did not match the cassette during the run.

func (*Replayer) Stop

func (r *Replayer) Stop() error

Stop tears down the interceptor and waits for handlers to drain.

type ReplayerOptions

type ReplayerOptions struct {
	Strict bool // fail unmatched requests instead of passing through
	Stderr io.Writer
}

ReplayerOptions configures the Replayer.

type Request

type Request struct {
	Method  string            `json:"method"`
	URL     string            `json:"url"`
	Headers map[string]string `json:"headers,omitempty"`
	BodyB64 string            `json:"body_b64,omitempty"`
}

Request captures the request side of an interception.

type Response

type Response struct {
	Status  int               `json:"status"`
	Headers map[string]string `json:"headers,omitempty"`
	BodyB64 string            `json:"body_b64,omitempty"`
}

Response captures the response side of an interception.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL