guzzlehttp/guzzle
Guzzle is a PHP HTTP client that makes sending HTTP requests simple and integrating with web services trivial. It supports sync and async requests, PSR-7 and PSR-18 interfaces, streaming uploads and downloads, middleware, and abstracts away the underlying transport.
Installation (major version bump — review before upgrading):
composer require guzzlehttp/guzzle:^8.3
Target the 8.3.x line. Before upgrading from a 7.x install, read the Breaking Changes for proxy/multiplexing/streaming behaviour, which remains enforced in 8.x.
Upgrade note for 8.3.0 — dependency floors moved:
guzzlehttp/promises is now ^3.1 and guzzlehttp/psr7 is now ^3.2.composer update guzzlehttp/guzzle guzzlehttp/promises guzzlehttp/psr7 --with-dependencies and check for conflicts with packages that pin the old constraints.GuzzleHttp\Promise\Is directly, switch to GuzzleHttp\Promise\State now (the Is class is deprecated for PHP 8.6):use GuzzleHttp\Promise\State;
// before: Promise\Is::pending($promise)
State::pending($promise); // true/false
Upgrade note for 8.2.0 (still relevant): connection caps can now be combined with persistent transport sharing on libcurl 8.22.0+. On older libcurl, caps continue to implicitly disable transport sharing — no code change required, but check the runtime version if you rely on connection reuse:
version_compare(curl_version()['version'], '8.22.0', '>='); // true => caps + sharing allowed
First Request (Unchanged):
use GuzzleHttp\Client;
$client = new Client();
$response = $client->get('https://api.example.com/data');
$data = json_decode($response->getBody(), true);
Key Entry Points:
Client: Core class; stricter multiplexing/connection validation.Promise\State replaces the deprecated Promise\Is; async completion callbacks no longer double-settle.stream => true is rejected on cap-configured handlers; StreamHandler bodies are now validated like libcurl (chunked, Content-Length, encodings).StreamHandler decoding/truncation behaviour changed in 8.3.0.Where to Look First:
CURLMOPT_* settings.StreamHandler docs if you run without ext-curl (many shared hosts / CI containers) — most 8.3.0 changes land there.$response = $client->request('GET', '/users', [
'query' => ['role' => 'admin'],
'headers' => ['Accept' => 'application/json'],
]);
$stack->push(Middleware::tap(function ($request) {
// Credentials in URIs (e.g., `http://user:pass@...`) are always redacted in logs.
logger()->debug('Request URI:', ['uri' => (string) $request->getUri()]);
}));
$client = new Client([
'proxy' => 'http://proxy.example.com:8080',
'proxy_credentials' => 'user:pass', // Normalized before reuse checks.
]);
$promises = [];
foreach ($urls as $url) {
$promises[] = $client->getAsync($url);
}
$results = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
Is → State (8.3.0). Promise\Utils::isFulfilled() etc. are unchanged, but hand-rolled checks should use the new class:
use GuzzleHttp\Promise\State;
if (State::pending($promise)) { /* ... */ }
// ❌ Throws InvalidArgumentException:
$client->get('https://example.com/large_file', [
'stream' => true,
'handler' => new \GuzzleHttp\Handler\CurlMultiHandler(['capabilities' => ['stream' => true]]),
]);
sink or a handler without caps:
$client->get('https://example.com/large_file', ['sink' => 'file.zip']);
The StreamHandler (used whenever ext-curl is unavailable, or when you explicitly pick it) now behaves like libcurl instead of trusting the wire:
Transfer-Encoding codings (matching libcurl 8.14+).Content-Length (including Content-Length: 0); ending early throws.use GuzzleHttp\Client;
use GuzzleHttp\Exception\ResponseTransferException;
$client = new Client(['handler' => \GuzzleHttp\HandlerStack::create(
new \GuzzleHttp\Handler\StreamHandler()
)]);
try {
$body = $client->get('https://example.com/export.csv')->getBody()->getContents();
} catch (ResponseTransferException $e) {
// Truncated body / bad Content-Length / malformed chunking / trailing gzip data
report($e);
}
Practical guidance:
StreamHandler may now throw where CurlHandler succeeded — that's the point of the change, but it is a behaviour break for sloppy upstreams.Content-Length/gzip edge-case payloads and assert both handlers agree.ResponseTransferException. It means the body you got is incomplete; caching or persisting it silently is how truncated files reach users.The reason to care: previously, configuring connection caps effectively forced you to choose between bounded concurrency and transport reuse (keep-alive / connection pooling). On libcurl 8.22.0+ these can be combined, so a capped client no longer silently loses connection reuse.
$handler = new \GuzzleHttp\Handler\CurlMultiHandler([
// Caps (bounded concurrency) ...
'max_connections' => 10,
// ... now compatible with persistent transport sharing on libcurl >= 8.22.0
]);
$client = new Client(['handler' => $handler]);
Practical guidance:
$client = new Client([
'curl' => [
CURLOPT_HTTPAUTH => CURLAUTH_ANY, // ❌ Rejected if NTLM is permitted.
CURLOPT_PIPELINING => CURLPIPE_MULTIPLEX, // Must be integer.
],
'multiplex' => true,
]);
// ❌ Throws InvalidArgumentException:
$client = new Client(['proxy' => 'http://user:pass@proxy:8080@invalid']);
$app->singleton(GuzzleHttp\Client::class, function ($app) {
return new Client([
'base_uri' => config('services.api.base_url'),
'timeout' => config('services.api.timeout'),
'proxy' => env('HTTP_PROXY'), // Normalized.
]);
});
If you tune caps through a container binding, expose the libcurl capability check as a config flag rather than branching on it inside the factory:
// config/services.php
'api' => [
'caps' => env('HTTP_MAX_CONNECTIONS', 10),
'supports_caps_with_sharing' => version_compare(curl_version()['version'], '8.22.0', '>='),
],
Dependency constraint bumps (promises ^3.1, psr7 ^3.2):
composer update on the 8.3.0 upgrade.composer why guzzlehttp/promises before assuming it's your own constraint.Promise\Is → Promise\State:
GuzzleHttp\Promise\Is is deprecated ahead of PHP 8.6; direct references will warn now and disappear later.GuzzleHttp\Promise\State. Prefer Utils::settle() / Utils::all() so you never touch state classes at all.StreamHandler is now strict — truncated bodies throw:
Content-Length, use malformed chunking, or carry unknown Transfer-Encoding now raise ResponseTransferException instead of returning a short body.ResponseTransferException and treat the payload as invalid rather than partially valid. If an upstream is genuinely broken, fix the upstream (or pin the handler) — don't blanket-catch and continue.cURL errors 8 and 23 are now ResponseTransferException:
CURLE_WEIRD_SERVER_REPLY (8) and CURLE_WRITE_ERROR (23) — when a response is available — are classified as transfer errors. Existing catch (ConnectException|RequestException) blocks may change which branch they hit.ResponseTransferException to retry/alert handling; note both extend TransferException, so catch broadly and inspect.Encoding strictness changed for stream handler only:
libcurl gating on the 8.2.0 feature:
version_compare(curl_version()['version'], '8.22.0', '>=')) and log the result; don't infer capability from the Guzzle version alone.Proxy credential normalization:
@ (e.g. user:pass@proxy@domain) fail closed if unparseable.user:[email protected]; validate before constructing the client.
if (substr_count($proxy, '@') > 1) {
throw new \InvalidArgumentException('Invalid proxy credentials');
}
Multiplexing conflicts:
multiplex => true with non-integer CURLMOPT_PIPELINING, CURLOPT_HTTPAUTH allowing NTLM, or cleartext proxies without CURLOPT_PROXYAUTH.$client = new Client([
'multiplex' => true,
'curl' => [
CURLOPT_PIPELINING => 1,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
],
]);
Streaming restrictions:
stream => true is rejected on handlers with capabilities.sink for downloads, or a handler without caps:
$client = new Client(['handler' => new \GuzzleHttp\Handler\CurlHandler()]);
Credential redaction:
auth instead, and log the host + auth type only.Async promise race conditions:
settle() or all(); never resolve/reject from inside then() on the same promise.StreamHandler; print get_class($handler) (or check extension_loaded('curl')) before debugging a body-length exception.curl_version()['version'] and curl_version()['features'] — the 8.2.0 behaviour is invisible without it.['debug' => true] to surface rejected proxy values ("Proxy credential 'user:pass@proxy@domain' is invalid.").\InvalidArgumentException and echo the message ("Multiplexing conflict: NTLM auth not allowed.").Content-Length, the received byte count, and Transfer-Encoding when catching ResponseTransferException — that triple identifies truncation vs. bad chunking vs. unsupported coding.on_trailers is only used with CurlHandler; StreamHandler throws.StreamHandler no longer waits for the full body, so large chunked responses start streaming immediately — but memory wins only materialise if you consume the stream rather than calling getContents().CurlMultiHandler composes caps with persistent transport sharing on libcurl 8.22.0+; it still rejects stream => true when caps are configured.ResponseTransferException and deciding retry policy.How can I help you explore Laravel packages today?