Overwriting The HTTP Client Handler In Facebook PHP SDK v5

How to inject your own HTTP client in the Facebook PHP SDK v5

Aug 31, 2015

Coding up an HTTP client that works well across a vast variety of server environments is particularly hard to do. The Facebook PHP SDK v5 provides a pretty good HTTP client solution out of the box, but there are perfectly valid reasons for injecting your own HTTP client.

Three of the most common reasons for customizing the HTTP client are:

  1. Customizing the cURL options. This is handy for servers running behind proxies with all kinds of funky forwarding rules and so forth.
  2. Increasing the default timeout.
  3. Implementing Guzzle 6 since the PHP SDK only supports Guzzle 5 out of the box.

The Facebook PHP SDK v5 supports custom HTTP client implementations but this functionality is undocumented. Fear not. I shall show you how to inject your own HTTP client implementation.

This guide is for v5 of the Facebook PHP SDK. But I also have a guide explaining how to overwrite the HTTP client in Facebook PHP SDK v4.0.

The three built-in HTTP clients

The Facebook PHP SDK v5 ships with three HTTP client implementations.

  1. cURL (default)
  2. PHP streams (better compatibility)
  3. Guzzle 5 (even better if you already use Guzzle 5)

If you're having issues with the default cURL implementation, you can force the PHP SDK to use one of the other two implementations by setting the http_client_handler configuration option with the name of the implementation.

# Use the PHP stream implementation
$fb = new Facebook\Facebook([
    'http_client_handler' => 'stream',
]);

# Or use the Guzzle 5 implementation
$fb = new Facebook\Facebook([
    'http_client_handler' => 'guzzle',
]);

Guzzle 5 is required. You'll need to install Guzzle 5 for the built-in guzzle implementation to work.

Now that know how to switch the built-in HTTP clients, let's build our own custom HTTP client implementation. But before we start coding, we'll need to learn about two very important players; the FacebookHttpClientInterface and the GraphRawResponse.

The FacebookHttpClientInterface

In order to create a valid custom HTTP client implementation, we'll need to create a class that implements the FacebookHttpClientInterface. The interface is quite simple containing only one method.

namespace Facebook\HttpClients;

interface FacebookHttpClientInterface
{
    public function send($url, $method, $body, array $headers, $timeOut);
}

The arguments for the send() method

The argument list for the send() method contains all the data you might expect for making an HTTP request.

  1. $url: The full URL of the request. The PHP SDK does quite a lot of URL manipulation by automatically prefixing the Graph API version, appending the access token and the app secret proof and so on. Once the script execution has reached this send() method, the URL will be the complete and final URL.
  2. $method: The HTTP verb for the request. This will be either GET, POST or DELETE. The Graph API doesn't support other HTTP verbs because YOLO.
  3. $body: The full request body. This will be an empty string for GET requests. For POST & DELETE requests the body may be a URL-encoded string. Additionally for POST the body may be encoded as multipart/form-data for file uploads.
  4. $headers: An associative key-value-pair array of request headers. The array is passed to the method in the format: ['header-key' => 'header-value', ...]. So Accept-Language: en-US would be passed as ['Accept-Language' => 'en-US'] for example.
  5. $timeOut: The timeout (in seconds) for the request. By default the PHP SDK timeout for requests is 1 minute. If the request contains photos the timeout will be bumped up to 1 hour and for videos the timeout bumps up to 2 hours. If you have a huge batch request or are uploading big files on a slow connection you could be touching the upper limits of the default timeouts. In our custom client we'll up this limit.

Video upload timeouts: If your script frequently times out with video uploads, you might want to take advantage of the new "upload by chunks" feature of the Graph API. An easy-to-use API for this feature is currently being added to the PHP SDK and could be ready by v5.1.

The GraphRawResponse return type for the send() method

The send() method should return an instance of GraphRawResponse which is an immutable entity containing the HTTP response. It exists to serve as a lingua franca between the HTTP client implementations and the FacebookClient service.

Since our custom HTTP implementation will need to return this entity, we should probably learn how to instantiate it. Let's look at the constructor's signature.

