Skip to content

Drop Guzzle requirement and replace with any PSR-18 HttpClient #396

Closed

Description

@tacman

The current version of this SDK requires Guzzle. My application uses Symfony's HttpClient (which allows for automatic retries and integration with the debug toolbar, compelling reasons for me to use it).

Rather than recommending this library switch, better would be use only require that any PSR-18 http client be installed, and use it.

Here's an article about how and why: https://medium.com/php-fig/psr-18-the-php-standard-for-http-clients-3254c58752bb

At a minimum, even if Guzzle is installed, the developer should be able to choose what client they want to use for the requests.

Thanks!

Activity

  1. pepeh commented on Oct 15, 2026

    @pepeh

    +1. This is now blocking real upgrades: Guzzle 8 came out in July 2026, and because the SDK hard-requires guzzlehttp/guzzle ^7.4.5 (plus guzzlehttp/promises ^1|^2 and guzzlehttp/psr7 ^2), every app using cloudinary_php is stuck on Guzzle 7. I opened #429 for the short-term constraint bump.

    Depending on PSR-18 (psr/http-client) and PSR-17 factories would end this for good: it would no longer matter which Guzzle major is installed, and apps could pass in their own client, e.g. Symfony HttpClient via Psr18Client.

    Since PSR-18 is sync-only and the SDK exposes *Async() methods that return Guzzle promises, a pragmatic middle ground could be: accept an optional injected ClientInterface for the sync calls, and keep Guzzle as the default and as the async implementation.

  2. const-cloudinary commented on Oct 19, 2026

    @const-cloudinary
    Member

    @tacman thanks for raising this, and @pepeh for the follow-up.

    Guzzle 8: supported since 3.2.0. The SDK now accepts guzzlehttp/guzzle ^7.4.5|^8.0, guzzlehttp/promises ^1.5.3|^2.0|^3.0, and guzzlehttp/psr7 ^2.7|^3.0.

    Custom PSR-18 client: #431 adds this. Any Psr\Http\Client\ClientInterface can carry the SDK's requests, so the logging, retries, and timeouts you configured on your client apply. With Symfony HttpClient:

    use Cloudinary\Cloudinary;
    use GuzzleHttp\Psr7\HttpFactory;
    use Symfony\Component\HttpClient\HttpClient;
    use Symfony\Component\HttpClient\Psr18Client;
    use Symfony\Component\HttpClient\RetryableHttpClient;
    
    $httpClient = new RetryableHttpClient(HttpClient::create(['timeout' => 60]), null, 3);
    
    $cloudinary = new Cloudinary();
    $cloudinary->setHttpClient(new Psr18Client($httpClient, new HttpFactory(), new HttpFactory()));

    In a Symfony app, $httpClient can be the http_client service you already configured. API objects created directly take the client through their own setHttpClient(), and Cloudinary::setDefaultHttpClient() sets a process-wide default.

    What it doesn't do:

    • Guzzle is still required. The *Async() methods return Guzzle promises, and removing that would break the public API. Guzzle still builds each request, then hands it to your client to send.
    • Async calls run one at a time with a custom client, because PSR-18 has no async interface.
    • timeout, connection_timeout, and api_proxy are configured on your client instead of in Cloudinary config.
    • Header logging needs redaction. Every request carries an Authorization header with your API secret; redact it if your client logs headers.

    Feedback on #431 is welcome.

  3. tacman commented on Oct 19, 2026

    @tacman
    Author

    When I opened this, I focused on PSR-18 without considering PSR-17 for request and stream creation. Would combining the two provide a path to making Guzzle optional? My goal is to use Symfony HttpClient without also requiring the Guzzle HTTP client. Symfony supports async too, although exposing that would require something beyond PSR-18’s synchronous interface.

  4. const-cloudinary commented on Oct 20, 2026

    @const-cloudinary
    Member

    @tacman thanks, good question.

    PSR-17 would let the SDK build requests without Guzzle, but making Guzzle optional means changing the SDK's public API, and that requires a new major version. Existing customers rely on the current types:

    • every *Async() method returns GuzzleHttp\Promise\PromiseInterface
    • network errors are thrown as Guzzle exceptions (ConnectException, RequestException), which applications catch

    Changing any of these would break code that works today, so it isn't something we can do in 3.x.

    With #431, now released in 3.3.0, Guzzle stays installed but your Symfony client does the transport. For async specifically, Symfony's own async API (HttplugClient::sendAsyncRequest()) also depends on guzzlehttp/promises.

    We'll keep a Guzzle-free transport in mind for the next major version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions