Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Guzzle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. 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.

  2. Upgrade note for 8.3.0 — dependency floors moved:

    • guzzlehttp/promises is now ^3.1 and guzzlehttp/psr7 is now ^3.2.
    • Run composer update guzzlehttp/guzzle guzzlehttp/promises guzzlehttp/psr7 --with-dependencies and check for conflicts with packages that pin the old constraints.
    • If you reference 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
    
  3. 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
    
  4. First Request (Unchanged):

    use GuzzleHttp\Client;
    
    $client = new Client();
    $response = $client->get('https://api.example.com/data');
    $data = json_decode($response->getBody(), true);
    
  5. Key Entry Points:

    • Client: Core class; stricter multiplexing/connection validation.
    • Middleware: Credential redaction and proxy handling.
    • Promises: Promise\State replaces the deprecated Promise\Is; async completion callbacks no longer double-settle.
    • Streaming: stream => true is rejected on cap-configured handlers; StreamHandler bodies are now validated like libcurl (chunked, Content-Length, encodings).
    • Handlers: connection caps + persistent transport sharing compose on libcurl 8.22.0+; StreamHandler decoding/truncation behaviour changed in 8.3.0.
  6. Where to Look First:

    • Official Docs.
    • Handler/curl option reference for caps and CURLMOPT_* settings.
    • StreamHandler docs if you run without ext-curl (many shared hosts / CI containers) — most 8.3.0 changes land there.
    • Breaking Changes for proxy/multiplexing/streaming updates.

Implementation Patterns

1. Request Construction (Unchanged)

$response = $client->request('GET', '/users', [
    'query' => ['role' => 'admin'],
    'headers' => ['Accept' => 'application/json'],
]);

2. Middleware Workflows

  • Credential Redaction:
    $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()]);
    }));
    
  • Proxy Handling:
    $client = new Client([
        'proxy' => 'http://proxy.example.com:8080',
        'proxy_credentials' => 'user:pass', // Normalized before reuse checks.
    ]);
    

3. Async/Await Patterns

  • Avoid double-settling:
    $promises = [];
    foreach ($urls as $url) {
        $promises[] = $client->getAsync($url);
    }
    $results = \GuzzleHttp\Promise\Utils::settle($promises)->wait();
    
  • Migrate 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)) { /* ... */ }
    

4. Streaming Large Files (restricted)

  • Cap-configured handlers:
    // ❌ Throws InvalidArgumentException:
    $client->get('https://example.com/large_file', [
        'stream' => true,
        'handler' => new \GuzzleHttp\Handler\CurlMultiHandler(['capabilities' => ['stream' => true]]),
    ]);
    
  • Workaround: use sink or a handler without caps:
    $client->get('https://example.com/large_file', ['sink' => 'file.zip']);
    

5. Stream Handler Body Decoding (new in 8.3.0)

The StreamHandler (used whenever ext-curl is unavailable, or when you explicitly pick it) now behaves like libcurl instead of trusting the wire:

  • Chunked bodies are decoded incrementally as they arrive, ending exactly at the last chunk (previously buffered; also fixes bytes that arrived together with the headers being delayed).
  • Truncated or malformed chunked bodies are rejected, as are unsupported Transfer-Encoding codings (matching libcurl 8.14+).
  • Bodies end at their declared Content-Length (including Content-Length: 0); ending early throws.
  • gzip/deflate bodies with trailing data are rejected; raw deflate is now accepted.
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:

  • Keep expectations aligned across handlers. A response that "worked on my machine" via StreamHandler may now throw where CurlHandler succeeded — that's the point of the change, but it is a behaviour break for sloppy upstreams.
  • Test against real fixtures. Record chunked/Content-Length/gzip edge-case payloads and assert both handlers agree.
  • Don't swallow ResponseTransferException. It means the body you got is incomplete; caching or persisting it silently is how truncated files reach users.

6. Connection Caps + Persistent Transport Sharing (8.2.0)

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:

  • Fan-out services (many hosts, bounded sockets): keep the cap and keep the reuse — that is the whole point of this release.
  • Check the runtime, not just the package version. The capability is gated on libcurl, so pin/verify the extension version in CI and on your production images.
  • Per-environment divergence: local Docker images often ship an older libcurl than production. Add a boot-time assertion so you don't tune caps locally against behaviour you won't get in prod.

7. Configuration Management (stricter)

  • Multiplexing requirements:
    $client = new Client([
        'curl' => [
            CURLOPT_HTTPAUTH => CURLAUTH_ANY, // ❌ Rejected if NTLM is permitted.
            CURLOPT_PIPELINING => CURLPIPE_MULTIPLEX, // Must be integer.
        ],
        'multiplex' => true,
    ]);
    
  • Proxy validation:
    // ❌ Throws InvalidArgumentException:
    $client = new Client(['proxy' => 'http://user:pass@proxy:8080@invalid']);
    

8. Integration with Laravel (unchanged)

$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', '>='),
],

Gotchas and Tips

