Skip to content

Allow customizing the GuzzleSender handler stack #458

@epixian

Description

@epixian

It would be nice to more easily customize the handler stack used by GuzzleSender. V3 only gives us the option to addMiddleware but not to splice or remove them.

One of the major headaches of dealing with Guzzle is that the httpErrors middleware is added to the stack as-is. That is, with a 120-char default length for any HTTP errors encountered during the request. Many APIs return way more than that in the error message.

Currently my workaround is to extend the GuzzleSender and override the createGuzzleClient method:

class GuzzleSenderNoTruncate extends GuzzleSender
{
    /**
     * Create a new Guzzle client
     */
    protected function createGuzzleClient(): Client
    {
        // We'll use HandlerStack::create as it will create a default
        // handler stack with the default Guzzle middleware like
        // http_errors, cookies etc.

        $this->handlerStack = HandlerStack::create();

        // Replace the httpErrors middleware
        $this->handlerStack->before(
            'http_errors',
            Middleware::httpErrors(new BodySummarizer(2000)),
            'http_errors_untruncated'
        );
        $this->handlerStack->remove('http_errors');

        // Now we'll return new Guzzle client with some default request
        // options configured. We'll also define the handler stack we
        // created above. Since it's a property, developers may
        // customise or add middleware to the handler stack.

        return new Client([
            RequestOptions::CRYPTO_METHOD => Config::$defaultTlsMethod,
            RequestOptions::CONNECT_TIMEOUT => Config::$defaultConnectionTimeout,
            RequestOptions::TIMEOUT => Config::$defaultRequestTimeout,
            RequestOptions::HTTP_ERRORS => true,
            'handler' => $this->handlerStack,
        ]);
    }
}

Unfortunately this also requires copying all of the original method's code. At the very least I'd like to mitigate this by moving the top part to a new method defaultHandlerStack():

class GuzzleSenderNoTruncate extends GuzzleSender
{
    /**
     * Create a new Guzzle client
     */
    protected function createGuzzleClient(): Client
    {
        $this->handlerStack = $this->defaultHandlerStack();

        // Now we'll return new Guzzle client with some default request
        // options configured. We'll also define the handler stack we
        // created above. Since it's a property, developers may
        // customise or add middleware to the handler stack.

        return new Client([
            RequestOptions::CRYPTO_METHOD => Config::$defaultTlsMethod,
            RequestOptions::CONNECT_TIMEOUT => Config::$defaultConnectionTimeout,
            RequestOptions::TIMEOUT => Config::$defaultRequestTimeout,
            RequestOptions::HTTP_ERRORS => true,
            'handler' => $this->handlerStack,
        ]);
    }

    /**
     * Get the default handler stack to be used by the Guzzle client.
     */
    protected function defaultHandlerStack(): HandlerStack|null
    {
        // We'll use HandlerStack::create as it will create a default
        // handler stack with the default Guzzle middleware like
        // http_errors, cookies etc.

        return HandlerStack::create();
    }
}

Then we could just set the defaultHandlerStack in our extending class.

use GuzzleHttp\BodySummarizer;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Saloon\Http\Senders\GuzzleSender;

class GuzzleSenderNoTruncate extends GuzzleSender
{
    protected function defaultHandlerStack(): HandlerStack
    {
        $handlerStack = parent::defaultHandlerStack();

        // Replace the httpErrors middleware
        $handlerStack->before(
            'http_errors',
            Middleware::httpErrors(new BodySummarizer(2000)),
            'http_errors_untruncated'
        );
        $handlerStack->remove('http_errors');

        return $handlerStack;
    }
}

Another option would be to also implement the underlying HandlerStack before, after, and remove methods on the GuzzleSender implementation, similar to how addMiddleware implements push, so we can customize the stack after it's instantiated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions