Debugging API Requests in the v2 Google PHP SDK

In a previous article we looked at using the new v2 Google PHP SDK to work with exporting Google Sheets. In working on that article I ran across some interesting side-effects of the middleware model. I’d like to mention some of those side-effects. First, let’s talk about what middleware is.

The Promise of Middleware

In order to facilitate extendability and customization, Guzzle 6 has made use of the middleware concept. Guzzle 6 middleware specifically, allows one to inject arbitrary code into the request and/or response lifecycle of an HTTP request. Among the many possible use cases are the ability to modify an outgoing request. This is what the Google_Client class does to include appropriate Authorization information for each request.

Rather than requiring the calling code to explicitly keep track of authentication state (grab a token if needed, inject it into the Google_Client instance, handle renewal and caching) the middleware does all of this for us. We need only to configure our credentials, and the middleware handles grabbing the auth code, injecting it into the request before getting sent, caching it, renewing it, et cetera. For most cases, we don’t need to even think about it. It “just works.” Naturally, the Google_Client class could implement this just as well without using middleware. The point here, is that the behavior is pluggable: we could write a different implementation with slightly characteristics without ever having to modify the Google_Client code (as long as we adhere to the interface specified by the middleware concept), which we discuss next.

Middleware in Abstract

In its most abstract form, Guzzle middleware takes the form of a higher order function:

use Psr\Http\Message\RequestInterface;

$middleware = function (callable $handler) {
    return function (RequestInterface $request, array $options) use ($handler) {
        return $handler($request, $options);
    };
};

Since PHP 5.4, middleware can also take the form of an object which implements an __invoke(callable $handler) magic function:

use Psr\Http\Message\RequestInterface;

class MyMiddleware
{
    public function __invoke(callable $handler)
    {
        return function (callable $handler) {
            return function (RequestInterface $request, array $options) use ($handler) {
                return $handler($request, $options);
            };
        };
    }
}

PHP 5.4 is required here because we’re utilizing the callable typehint. __invoke was added in PHP 5.3.

For a more humane introduction to __invoke() than the PHP docs, read this.

Back to Google Drive

Assume we have the following setup:

$client = new Google_Client;
$client->setAuthConfig($path_to_keyfile);
$client->setScopes(array('https://www.googleapis.com/auth/drive'));
$client->setSubject($sub_account);

Suppose we want to try to export a Google Sheet where $export_uri contains the URL of the format we want.

$http = $client->getHttpClient();
$response = $http->get($export_uri);

From the last article, we know this won’t work because we’ve omitted the 'auth' => 'google_auth' option in the get() call. But what if we didn’t know this? Or, suppose we were having other issues with the request, how might we examine the headers getting sent by the HTTP client? It’s easy enough to see the headers returned by examining the $response object, but perhaps the errors might be due to an erroneous request (for example, missing Authorization header or incorrect authorization token). It would be helpful if we could see the full request headers along with the body of the request (if any). One can come up with other use cases why having access to the request headers would be useful: logging, for instance.

Developing Debugging Middleware

We can use middleware to get at the internals of the request/response cycle:

use GuzzleHttp\Middleware;

function errorln($msg)
{
    return fwrite(STDERR, "$msg\n");
}

function debug_guzzle_middleware()
{
    return Middleware::tap(function ($request, $options) {
        $method = $request->getMethod();
        $target = $request->getRequestTarget();
        errorln("--->>");
        errorln('REQUEST> ' . $method . ' ' . $target);
        foreach ($request->getHeaders() as $k => $v) {
            errorln("REQUEST> $k: " . implode(', ', $v));
        }
        errorln("REQUEST> ---\n" . $request->getBody()->getContents() . "\n---");
    }, function ($request, $options, $promise) {
        $promise->then(function ($response) {
            $protver = $response->getProtocolVersion();
            $status = $response->getStatusCode();
            $reason = $response->getReasonPhrase();
            errorln("<<---");
            errorln("RESPONSE> HTTP/" . $protver . " " . $status . " " . $reason);
            foreach ($response->getHeaders() as $k => $v) {
                errorln("RESPONSE> $k: " . implode(', ', $v));
            }
            errorln("RESPONSE> ---\n" . $response->getBody()->getContents() . "\n---");
        });
    });
}

We make use of the GuzzleHttp\Middleware::tap() method which implements the pattern of injecting functionality to run before and after a request. It takes two arguments: a callable to run before the request and a callable to run after the request. In theory, this will show us all the headers and body content for all requests and responses. In reality, however, we’re only going to see the headers and content contained in the request at the time our code executes. This is a key distinction. If there are other middleware that get registered after ours, then we’re never going to see any modifications that other middleware makes (if any).

In the case of Google_Client, the solution is to explicitly call the authorize() method. Examining the code for this method reveals that this will have the effect of instantiating the underlying GuzzleHttp\Client instance as well as pushing the Google Auth middleware onto the handler stack. This gives us a way to control the order in which middleware is pushed onto the handler stack.

$client->authorize();
$http = $client->getHttpClient();
$stack = $http->getConfig('handler');
$stack->push(debug_guzzle_middleware());

This guarantees that our debug middleware is not called until after the Google\Auth\Middleware\AuthTokenMiddleware has done its work.

With our debug middleware in place, we should now be able to see all the headers being sent with our original request. When we execute,

$response = $http->get($export_uri);

We now have:

Note the absence of the Authorization header. After we’ve added the 'auth' => 'google_auth' to the get() call, however, we have:

Conclusion

The middleware we’ve developed helps us see the (dis)function in action, but it is important to realize that ordering of the handler stack matters. If you’re not careful how you setup your middleware stack, it is possible you are missing important details. Extending behavior of an object does not happen in a vacuum: the middleware concept is not some magical pattern that solves problems without side effects. It is still your responsibility to understand the internals of how a class works and the possible ramifications of relying on middleware.

If you would like to comment on this article, please send me an email at pennedav@gmail.com.
Feedback from comments may be incorporated in the form of updates to the article text.