Http::fake() and when the mock needs to be a real URLHonest split first: Http::fake() is one of the nicest HTTP stubbing APIs anywhere. URL patterns, Http::response(), fakeSequence(), assertSent() โ for fast offline feature tests of code that calls external APIs through the Http facade, it's exactly the right tool and this page is not going to pretend otherwise.
Everything below was verified the week of writing (Aug 2026, Laravel 13.23, PHP 8.3, PHPUnit 12) โ every snippet on this page was actually run.
This is the one that bites in CI. Fake one host and the default for every other request is passthrough โ no warning, no failure:
Http::fake([
'api.example.com/*' => Http::response(['ok' => true]),
]);
// NOT matched by the fake โ and Laravel silently sends it for real:
$res = Http::get('https://mockbird.mockbird.workers.dev/m/demo/products?limit=2');
$res->status(); // 200 โ from the real server
count($res->json()); // 2 โ real records came back
We ran exactly that: the unmatched request returned live seeded data over the network, mid-test. If that URL had been a payment provider, the test would have charged someone. The fix is one line โ put it in your base TestCase::setUp():
Http::preventStrayRequests();
// now any unfaked request throws:
// StrayRequestException: "Attempted request to [https://โฆ] without a matching fake."
Http facade is faked โ Guzzle and curl sail past itWe faked everything (Http::fake(['*' => โฆ])) and then made the same request three ways:
Http::fake(['*' => Http::response(['faked' => true])]);
Http::get($url)->json(); // ['faked' => true] โ
faked
(new \GuzzleHttp\Client())->get($url); // real network โ not faked
curl_exec(curl_init($url)); // real network โ not faked
Any SDK that news up its own Guzzle client (most payment/shipping/analytics SDKs), any legacy service class using raw curl, any Symfony HttpClient call โ Http::fake() never sees them. It hooks Laravel's HTTP client factory, not PHP's network layer.
This is the Dusk question that lands people here. Http::fake() mutates state inside the process running the test. Laravel Dusk drives a real browser against an app served by a different PHP process โ the fake cannot reach it. Same story for queue workers, scheduled commands, Octane workers, and your Vue/Livewire frontend's own fetch calls.
We proved it the direct way: an app route that calls Http::get() upstream, served by php artisan serve; the test process ran Http::fake(['*' => โฆ]) and then hit that route like a browser would:
// routes/web.php (running under `php artisan serve` โ a separate process)
Route::get('/proxy-products', function () {
$res = Http::get('https://mockbird.mockbird.workers.dev/m/demo/products?limit=2');
return $res->json();
});
// In the test process:
Http::fake(['*' => Http::response(['faked' => true])]);
$body = json_decode((new \GuzzleHttp\Client())
->get('http://127.0.0.1:8909/proxy-products')->getBody(), true);
$body[0]['price']; // real upstream data โ the fake never applied
No amount of Http::fake() configuration fixes any of these three โ the mock has to live somewhere every process and every client can reach: a URL.
# one curl, no signup โ a whole seeded e-commerce backend
curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
-H 'content-type: application/json' -d '{"name":"shop","preset":"ecommerce"}'
# โ {"id":"abc123","adminKey":"KEY", ...} โ save both
curl https://mockbird.mockbird.workers.dev/m/abc123/products?limit=3
# โ 3 seeded products, CORS on, writes persist
Then wire it the way Laravel already wants external APIs wired โ config/services.php plus an env var:
// config/services.php
'catalog' => [
'base_url' => env('CATALOG_API_URL', 'https://api.yourapp.com'),
],
// anywhere in the app
$res = Http::baseUrl(config('services.catalog.base_url'))->get('/products');
# .env.testing / .env.dusk.local / CI
CATALOG_API_URL=https://mockbird.mockbird.workers.dev/m/abc123
Now the Dusk browser, the queue worker, the frontend's fetch, the Guzzle-based SDK and your terminal all see the same mock โ because it's just a URL. Prefer clicking? This link creates the same project in your browser โ no account. Have an OpenAPI spec? Paste it at /app#import.
A faked response returns instantly, so retry waits and timeout durations never actually run. Against a hosted URL, real time passes โ all three of these are from the verified test suite:
// a real 503, no fake handler to write
$res = Http::get('โฆ/m/demo/products?mock_status=503');
$res->serverError(); // true
// a real timeout: response held for 3s, 1s timeout genuinely fires
Http::timeout(1)->get('โฆ/m/demo/products?mock_delay=3000');
// โ Illuminate\Http\Client\ConnectionException
// Http::retry() against a genuinely flaky endpoint:
// mock_chaos=0.5 fails ~half of requests with real 5xx/429s
$res = Http::retry(5, 100, throw: false)
->get('โฆ/m/demo/products/1?mock_chaos=0.5');
$res->status(); // 200 โ five runs in the suite, five 200s after retries
More recipes in testing loading & error states.
Http::fakeSequence() scripts responses in order โ call it a third time when you pushed two and you get OutOfBoundsException: A request was made, but the response sequence is empty (verified). And a faked POST changes nothing: the next GET returns the same canned body, because there is no store behind the script. Against Mockbird the write is real:
$created = Http::post("$base/products", ['name' => 'Laravel guide test product', 'price' => 9.99]);
$id = $created->json('id'); // 201, real id
Http::get("$base/products/$id")->json('name'); // the record is actually there
Http::delete("$base/products/$id"); // and cleanup is real too โ then GET โ 404
For deterministic test data across runs (and parallel Dusk workers), save a named snapshot and pin it per-request with the X-Mockbird-Snapshot header โ deterministic test data guide.
Http::fake() concepts โ Mockbird| Laravel | Mockbird | Notes |
|---|---|---|
Http::fake(['host/*' => Http::response($data)]) | a resource on a real URL | list/get/create/update/delete generated, plus filtering, sorting, pagination, relations |
| Hand-rolled response arrays | seeded realistic records | faker-style names/emails/prices/dates; or import your exact records from db.json/CSV/OpenAPI |
Http::response([], 500) | ?mock_status=500 | on any URL, no handler edit |
| (no latency simulation) | ?mock_delay=3000 / ?mock_jitter | real time passes โ Http::timeout() genuinely fires |
| (no flakiness simulation) | ?mock_chaos=0.5 | real 5xx/429s for Http::retry() to absorb |
fakeSequence() | snapshot pinning | named data states, no script-exhaustion exception, safe across parallel workers |
Http::assertSent(fn โฆ) | request inspector | last 50 requests with method, path, query, headers, body |
preventStrayRequests() | n/a | keep using it! it guards the fakes you keep |
curl 'https://mockbird.mockbird.workers.dev/m/demo/products?limit=2&select=name,price'
curl -i 'https://mockbird.mockbird.workers.dev/m/demo/products?mock_status=503'
curl 'https://mockbird.mockbird.workers.dev/m/demo/products/1?mock_delay=2000'
All against the shared demo project (resets daily).
Http::fake() for millisecond feature tests of facade-based code โ with preventStrayRequests() on. Point Dusk, queue workers, frontend fetches, Guzzle-based SDK configs and dev servers at a Mockbird base URL via config/services.php. App code doesn't change.Http::fake() that one host and let the rest of the suite talk to the hosted mock.GET /m/<project>/db.json ejects your entire dataset any time; openapi.json and postman.json are generated from your live schema.Http::fake() | Mockbird | |
|---|---|---|
| What it is | built into Laravel | free hosted service |
| Reachable from | the Http facade, in the test process only | anything with HTTP: browser, Dusk, curl, Guzzle, SDKs, queue workers, mobile, CI, teammates |
| Guzzle / curl / SDK-internal clients | not intercepted (verified) | it's a real URL โ every client works by definition |
| Unmatched request | silently hits the real network (default; verified) | n/a โ real endpoints answer real queries |
| Setup | write + maintain fake arrays per test | one curl or one click; no code |
| Stateful CRUD | no โ scripted answers only | default โ writes persist |
| Latency/retry/timeout testing | instant responses โ durations never run | ?mock_delay/?mock_jitter/?mock_chaos, real time passes |
| Works offline | yes | no โ it's a real network call |
| Request assertions | assertSent(), precise, in-test | request inspector (last 50, headers/body) |
| Request cap | none | 10,000/project/day |
Written by the Mockbird maker โ bias disclosed. Where Http::fake() genuinely wins: it ships with Laravel, runs offline at zero latency, assertSent() closures are more precise than any log-based check, and a fake callback is arbitrary PHP. For fast feature tests of facade-based code it should stay your default. When the thing you need is a URL โ for Dusk, a queue worker, a Guzzle SDK, a frontend fetch, a teammate, or CI against a deployed preview โ that's us.
Full API reference in the docs. More guides: mock API for PHP ยท mock APIs in Cypress ยท mock APIs in Playwright ยท deterministic test data ยท testing loading & error states ยท free mock API tools compared. Create your API โ