kipchak/driver-http
A lightweight HTTP driver for Kipchak applications that provides a clean, configurable interface for making outbound requests and handling responses. Integrates with the framework’s driver system to keep HTTP client setup consistent and simple across your PHP projects.
kipchak/driver-http gives you named HTTP "drivers" — small classes that bundle a base URI, default headers, auth, and options — so every call to, say, Stripe or an internal service goes through one configured object instead of Http::withHeaders(...)->post(...) scattered across the codebase. It sits on top of Laravel's HTTP client, so the idioms you already know (Http::fake(), ->retry(), ->throw(), ->json()) still apply.
Install:
composer require kipchak/driver-http
Laravel 11+ auto-discovers the service provider. If not, append the provider found under vendor/kipchak/driver-http/src to bootstrap/providers.php.
Read these three things before configuring anything, in this order:
vendor/kipchak/driver-http/src. This is the real API — it tells you what a driver is: which properties/methods control base URI, headers, auth, and defaults.default vs named connection). It's where you'll plug your driver in.composer.json. Its constraint on the Kipchak core package is the compatibility surface you actually care about — everything else can be adapted, that constraint cannot.Publish the config if a tag ships:
php artisan vendor:publish --tag=kipchak-http
First real use case: a single driver against one non-critical upstream, and one GET through it. The shape below is illustrative — verify property names against the package's base class before committing to them:
namespace App\Http\Drivers;
use Kipchak\Driver\Http\Driver;
class StatusDriver extends Driver
{
protected string $baseUri = 'https://status.example.com';
protected array $headers = ['Accept' => 'application/json'];
}
Then, in a test or an artisan command:
Http::fake(['status.example.com/*' => Http::response(['ok' => true])]);
$response = app(StatusDriver::class)->get('/health');
$response->throw()->json();
If Http::fake() intercepts the call, the driver really is layered on the framework client and every Laravel HTTP testing idiom is available. If it doesn't, stop and identify the actual transport (Guzzle, PSR-18, custom) before building further on top of it. That single fact determines how you fake, log, and debug everything downstream.
One driver per upstream. Resist the urge to build a generic ApiDriver with a settable base URI. The value is that StripeDriver always points at Stripe with the right auth and InternalUserDriver always points at your user service with the right headers. Config drift between call sites is exactly what you're eliminating.
Drivers should expose domain methods, not just verbs. $driver->get('/v1/charges') is fine for a spike; the shape that survives review is:
public function charge(int $amount, string $currency = 'usd'): array
{
return $this->post('/v1/charges', [
'amount' => $amount,
'currency' => $currency,
])->throw()->json();
}
This makes the driver the single place the external contract is maintained, and it gives you an obvious seam to mock when testing application code.
Inject drivers, don't resolve them inline. Bind drivers in a service provider or rely on container resolution through constructor type-hints. app(StripeDriver::class) buried inside a controller is the same anti-pattern as new GuzzleHttp\Client().
Push cross-cutting behavior into the driver, not the call site. Auth, Accept headers, base paths, timeouts, retry policy, and user-agent belong in the driver's config. If you find yourself writing ->withHeaders([...]) or ->withOptions([...]) at the call site, the driver is under-specified — move it up.
Per-environment drivers. Because a driver is just a class, you can point it at sandbox or production via config. Do the switch in config or a driver bootstrap hook — never if (app()->isProduction()) inside a domain method.
Testing, from most to least isolated:
$this->mock(StripeDriver::class, fn ($m) => $m->shouldReceive('charge')->andReturn([...])). Fastest unit test for code that calls the driver.Http::fake() plus Http::assertSent(fn ($req) => $req->url() === 'https://api.stripe.com/v1/charges'). Slower, but the only thing that verifies the driver forms the correct request. Use this for anything where URL, headers, or body shape is part of the contract.Http::preventStrayRequests() + a fake). Reserve for the endpoints you can't reason about confidently.Error-handling wrapper. Put a thin application-layer client between the driver and your callers that normalizes failures — ConnectException, 4xx, 5xx, rate limits — into your own exception types. The driver is a transport concern; your controllers shouldn't know whether the upstream returned 429 or 503.
Logging and correlation IDs on day one. Attach request/response logging at the driver level (beforeSending callback, a middleware, or Http::globalRequestMiddleware if the driver uses the framework client) so every call is traceable without touching call sites. Propagate an X-Request-Id from the incoming request into the driver headers. It's the single most valuable line of plumbing you can add in the first hour.
"Driver" here is not "Laravel HTTP client." The package is a driver abstraction over the client; Http:: is still what you use for ad-hoc calls. Don't assume every Illuminate\Http\Client feature is mirrored. Explicitly test the ones you depend on: retries, middleware, fake, exception-on-status, multipart, streaming.
Score 0, zero stars, zero dependents — treat it as unmaintained until proven otherwise. Pin an exact version ("kipchak/driver-http": "0.3.1"), add it to your dependency-risk register, and read the diff of every upgrade. You are the QA for changes.
The constraint that matters is on the Kipchak core package, not this one. A driver's API surface is defined by the interface it implements. If this driver and the core drift even one minor version apart, you get a fatal error on boot, not a subtle bug. Lock both together, upgrade together, and read the core's CHANGELOG before either moves.
Config caching. php artisan config:cache freezes env values. When you change STRIPE_BASE_URI and "nothing happens," this is why. Clear and recache — and never env() outside config files.
Add Http::preventStrayRequests() to your test base class during the trial period. It turns "silently hit the sandbox from a CI run" into a failing test, which is what you want while you're still discovering the package's boundaries.
Exception semantics vary by underlying client. Guzzle-style clients throw on 4xx/5xx by default; Laravel's client returns a response unless you call ->throw(). Whichever the driver inherits, decide explicitly and normalize it in your wrapper. Half your code checking $response->failed() and half expecting exceptions is a future incident.
Check where auth is resolved. Bearer tokens and API keys can be resolved at driver construction, at request time, or via a callback. For OAuth2 flows with expiring tokens, verify you can refresh without rebuilding the driver — otherwise you end up with a driver-per-request pattern that defeats the whole point.
Retries amplify outages. Configure retries per driver with a bounded count and jitter, and don't let the driver blindly retry non-idempotent POSTs that create things. Most HTTP clients default to no retries for exactly this reason.
When the docs run out, decorate the driver. Five lines that survive package upgrades and don't depend on internals:
class LoggingDriver
{
public function __construct(private object $inner) {}
public function __call($m, $a)
{
Log::debug('http.request', ['driver' => $this->inner::class, 'method' => $m, 'args' => $a]);
return $this->inner->$m(...$a);
}
}
Your extension point is the driver base class, not the underlying client. When you need behavior the package doesn't ship — a signature scheme, a specific retry policy, an observability hook — subclass the driver rather than reaching past it. Keeping all of your code on your side of the driver boundary is cheap insurance against the realistic outcome here: forking and owning the package internally.
How can I help you explore Laravel packages today?