vinelab/http
Vinelab HTTP is a lightweight Laravel HTTP client that simplifies sending GET, POST, PUT, PATCH, and DELETE requests, handling headers, query parameters, JSON payloads, authentication, and response parsing through a fluent, expressive API.
Install the package:
composer require vinelab/http
Register the service provider (this package predates Laravel package auto-discovery, so add it manually to config/app.php):
'providers' => [
// ...
Vinelab\Http\ClientServiceProvider::class,
],
Optionally publish the config:
php artisan vendor:publish --provider="Vinelab\Http\ClientServiceProvider"
This produces config/http.php, where you set defaults like user agent, timeouts, and redirect behaviour.
First request — resolve the client from the container and call a verb:
use Vinelab\Http\Client;
$client = app(Client::class);
$response = $client->get('https://api.example.com/users');
if ($response->statusCode() === 200) {
$users = $response->json(); // already-decoded array
}
Where to look first: src/Client.php for the request verbs and how parameters are encoded, src/Response.php for the accessor methods (json(), xml(), body(), statusCode()), and config/http.php once published. The response accessors are the whole surface area of the package — read them before writing anything clever.
Best first use case: a thin wrapper around one third-party JSON API in a legacy (Laravel 5.x–5.6 era) application. That is where it fits and where $response->json() saves you a line of boilerplate versus plain Guzzle.
Constructor-inject the client into a service class. The container resolves it automatically, which keeps your code testable and hides the client:
class GitHubApi
{
public function __construct(private Client $client) {}
public function repos(string $org): array
{
$response = $this->client->get("https://api.github.com/orgs/{$org}/repos");
return $response->statusCode() === 200 ? $response->json() : [];
}
}
JSON requests are implicit. Pass an associative array as the params argument and the package encodes it as JSON and sets the content type for you:
$response = $client->post('https://api.example.com/orders', [
'sku' => 'ABC-1',
'quantity' => 2,
]);
For raw bodies (XML, form-encoded strings, pre-serialized JSON) pass a string instead and set the content type through the options/headers argument.
XML round-trip. Pass an XML string and read it back with xml(), which returns a SimpleXMLElement:
$response = $client->post('https://legacy.example.com/soap', $xmlPayload);
$node = $response->xml()->result->status; // cast to string when comparing
Centralize auth and headers. Rather than repeating tokens on every call, build a small factory or set defaults in config/http.php:
$client = app(Client::class);
$response = $client->get($url, [], [
'headers' => ['Authorization' => 'Bearer '.config('services.foo.token')],
]);
Status-aware handling. Treat statusCode() as the contract; the convenience of json() is that you don't decode manually, not that errors are handled for you:
$status = $response->statusCode();
return match (true) {
$status === 200 => $response->json(),
$status >= 400 && < 500 => throw new ApiClientError($response->body()),
default => throw new ApiServerError($response->body()),
};
Testing. Because the client is container-resolved, bind a fake in your test base class or swap it per test with Mockery:
$this->mock(Client::class, function ($mock) {
$mock->shouldReceive('get')->andReturn($fakeResponse);
});
Keep a small helper that builds a Response from a fixture body so tests don't hit the network.
Http facade (Illuminate\Support\Facades\Http) instead — it is a strict superset of this package's functionality. Only reach for vinelab/http in projects already pinned to it.ClientServiceProvider by hand; a missing provider shows up as "Target [Vinelab\Http\Client] is not instantiable".Response is a bespoke wrapper, so middleware, Laravel's assertSent helpers, and Guzzle's getBody() idioms don't apply. If you need PSR-7, drop down to Guzzle directly.json() on non-JSON returns null, silently. It does not throw. If an upstream API returns HTML on error (very common with proxies and CDNs), you'll get null and no warning — always check statusCode() before trusting the decoded payload.ConnectException. Wrap calls in try/catch (\Exception $e) at the boundary.['timeout' => 5, 'connect_timeout' => 2] in the options array.'verify' => false when hitting staging with a self-signed cert — scope that to a named config value, never to production defaults.$response->xml()->status is a SimpleXMLElement; compare as (string) $response->xml()->status or your === checks will mysteriously fail.json() returns null, dump $response->body() and $response->statusCode() first. If you need wire-level logs, pass a Guzzle handler option through the options array rather than patching the package.Client, override the verb methods, and rebind Client::class in AppServiceProvider — not to edit files under vendor/.How can I help you explore Laravel packages today?