Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Http Playback Laravel Package

garethp/http-playback

Record real HTTP responses once, then replay them deterministically in your PHP tests. GarethP's HTTP Playback makes it easy to capture and reuse external API interactions, keeping test suites fast, reliable, and free of network flakiness.

View on GitHub
Deep Wiki
Context7

Getting Started

Install (dev-only — you never want this in production):

composer require --dev garethp/http-playback

Version to pin: v2.0 is the current release and is the one you want — it moves the package to Guzzle 7 and adds PHP 8.x support, dropping PHP 5.x. If you're still on Guzzle 6 (common when another dependency, including older Laravel releases, has it locked), v2.0 will not install; you'll get a Composer conflict and have to either upgrade Guzzle or stay on the 1.x line, which is now the unsupported branch. Check composer why guzzlehttp/guzzle before you start.

Prerequisites: PHP 7.x/8.x (8.x only from v2.0) and Guzzle 7. The package is a thin layer over Guzzle's handler stack — it records real HTTP traffic to disk and replays it later. If your code doesn't make requests through Guzzle, this package can't help you.

Where to look first: the README usage example, then src/ — the interesting bits are the recording middleware (writes fixtures) and the playback handler (reads them). Everything else is a filesystem wrapper.

Minimal flow:

  1. Pick a fixtures directory, e.g. tests/fixtures/http.
  2. Run your code once in record mode against the live API; each request/response pair is dumped to that directory.
  3. Run the same code in playback mode; matching requests return the stored response instead of hitting the network.
use Garethp\HttpPlayback\HttpPlayback;

$playback = new HttpPlayback(__DIR__ . '/fixtures/http');

// Step 1 — record once, against the real API
$client = $playback->record()->getClient();
$client->get('https://api.example.com/v1/users');

// Step 2 — from now on (locally and in CI), replay offline
$client = $playback->playback()->getClient();
$response = $client->get('https://api.example.com/v1/users');

The exact mode API (chainable record()/playback() vs. separate Record/Playback classes) has shifted across versions — confirm against the README for the tag you pull. Everything below is independent of that detail.

First use case: an integration test or artisan command that hits a flaky third-party API. Record a happy path plus one or two error paths, commit the fixtures, and the test suite stops depending on the network, tokens, or rate limits.

Implementation Patterns

Upgrade deliberately, then re-record. v2.0 is a hard major bump with no deprecation path: Guzzle 6 is gone and the handler stack it patches is Guzzle 7's. The release notes say nothing about fixture format, so assume the on-disk files still parse but that matching may drift — the safest sequence is bump the constraint, run the existing playback suite, and re-record any scenario that misses. Do the bump as its own commit so a fixture churn diff doesn't hide inside a feature PR.

Swap the client at the boundary, not the call sites. Build your API service around an injected GuzzleHttp\ClientInterface. In production you bind a normal client; in tests you bind the playback client. That single seam means no test ever needs to know HttpPlayback exists.

// AppServiceProvider
$this->app->bind(AcmeApi::class, function () {
    $client = app()->environment('testing')
        ? (new HttpPlayback(base_path('tests/fixtures/http')))->playback()->getClient()
        : new \GuzzleHttp\Client(['base_uri' => config('services.acme.url')]);

    return new AcmeApi($client);
});

Record deliberately, replay always. Never let record mode be the default. Add a dedicated command (php artisan http:record acme) or a Composer script that flips into record mode, runs a known-good scenario, and exits. Normal test runs are playback-only. This keeps fixtures intentional and diffs reviewable.

Organise fixtures per scenario, not per endpoint. One directory per feature or test class:

tests/fixtures/http/
  checkout_success/
  checkout_card_declined/
  checkout_timeout/

Since the store is a plain directory, you can point a test at a specific scenario by constructing the playback object with that path. Much easier to reason about than one giant flat dump.