Pitfalls

  1. Dependency constraint bumps (promises ^3.1, psr7 ^3.2):

    • Issue: 8.3.0 raises the floors. Laravel apps with transitive pins (custom PSR-7 middleware, promise helpers, some SDKs) can fail composer update on the 8.3.0 upgrade.
    • Fix: update all three packages together and run the suite; check composer why guzzlehttp/promises before assuming it's your own constraint.
  2. Promise\Is → Promise\State:

    • Issue: GuzzleHttp\Promise\Is is deprecated ahead of PHP 8.6; direct references will warn now and disappear later.
    • Fix: replace static calls with GuzzleHttp\Promise\State. Prefer Utils::settle() / Utils::all() so you never touch state classes at all.
  3. StreamHandler is now strict — truncated bodies throw:

    • Issue: responses that end before their declared Content-Length, use malformed chunking, or carry unknown Transfer-Encoding now raise ResponseTransferException instead of returning a short body.
    • Fix: expect it. Catch 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.
  4. cURL errors 8 and 23 are now ResponseTransferException:

    • Issue: 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.
    • Fix: add ResponseTransferException to retry/alert handling; note both extend TransferException, so catch broadly and inspect.
  5. Encoding strictness changed for stream handler only:

    • Issue: gzip/deflate bodies with trailing garbage are rejected, while raw deflate is now accepted. A payload that used to fail may now succeed, and vice versa.
    • Fix: don't branch on "did it throw before". Assert content correctness in tests, and pin the handler type in your test bootstrap so you test the one you actually deploy.
  6. libcurl gating on the 8.2.0 feature:

    • Issue: caps + persistent transport sharing is conditional on libcurl 8.22.0+. On older libcurl the old exclusion still applies, silently.
    • Fix: assert at boot (version_compare(curl_version()['version'], '8.22.0', '>=')) and log the result; don't infer capability from the Guzzle version alone.
  7. Proxy credential normalization:

    • Issue: proxy credentials with multiple @ (e.g. user:pass@proxy@domain) fail closed if unparseable.
    • Fix: use user:[email protected]; validate before constructing the client.
      if (substr_count($proxy, '@') > 1) {
          throw new \InvalidArgumentException('Invalid proxy credentials');
      }
      
  8. Multiplexing conflicts:

    • Issue: combining multiplex => true with non-integer CURLMOPT_PIPELINING, CURLOPT_HTTPAUTH allowing NTLM, or cleartext proxies without CURLOPT_PROXYAUTH.
    • Fix:
      $client = new Client([
          'multiplex' => true,
          'curl' => [
              CURLOPT_PIPELINING => 1,
              CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
          ],
      ]);
      
  9. Streaming restrictions:

    • Issue: stream => true is rejected on handlers with capabilities.
    • Fix: use sink for downloads, or a handler without caps:
      $client = new Client(['handler' => new \GuzzleHttp\Handler\CurlHandler()]);
      
  10. Credential redaction:

    • Issue: URIs with embedded credentials are always redacted in logs/middleware — you cannot debug auth failures from the logged URI.
    • Fix: use auth instead, and log the host + auth type only.
  11. Async promise race conditions:

    • Issue: promises may double-settle if cancelled from completion callbacks.
    • Fix: use settle() or all(); never resolve/reject from inside then() on the same promise.

Debugging Tips

  1. Know which handler you're on. 8.3.0's strictness is concentrated in StreamHandler; print get_class($handler) (or check extension_loaded('curl')) before debugging a body-length exception.
  2. Capability check first: before blaming Guzzle for lost connection reuse, print curl_version()['version'] and curl_version()['features'] — the 8.2.0 behaviour is invisible without it.
  3. Proxy validation errors: run with ['debug' => true] to surface rejected proxy values ("Proxy credential 'user:pass@proxy@domain' is invalid.").
  4. Multiplexing warnings: catch \InvalidArgumentException and echo the message ("Multiplexing conflict: NTLM auth not allowed.").
  5. Transfer exceptions: log Content-Length, the received byte count, and Transfer-Encoding when catching ResponseTransferException — that triple identifies truncation vs. bad chunking vs. unsupported coding.
  6. Streaming handler errors: verify on_trailers is only used with CurlHandler; StreamHandler throws.
  7. Connection reuse signatures: proxy credentials are normalized before computing signatures — clear any cached/pooled clients after changing proxy config.

Performance Optimization

  1. Capped + shared (8.2.0): on libcurl 8.22.0+ you can bound concurrency and keep reuse — prefer this over removing caps to regain keep-alive.
  2. Incremental chunked decoding (8.3.0): 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().
  3. Multiplexing safety: explicit opt-in for high throughput; avoid on 32-bit systems (integer overflow risk in delay timing).
  4. Async event loop: pair cURL multi with ReactPHP for large fan-outs.

Extension Points

  1. Custom handlers: CurlMultiHandler composes caps with persistent transport sharing on libcurl 8.22.0+; it still rejects stream => true when caps are configured.
  2. Middleware stack: preferred place for cross-cutting concerns (logging, retry, redaction) rather than per-call options — including catching ResponseTransferException and deciding retry policy.
  3. Handler swapping per environment: keep the handler construction in one factory so caps/sharing assumptions and the stream-vs-curl strictness profile live in a single place.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core