Build A Better API Client with Guzzle Middleware

6 min readJul 21, 2017

Guzzle is pretty much the de facto standard for making HTTP requests in PHP these days, but lately there’s been a bit of buzz around switching to simpler alternatives. There definitely are times where Guzzle just feels like overkill and a simple file_get_contents would be good enough. However, there are some great features of Guzzle that, in my opinion, still make it worth keeping in your toolbox — not the least of which is its middleware.

Perhaps it’s just the type of projects I tend to work on, but I rarely find myself just wanting to make a simple get request to a URL. Typically I need to worry about authorization headers, refresh tokens, ETags, caching, retries and response codes to name a few.

When I started working with Guzzle, I’d create my own wrapper class that, before every request would ensure a valid token was set (or refreshed if necessary), set the appropriate headers and format the body for the type of request being sent. Then I’d have a response method that would catch any exceptions, manage response codes and typically json_decode the content.

It might look something like this:

While the code above is by no means terrible or hard to follow, it meant this Client class would constantly be edited as business rules changed. Inevitably would end up being filled with if blocks, adding to the code’s fragility and detracting from its readability. It definitely violates SOLID’s Open/Closed principal. Before you stop reading, I know many in the PHP community treat the SOLID principals as curse words these days, but my personal opinion is that most of the pain people feel from following SOLID comes from treating it like law rather than guidelines. Like everything in programming, use them where the trade-offs make sense.

They’re more what you’d call guidelines than actual rules. — Captain Barbossa

The Open/Close principal is always a confusing one, especially for newer developers. How do we add new functionality to a class without modifying it? While there’s lots of ways to handle the problem, in Guzzle’s case we can turn to middleware.

Guzzle’s constructor takes a few configuration options, one of which is the handler — an instance of Guzzle’s HandlerStack. This HandlerStack lets us push middleware onto the stack, allowing us to manipulate both incoming and outgoing HTTP messages. In modern PHP frameworks, you’re probably used to using built in middlewares for things like authentication, CORS validation, CSRF protection and any number of other tasks that make sense to operate on the request before it actually enters your core application code. Guzzle’s middleware works much the same, allowing us to modify the request or response as needed.

A common pattern I use to initialize my client looks something like this:

Let’s quickly run though what’s going on here. I’m using a static factory method to create my Guzzle client and passing it into the constructor of my consumer class. This makes it extremely easy to mock the client or pass in a client that uses Guzzle’s MockHandler for unit testing our application. I’m a pretty big proponent of value objects and, as such, I’m passing a Config object to my create method that will contain basic configuration options for my client such as the base URI, whether or not I want Guzzle to throw exceptions for 4xx and 5xx response codes, API keys, etc.

In this case, my Config class also contains an array of middlewares I want to apply. This is where we get back to that Open/Closed principal. By simply adding or removing a middleware from that stack, I can add different kinds of functionality to my Client without ever having to modify it. Of course it isn’t perfect. There’s always going to be times when some change requires us to modify the Client, but we’re minimizing the frequency of that happening.

So we’ve seen how we can add middleware, but we still haven’t really answered the question of why we should use middleware. To do that, let’s look at line 17 of the method above:

$stack->push(new RefreshToken($client, $config));

The RefreshToken middleware is where we’re going to handle ensuring that we get (or refresh) a token and set a valid Authorization header on each request. Here’s what that might look like:

The middleware’s __invoke method accepts a callable (which will be the next middleware in line) and expects us to return a callable. This is where our logic will live. We’re running a quick check to see if we already have a valid token from a previous request. If we don’t have a token (or our token is expired/expiring) we’ll make a request for a new one, then apply the resulting token to our header. How exactly that logic works is going to depend on the API we’re dealing with, but it’ll probably look something like this:

This code is far from perfect and definitely not something that I’d use in production as is, but hopefully it’s easy enough to follow to get the point across. We’re building our request based on whether we need to get a new token or a refreshed token, then wrapping the result in our BearerToken class. This BearerToken would just be a simple value object that holds our response values and contains some simple logic like checking if the token is expired (or about to expire).

With this middleware in our Client’s stack, we’ve extracted all our authentication logic out to a class that’s unlikely to change and easy to reuse. Now we can simply call methods on our Client without having to think about whether or not we need a token.

Authorization is probably the most common place I use middleware but it’s far form the only one. We could modify our request to include an If-None-Match header, taking advantage of ETags to save server load by pulling unchanged responses from cache instead of constantly parsing new requests.

By default, Guzzle will throw a ClientException and ServerException for 4xx and 5xx responses respectively. While it does give you several different ways of catching these exceptions, I almost always find that I want my exceptions to be a bit more specific: was it a validation exception? Was my token invalid? Was the resource not found? We could handle this in our Client but using middleware keeps things clean:

All we’re doing here is a quick check to see if the response was successful (a very naïve check that the status code is less than 400) and returning it if it is. Otherwise we throw the appropriate exception so our application can handle the error gracefully. Again, this is by no means production code. We’d probably want to use a factory class to contain our error handling and factory methods on our exceptions to extract the relevant data from the response body.

Guzzle also comes with some middleware out of the box. By default, it uses middleware to handle errors, cookies, preparing the response body, etc. But there’s also a few optional ones that come in handy. The RetryMiddleware lets us pass in a “decider” callable that will determine if a request should be retried on failure. This can be great for reducing errors in your app, managing throttling limits or even extending it to allow for modifying a request before retrying.

Signature: Middleware::retry(callable $decider, callable $delay = null);

Guzzle’s Middleware class has a few methods that return callback middlewares that can be pretty useful too. Middleware::tap is great for debugging, allowing you to insert functionality before or after the rest of the stack is processed. Middleware::log has obvious uses and Middleware::mapRequest and Middleware::mapResponse allow us to quickly modify requests or responses with a simple closure. There’s also methods for redirecting or tracking history.

Middleware is a great way to keep our Client’s API simple while still adding all the extra functionality a modern API needs. There are, without a doubt, a lot of cases where Guzzle is overkill and file_get_contents or maybe a wrapper like Colin DeCarlo's Uhura are quick and simple solutions that are more than enough to get the job done. However Guzzle, especially combined with it’s middleware stack, is a very useful tool to have in your toolbox.