# Facebook\Http\GraphRawResponse
public function __construct($headers, $body, $httpStatusCode = null);
  1. $headers: An associative array or string of the response headers. If $headers is passed as an array it should be formatted as key-value pairs (just like the request headers as explained above: ['header-key' => 'header-value', ...]). The GraphRawResponse can also accept the raw response header as a string. The raw string will be parsed into an associative array of key-value pairs.
  2. $body: The raw response body as a string.
  3. $httpStatusCode (optional): The HTTP response code. You only need to set this argument if you're passing the response headers in as an array of key-value pairs. If you're passing the response header as a string and the raw header contains the Status-Line, the GraphRawResponse will set the HTTP response code automatically based on parsing the Status-Line.

Let's see hard-coded examples of instantiating the GraphRawResponse entity using the two different ways to pass in the response header.

# Passing response header as raw string
$header = "HTTP/1.1 200 OK
Etag: \"9d86b21aa74d74e574bbb35ba13524a52deb96e3\"
Content-Type: text/javascript; charset=UTF-8
X-FB-Rev: 9244768
Date: Mon, 19 May 2014 18:37:17 GMT
X-FB-Debug: 02QQiffE7JG2rV6i/Agzd0gI2/OOQ2lk5UW0=
Access-Control-Allow-Origin: *\r\n\r\n";
$body = 'Foo Response';
$response = new Facebook\Http\GraphRawResponse(
    $header,
    $body);

# Passing response header key-value pairs as associative array
$header = [
    'Etag' => '"9d86b21aa74d74e574bbb35ba13524a52deb96e3"',
    'Content-Type' => 'text/javascript; charset=UTF-8',
    'X-FB-Rev' => '9244768',
    'Date' => 'Mon, 19 May 2014 18:37:17 GMT',
    'X-FB-Debug' => '02QQiffE7JG2rV6i/Agzd0gI2/OOQ2lk5UW0=',
    'Access-Control-Allow-Origin' => '*',
];
$body = 'Foo Response';
$responseCode = 200;

$response = new Facebook\Http\GraphRawResponse(
    $header,
    $body,
    $responseCode);

The two instances of GraphRawResponse above will be functionally identical and will produce the same output.

echo $response->getHttpResponseCode();
# prints: 200

echo $response->getBody();
# prints: Foo Response

var_dump($response->getHeaders());
/*
prints:

array(6) {
  ["Etag"]=>
  string(42) ""9d86b21aa74d74e574bbb35ba13524a52deb96e3""
  ["Content-Type"]=>
  string(30) "text/javascript; charset=UTF-8"
  ...
*/

Customizing the cURL predefined constants

If you're one of those kids who likes to use cURL and you have mile-long CURLOPT_* constants options to customize it, then you'll want to customize the options for the cURL implementation.

We could code up the implementation from scratch, but since the PHP SDK already has a cURL implementation (FacebookCurlHttpClient), let's just extend from it and add some of our own cURL pre-defined constants.

class CustomCurlOptsHttpClient extends Facebook\HttpClients\FacebookCurlHttpClient
{
    public function openConnection($url, $method, $body, array $headers, $timeOut)
    {
        parent::openConnection($url, $method, $body, $headers, $timeOut);
        
        $options = [
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_CONNECTTIMEOUT => 30,
            // ... all the cURL options you like
        ];
        
        $this->facebookCurl->setoptArray($options);
    }
}

All you have to do now is tell the PHP SDK to use your custom implementation.

$fbCurl = new Facebook\HttpClients\FacebookCurl;
$fb = new Facebook\Facebook([
    'http_client_handler' => new CustomCurlOptsHttpClient($fbCurl),
]);

Now all HTTP requests will be sent with all your fancy-pants CURLOPT_* constants.

Increasing the default timeouts

The default timeout options are sufficient for about 90% of the use cases, but if you're in the 10% that needs more time (or less time), you can increase or decrease the timeouts as much as you need to.

This method will work with any of the built-in HTTP clients, but we'll use the PHP stream HTTP client implementation that comes with the PHP SDK (FacebookStreamHttpClient) as a base for this example.

class LongerTimeoutHttpClient extends Facebook\HttpClients\FacebookStreamHttpClient
{
    public function send($url, $method, $body, array $headers, $timeOut)
    {
        // Double the default timeout
        $timeOut *= 2;
        
        return parent::send($url, $method, $body, $headers, $timeOut);
    }
}

