How to create robust HTTP clients with Guzzle 
Tags: php, laravel
Published: 2021-05-25 19:00:37

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!

```php
use GuzzleHttp\Client;

$client = new Client([
    'timeout' => 15,
    'connect_timeout' => 15,
]);
```

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.

```php
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,
]);
```

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.

```php
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}");
});
```

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

```php
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;
}));
```

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.

```php
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));
```

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!