Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Edge Cases

The awkward inputs a record/replay library meets in practice, and what http-vcr does with each. Every behaviour on this page has a test in tests/Integration/ named after it — if a row here and the code ever disagree, the test is the one telling the truth.

Streams

PSR-7 bodies are streams, and streams are not obliged to rewind. A request body built from a socket, a pipe, or php://input can be read exactly once.

SituationBehaviour
Request body cannot be rewoundBuffered once at the snapshot boundary. Both the recording and the real client get a readable stream over the buffered bytes, so the request still reaches the network intact
Response body cannot be rewoundBuffered the same way. The code under test gets a fresh readable stream, not the drained original
Body is seekableHanded back as the same stream, rewound — no needless copy

This is the practical consequence of ADR-0004: everything becomes a string at the boundary, so nothing downstream can be surprised by a consumed handle.

Bodies

SituationBehaviour
Binary response (PDF, image, gzip under test)Stored base64-encoded, flagged in bodyEncoding
TextLeft readable in the file, so diffs stay reviewable
Claims to be text but is not valid UTF-8Treated as binary. The decision is made on the content, not on Content-Type
Binary request bodyEncoded too — the rule is not response-only
Empty bodyNever encoded. An empty string stays an empty string

Large bodies and sidecar files

Past inlineBodyLimit (default 1 MiB) the body moves to a file of its own — see ADR-0013.

SituationBehaviour
Body over the thresholdWritten beside the cassette; the cassette keeps a reference
Replay of a sidecar bodyByte-for-byte identical to what was recorded
Body under the thresholdStays inline in the cassette
Two interactions, identical bodiesShare one file — content-addressed, stored once
Sidecar no longer referencedRemoved when the cassette is next written
Sidecar edited by handRefused, not replayed as wrong bytes
Sidecar missingError naming the file that is gone
Body files in the cassette directoryRecognised as not-cassettes; the CLI inventory skips them

Compression

SituationBehaviour
Gzipped responseDecompressed and stored as readable text
The recording run itselfSees the same decompressed response the replaying run will see — ADR-0014
Content-LengthCorrected to describe the decompressed bytes
deflateBoth spellings found in the wild are accepted
Compression is what you are testingdecodeCompressedResponse: false turns it all off
Encoding this build cannot decompressStored exactly as it arrived — not half-processed, not rejected

Transport errors

Off by default: a failure reaches the caller and nothing is written. With recordTransportErrors: true:

SituationBehaviour
PSR-18 network failureRecorded in place of a response, category Network
PSR-18 request failureRecorded, category Request
ReplayThrows http-vcr’s own exception implementing the matching PSR-18 interface
The original client’s exception classNever reconstructed — ADR-0015
An exception that is neither kindNot recorded. It propagates untouched
A recorded failure on replayConsumed like any other interaction

Repeats and consumption

SituationBehaviour
Same request twice, two recordingsReplayed in the order they were made
Asking once more than was recordedFails, saying the cassette is exhausted
Two VcrClient instancesSeparate sessions — they do not share consumption
repeatablePlaybackOne interaction answers as often as it is asked
A single interaction marked repeatable in the dataSame, for that interaction only
A recording session asking twiceRecords twice; it never replays what it just recorded — ADR-0009
…unless the cassette is repeatableThen one recording serves the repeats

Strict mode

SituationBehaviour
AllPlayed, everything replayedPasses
AllPlayed, leftoversFails, naming the interactions nothing asked for
AllPlayed on a cassette the test never touchedFails
A repeatable interactionCounts as played once it has been replayed at all
InOrder, sequence matchesPasses
InOrder, out of orderFails, naming the pair that came out backwards
InOrder, something missing entirelyIgnored — that is AllPlayed’s job, not ordering’s
What the session recorded itselfNot judged
The assertion failsThe lock is still given back — ADR-0012

Staleness

SituationBehaviour
Stale cassette, no enforcementReplays as usual
VCR_ENFORCE_STALE_CHECK setThe same cassette becomes a failure, naming the interaction
Ignore-stale set as wellIgnoring outranks enforcement
Inside the thresholdPasses under enforcement
Mixed agesOnly the interaction that outlived the threshold is reported
First recordingEnforcement has nothing to say about it
Two tests declaring different staleAfter for one cassetteReported and skipped, not silently resolved

Scoping

SituationBehaviour
A scope is resolvedIt becomes part of the filename
Two scopes of one cassetteSeparate files, independent locks and counters
A scope already on diskReplays with no real request
PlaybackOnly with a missing scopeError lists the scopes that do exist
Recording blockedBlames the variable and still lists the scopes
A request the resolver does not scopeUses the cassette’s own file
Strict modeChecked per scope file, not over one pool
A scope that cannot be a filenameRefused, not mangled into a surprising path
A scope that can be sanitisedReduced to a single path segment

Credentials

SituationBehaviour
A provider key is missing while recordingThe request is stopped before it goes out
Only one API being recordedOnly that API’s credentials are required
A replaying runNever asks for credentials it will not use
A cassette requiring its own variableWorks with no provider involved
Several missing at onceReported together, not one run at a time

Redaction and matching

The subtle one: redaction is applied to the incoming request too, so both sides match placeholder-to-placeholder — ADR-0007.

SituationBehaviour
A secretNever reaches the cassette file
A two-way ruleGives the code under test the real value back from the response
A redacted header, query param or form fieldStill matches on replay
The recording runStill sees the real response
AuthorizationRedacted with no configuration at all
An auto-redacted headerStops telling two otherwise-identical requests apart
includeSensitiveHeaders()Stores it as sent, so it tells them apart again
A project-wide rule in http-vcr.phpApplies without touching the client

Lifecycle

SituationBehaviour
Configuring after the first requestLogicException naming the method — ADR-0006
A satellite from withInner() going out of scopeDoes not end the session
A request through a satelliteFreezes configuration for the whole session
close()Releases the lock and checks strict mode
__destruct()Releases the lock only, never asserts

Forced re-recording (VCR_ERASE_TAPE)

SituationBehaviour
A named cassetteRecorded from scratch
A cassette the selector does not nameReplayed as usual
A locked interactionSpared, and keeps being replayed
A provider nameSelects every host it covers, leaving other APIs in the same cassette alone
SurvivorsKeep their order at the front; fresh recordings follow
A fully locked cassetteLeft exactly as it was, and says the erase came to nothing
Recording disabledThe cassette is left alone rather than erased
A bad value for the variableInvalidArgumentException — not a new exception type