Don't hard-code a constant timeout: As explained above, the timeout value will change depending on the type of request (like uploading a file). Since the timeout flexes in a way that's "smart" you'll want to increase the timeout by multiplying/dividing/adding/subtracting seconds to/from the existing timeout value.

And just as we did with the custom cURL implementation above, we'll need to tell the PHP SDK of our custom PHP stream implementation.

$fbStream = new Facebook\HttpClients\FacebookStream;
$fb = new Facebook\Facebook([
    'http_client_handler' => new LongerTimeoutHttpClient($fbStream),
]);

HTTP Client dependancies: You might be wondering what the FacebookCurl and FacebookStream objects do. They are just object wrappers of the cURL functions and PHP stream functions respectively. They are wrapped as objects so that mocked versions of them can be injected into the HTTP client implementations for unit testing purposes.

Writing a Guzzle 6 HTTP client implementation from scratch

If your project uses Guzzle 6, you won't be able to use the built-in Guzzle implementation built into the PHP SDK since it is built for Guzzle 5. So let's just roll our own Guzzle 6 HTTP client from scratch and inject it into the PHP SDK.

Create a class for our HTTP client

First we need to install Guzzle 6. Then we can create a class that implements the FacebookHttpClientInterface and create a PSR-7 Request entity with the values passed into the send() method.

class Guzzle6HttpClient implements Facebook\HttpClients\FacebookHttpClientInterface
{
    public function send($url, $method, $body, array $headers, $timeOut)
    {
        $request = new GuzzleHttp\Psr7\Request($method, $url, $headers, $body);
    }
}

Simply instantiating the Request entity doesn't actually send the request over HTTP; we need the Guzzle Client service to send it.

Inject the Guzzle client

We'll set up our class so that the Guzzle Client dependency is injected via the constructor. This will allow us to mock it during a unit test so that we don't have to send real requests over HTTP.

This constructor-injection technique also allows us to use existing instantiations of the Guzzle Client which have been configured for our unique server environment if the project is already running on Guzzle 6 for example.

class Guzzle6HttpClient implements Facebook\HttpClients\FacebookHttpClientInterface
{
    private $client;

    public function __construct(GuzzleHttp\Client $client)
    {
        $this->client = $client;
    }

    public function send($url, $method, $body, array $headers, $timeOut)
    {
        $request = new GuzzleHttp\Psr7\Request($method, $url, $headers, $body);
        $response = $this->client->send($request, ['timeout' => $timeOut]);
    }
}

So far we've successfully created a PSR-7 Request entity, sent it over HTTP via the Guzzle Client and the Guzzle Client returned a PSR-7 Response. But we're not quite done yet.

Convert the response to a GraphRawResponse

We need to convert the PSR-7 Response entity into the PHP SDK equivalent which is an instance of GraphRawResponse. This is the entity we will return from the send() method.

As we learned above, we need three pieces of data in order to create a GraphRawResponse:

  1. A response header
  2. A response body
  3. An HTTP response code

The PSR-7 Response entity gives us access to all those pieces of data, but the header and body need to be reformatted a bit before we can used them. Let's look at the final implementation and I'll explain how we're reformatting the response header and body.

class Guzzle6HttpClient implements Facebook\HttpClients\FacebookHttpClientInterface
{
    private $client;

    public function __construct(GuzzleHttp\Client $client)
    {
        $this->client = $client;
    }

    public function send($url, $method, $body, array $headers, $timeOut)
    {
        $request = new GuzzleHttp\Psr7\Request($method, $url, $headers, $body);
        $response = $this->client->send($request, ['timeout' => $timeOut]);
        
        $responseHeaders = $response->getHeaders();
        foreach ($responseHeaders as $key => $values) {
            $responseHeaders[$key] = implode(', ', $values);
        }
        
        $responseBody = $response->getBody()->getContents();
        $httpStatusCode = $response->getStatusCode();

        return new Facebook\Http\GraphRawResponse(
                        $responseHeaders,
                        $responseBody,
                        $httpStatusCode);
    }
}

Formatting the response header array

