DEV Community

Roman
Roman

Posted on

How to create robust HTTP clients with Guzzle

I never was a big fan of Guzzle until recently when I finally learned about Guzzle middleware and some basic config.

Here is how you can step up your Guzzle game and build robust HTTP clients.

First of all, start from setting timeouts. Timeouts are not limited by default, and this can cause headaches when debugging issues it can cause.

I remember spending 2 days trying to understand why some jobs randomly time-outed. It wasn't fun when I noticed some requests stuck!

use GuzzleHttp\Client;

$client = new Client([
    'timeout' => 15,
    'connect_timeout' => 15,
]);
Enter fullscreen mode Exit fullscreen mode

So Guzzle's middleware stack. Guzzle passes any request through the middleware stack. It allows you to add your middleware and do something for every request (or every response): add the API key to headers, handle errors, and even retry failed requests.

To start you need to create HandlerStack. The simplest way to do it is to call the static method create on HandlerStack class. You will add middleware to the stack.

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;

$stack = HandlerStack::create();

// ...

$client = new Client([
    'base_uri' => 'https://api.some.com',
    'timeout' => 15,
    'connect_timeout' => 15,
    'handler' => $stack,
]);
Enter fullscreen mode Exit fullscreen mode

Guzzle's middleware on its own is pretty tricky: a function that returns a function. But thankfully, they've added sugar for it, so it looks readable.

Let's start from Middleware::mapRequest middleware. It allows you to modify a request. I use this middleware to add the API key to every request, either in headers or in the query string. It helps to reduce the amount of boilerplate code I write when making requests.

use GuzzleHttp\Middleware;
use GuzzleHttp\HandlerStack;
use Psr\Http\Message\RequestInterface;

$stack->push(function (RequestInterface $request) {
    $token = config('services.some.api_token');

    return $request->withHeader('Authorization', "Basic {$token}");
});
Enter fullscreen mode Exit fullscreen mode

Middleware::mapResponse allows you to do something with the response. I use it to do basic error handling. This way, I can avoid excessive try/catch usage or wrapping my entire client in a separate method.

use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;

$stack->push(Middleware::mapResponse(function (ResponseInterface $response) {
    if (in_array($response->getStatusCode(), [401, 403])) {
        // Some logic to hanle the error. For example, save it into DB.
    }
    return $response;
}));
Enter fullscreen mode Exit fullscreen mode

And my favorite Middleware::retry. It allows you to specify when and how to retry failed requests. For example, when you get 50x errors, connection timeouts, etc you would probably want to try again before giving up.

use Exception;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use GuzzleHttp\Exception\ConnectException;

$retryDecider = function (
    $retries, 
    RequestInterface $request, 
    ResponseInterface $response = null, 
    Exception $exception = null
) {
    if ($retries >= 5) {
        return false;
    }
    if ($exception instanceof ConnectException) {
        return true;
    }
    if ($response && $response->getStatusCode() >= 500) {
        return true;
    }
    return false;
};

$retryDelay = function ($numberOfRetries) {
    return 1000 * $numberOfRetries;
};

$stack->push(Middleware::retry($retryDecider, $retryDelay));
Enter fullscreen mode Exit fullscreen mode

Retry middleware also allows you to set the delay between requests so you can configure something like exponential back-off pretty easily. On the screenshot in the previous tweet delay between requests will be a bit bigger between every failed request.

At this point, you have a robust Guzzle HTTP client that is enjoyable to use. It will automatically retry failed requests, add authorization to the request, or do whatever you need!

Top comments (0)