jaeger/g-http
jaeger/g-http is a lightweight Guzzle-based HTTP client for PHP with a simple API for GET/POST requests, headers, cookies, proxies, and concurrent async calls, making it useful for scraping, API integration, and Laravel apps.
jaeger/g-http (Jae\GHttp\GHttp) is a thin convenience wrapper around Guzzle, so installation is a single Composer line:
composer require jaeger/g-http
There is no service provider, no Facade, and no config/ file — it is a plain PHP class you instantiate yourself. So the first step in a Laravel app is to decide where it lives:
vendor/jaeger/g-http/src/GHttp.php first. It is small; read the actual public method signatures before writing anything. The mental model to confirm: a new GHttp() object holds default headers/cookies/timeout/proxy/retry settings, and exposes get(), post(), put(), delete(), etc., each returning a Guzzle ResponseInterface.new GHttp() in a service) rather than scattering imports.use Jae\GHttp\GHttp;
$http = new GHttp();
$http->setHeaders(['Accept' => 'application/json']);
$response = $http->get('https://api.example.com/v1/status');
$data = json_decode($response->getBody()->getContents(), true);
If you are already inside Laravel and only need this for one call, stop and use the framework's Http:: facade instead — it covers the same ground with better ergonomics and testing support.
Wrap it in a dedicated client class. Because the package has no container integration, the cleanest day-to-day pattern is a small service that owns a configured GHttp instance:
class BillingClient
{
private GHttp $http;
public function __construct()
{
$this->http = new GHttp();
$this->http->setHeaders([
'Authorization' => 'Bearer '.config('services.billing.token'),
'Accept' => 'application/json',
]);
$this->http->setTimeout(10);
$this->http->setConnectTimeout(3);
}
public function invoice(int $id): array
{
$res = $this->http->get("https://billing.example.com/invoices/{$id}");
return json_decode($res->getBody()->getContents(), true);
}
}
Bind BillingClient as a singleton in a service provider if you want connection reuse semantics to be predictable.
Use it where the Laravel HTTP client is a poor fit. Good fits: artisan commands, queue jobs that shell out, standalone scripts bootstrapped against the Laravel container, or package code that must not depend on Illuminate\Support\Facades\Http. For anything that needs Http::fake() in tests, prefer the framework client.
Configuration as defaults, not per-call. The package's setters (setHeaders, setCookies, setProxy, setTimeout, setConnectTimeout, setVerify, setRetry) mutate instance state, so treat each instance as a configured profile. Build one "authed + JSON" instance and one "public + short timeout" instance rather than reconfiguring a shared object mid-request.
Retry at the boundary. If the wrapper exposes a retry setting, use it only for idempotent GETs. For POSTs, wrap the call in your own try/catch and retry() (Laravel helper) so you control backoff and side-effect safety.
Response handling. Returns are Guzzle responses, so the same idioms apply: $response->getStatusCode(), $response->getBody()->getContents() (consumes the stream — read once), and header access via $response->getHeaderLine('...'). Cast JSON with json_decode(..., true, 512, JSON_THROW_ON_ERROR) inside your wrapper so failures surface as your exception type.
php artisan vendor:publish, no config, no Facade. If you want GHttp::get(...)-style static access, build your own facade or a app()->make() accessor.GuzzleHttp\Exception\* (ClientException / ServerException) unless http_errors is disabled on the underlying client. Catch RequestException and log $e->getResponse() body — otherwise you get opaque 500s in your app.getContents() twice returns an empty string the second time. Decode once and pass the array around.verify to false for convenience. Check setVerify() behavior in source; in production you want verification on, and if a vendor cert is broken, fix the trust store rather than disabling verification globally.composer.json, and be ready to fork: the entire library is one file, so replacing it with a 40-line Guzzle wrapper is a realistic escape hatch.debug option (if exposed via client construction) logs raw request/response to stderr; otherwise use Laravel's Http::fake() on the equivalent framework call, or drop a Log::debug() around status code + body length in your wrapper. $response->getConfig()/$client->getConfig() helps confirm headers actually sent.BillingClient wrapper, not GHttp. That's another reason to wrap early — it isolates you from the package's API surface.How can I help you explore Laravel packages today?