Let's first examine the response headers which we can access as an array via the Response::getHeaders() method. You might be wondering why we can't just pass it along to the GraphRawResponse constructor since it too expects an array of headers (or a string of the raw header).

We have to reformat the $responseHeaders array since the Response::getHeaders() method returns a multidimensional array in the following format:

[
    'header-key' => ['header-value1', 'header-value2', ...],
    ...
]

That might seem strange at first until we realize that response headers can contain multiple values for a single key. They are concatenated with a comma in the raw header. For example, the following raw header:

...
X-SammyK: Says Hi
X-Foo: foo, bar
...

Would be parsed as:

[
    ...
    'X-SammyK' => ['Says Hi'],
    'X-Foo' => ['foo', 'bar'],
    ...
]

The GraphRawResponse expects a string for each value of the keys instead of an array so we implode() the values with a , and generate:

[
    ...
    'X-SammyK' => 'Says Hi',
    'X-Foo' => 'foo, bar',
    ...
]

Formatting the response body

When we call Response::getBody() we might expect a string containing the response body; instead we get an instance of StreamInterface which is how PSR-7 represents the response body. To get the body as a plain-old PHP string we call StreamInterface::getContents() thus the chained methods, $responseBody = $response->getBody()->getContents().

Inject the custom Guzzle 6 implementation

The final step is to inject our custom Guzzle 6 HTTP client implementation into the PHP SDK.

$client = new GuzzleHttp\Client;
$fb = new Facebook\Facebook([
    'http_client_handler' => new Guzzle6HttpClient($client),
]);

Now all requests sent through the PHP SDK will be sent via the Guzzle 6 HTTP Client.

Handling errors

We should tie up a loose end on our custom Guzzle 6 HTTP client. Right now the client assumes all servers poop out rainbows without fail. But the reality is that HTTP requests can fail for myriad reasons so we need to make sure to handle the case when things go awry.

To do this we'll catch the RequestException that Guzzle 6 throws and we'll re-throw it as a FacebookSDKException so that our scripts can catch the documented base PHP SDK exception. We'll also add an option to the send() method that will disable exceptions when error responses are returned from the Graph API.

class Guzzle6HttpClient implements Facebook\HttpClients\FacebookHttpClientInterface
{
    private $client;

    public function __construct(GuzzleHttp\Client $client)
    {
        $this->client = $client;
    }

    public function send($url, $method, $body, array $headers, $timeOut)
    {
        $request = new GuzzleHttp\Psr7\Request($method, $url, $headers, $body);
        try {
            $response = $this->client->send($request, ['timeout' => $timeOut, 'http_errors' => false]);
        } catch (GuzzleHttp\Exception\RequestException $e) {
            throw new Facebook\Exceptions\FacebookSDKException($e->getMessage(), $e->getCode());
        }
        $httpStatusCode = $response->getStatusCode();
        $responseHeaders = $response->getHeaders();

        foreach ($responseHeaders as $key => $values) {
            $responseHeaders[$key] = implode(', ', $values);
        }

        $responseBody = $response->getBody()->getContents();

        return new Facebook\Http\GraphRawResponse(
                        $responseHeaders,
                        $responseBody,
                        $httpStatusCode);
    }
}

What about the FacebookResponseException? You may be wondering why our HTTP client is throwing the general FacebookSDKException instead of the more specific FacebookResponseException. The FacebookResponseException can only be thrown once an HTTP response has been obtained from the Graph API. If the HTTP response from Graph is a 400 or 500 level error, the PHP SDK will parse the response and throw the proper FacebookResponseException subtype.

That's why we set the http_errors Guzzle configuration option to false so that Guzzle won't throw exceptions for error responses from Graph and allow the PHP SDK to handle them. The RequestException that Guzzle 6 throws is for catching errors that occur before a response from Graph is obtained (timeouts, DNS errors, etc.) These type of connection errors get thrown as general FacebookSDKException's in the PHP SDK.

Now our custom HTTP client is not only integrated into the PHP SDK as the primary HTTP client, but it is properly tied into the native PHP SDK's error handling.

All done

Now that you have the knowledge, go forth and customize thy HTTP client. Good luck!

If you found this guide helpful, say, "Hi" on twitter! I'd love to hear from you. :)