Treat fixtures as code. They get committed, reviewed in PRs, and refreshed on a schedule (or when the upstream contract changes). A composer record script that regenerates them all and a CI job that runs it monthly against staging is a cheap contract test.

Combine with your normal assertions. Playback returns a real Response object, so your existing assertions (assertStatus, json(), getBody()) work unchanged. The only difference is latency and determinism.

Test error paths cheaply. Because you can hand-write or record a 429/500/timeout fixture, this is the easiest way to exercise retry and backoff logic without a fault-injection proxy.

public function test_retries_on_429(): void
{
    $client = (new HttpPlayback(__DIR__ . '/../fixtures/http/rate_limited'))->playback()->getClient();
    $api = new AcmeApi($client);

    $this->expectNotToPerformAssertions();
    $api->fetchUsers(); // must retry internally and eventually succeed
}

Gotchas and Tips

  • Matching is the whole game. Fixtures are keyed by request fingerprint — usually method + URI, sometimes body and headers. Query-string parameter order, trailing slashes, and default headers can all cause a miss. If a replay silently hits the network or throws "no fixture found", dump the key it computed and compare it to the key on disk.
  • The Guzzle 6 → 7 header change is the upgrade trap. Guzzle 7 stopped sending some default headers that Guzzle 6 emitted (notably User-Agent). If your matcher considers headers, or your recorded fixtures were captured under Guzzle 6, expect a wave of misses immediately after moving to v2.0. Re-record rather than hand-patching keys.
  • Volatile values poison fixtures. Timestamps, request IDs, pagination cursors, and OAuth tokens baked into a recording will break assertions tomorrow. Scrub them on write, or make your assertions ignore those fields rather than asserting exact bodies.
  • Secrets end up in fixtures. Authorization, Set-Cookie, API keys, and PII all get serialised verbatim. Add a sanitising step (or a .gitattributes/grep check in CI) before the first commit. Treat a fixture directory as you would a snapshot of production data.
  • v2.0 fixes the maintenance smell, not all of it. PHP 8 support and Guzzle 7 remove the biggest blocker, so it's now reasonable to adopt. But it's still a one-maintainer package with a long gap between releases (previous release was May 2021) — pin the exact version, and note that "PHP 8.x compatible" in the notes likely means 8.0/8.1-era; validate on 8.2/8.3 and expect deprecation notices. The codebase is small enough to vendor if upstream goes quiet again.
  • Record mode is a live-fire exercise. Guard it behind an explicit env flag (HTTP_PLAYBACK_RECORD=1) and assert in CI that recording is off. A misconfigured pipeline that quietly re-records will overwrite good fixtures with error pages.
  • Laravel's Http facade is Guzzle underneath but you can't inject a client through it directly. Either drop to Http::withHandler(...)/buildClient() to install the playback handler, or refactor that call site onto an injected Guzzle client. Don't assume the facade picks this up automatically. On a modern Laravel (Guzzle 7 under the hood) the facade and v2.0 are finally on the same major, so withHandler is more likely to work than it was against 1.x.
  • Async/promise and streaming are likely unsupported. The record-and-replay model assumes a fully buffered response. Multipart uploads, chunked downloads, and Pool/promise concurrency are areas to test manually before relying on this anywhere important.
  • Parallel test runs share a fixture directory. Playback is read-only so it's mostly safe, but any test that writes (accidentally in record mode) will race. Keep record mode out of the parallel suite.
  • What happens on a miss matters. Determine early whether a missing fixture throws, returns a 404-looking response, or falls through to the live network. Only one of those is acceptable for CI; if it's fall-through, wrap the client so misses are loud.
  • Extension points are your escape hatch. The storage and matcher are the two customisable pieces — swapping the storage layer gets you database-backed or S3-backed fixtures, and a custom matcher lets you ignore volatile query params or headers instead of scrubbing them from recordings. Any custom handler/matcher you wrote is Guzzle-7-shaped now, so review it during the upgrade; expect to maintain it yourself.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings