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:
- Customizing the cURL options. This is handy for servers running behind proxies with all kinds of funky forwarding rules and so forth.
- Increasing the default timeout.
- 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.
- cURL (default)
- PHP streams (better compatibility)
- 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.
-
$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 thissend()
method, the URL will be the complete and final URL. -
$method
: The HTTP verb for the request. This will be eitherGET
,POST
orDELETE
. The Graph API doesn't support other HTTP verbs because YOLO. -
$body
: The full request body. This will be an empty string forGET
requests. ForPOST
&DELETE
requests the body may be a URL-encoded string. Additionally forPOST
the body may be encoded asmultipart/form-data
for file uploads. -
$headers
: An associative key-value-pair array of request headers. The array is passed to the method in the format:['header-key' => 'header-value', ...]
. SoAccept-Language: en-US
would be passed as['Accept-Language' => 'en-US']
for example. -
$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);
-
$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', ...]
). TheGraphRawResponse
can also accept the raw response header as a string. The raw string will be parsed into an associative array of key-value pairs. -
$body
: The raw response body as a string. -
$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, theGraphRawResponse
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
andFacebookStream
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
:
- A response header
- A response body
- 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!