Advanced configuration of HTTP client in Drupal 7/9

How to add additional settings to the Drupal 7 or 9 bundled HTTP client. An example with a self-signed certificate

Posted on October 01, 2021 · 9 mins read

Motivation

Drupal makes use of Guzzle HTTP client to provide API client capabilities directly in its Core install. When a developer needs to call any external API it is as easy as requesting for the client service and perform the call:

$response = \Drupal::httpClient()->get('https://demoserver/demo-items.json');

The problem arises when the developer wants to add settings to the HTTP Client. In a bare-bones PHP project it could be done simply passing parameters to the Guzzle client constructor as in following code, but this code does not use Drupal core functionalities and might have problems on the long term:

$client = new GuzzleHttp\Client([
  'base_uri' => 'https://demoserver',
  'verify' => 'a_self_signed_certificate_we_want_to_accept.crt',
  (...)
]);
$response = $client->request('GET', 'demo-items.json');

I will show an example below on how to add some additional parameters in a Drupal context without instantiating Guzzle HTTP client directly but using the bundled Drupal service, to keep our code cleaner and more easily testable.

I will also how to do the same in Drupal 7, and how to replace the core drupal_http_request method by Guzzle too.

Drupal 9: Making API requests to self-signed servers

The basic method to do an HTTP client request to a server with a self-signed SSL certificate in Drupal 9 is shown below. But the client throws the displayed error if anything happens to the client system when validating the SSL certificate:

$response = \Drupal::httpClient()
  ->get('https://selfsigned.webfutura.eu/demo-items.json');

// Results in
GuzzleHttp\Exception\RequestException: cURL error 60: SSL certificate problem: self signed certificate
(see https://curl.haxx.se/libcurl/c/libcurl-errors.html) in GuzzleHttp\Handler\CurlFactory::createRejection()
(line 201 of (..)/vendor/guzzlehttp/guzzle/src/Handler/CurlFactory.php). 

To solve this self-signed certificate problem or other problems related to the SSL chain we need to do two things. The first is to download the public certificate we want to accept, which can be done from our web browser, and the second is to tell the HTTP client to use it in our get / post / request method. This way we can finally obtain the requested demo data:

$response = \Drupal::httpClient()
    ->get('https://selfsigned.webfutura.eu/demo-items.json', [
      'verify' => 'data/selfsigned_webfutura_eu.crt',
    ]);
$response_body = $response->getBody();
print_r(json_decode($response_body->getContents()));

// Results in
Array
(
    [0] => stdClass Object
        (
            [name] => item-1
            [label] => Item 1
        )

    [1] => stdClass Object
        (
            [name] => item-2
            [label] => Item 2
        )

)

Drupal 7: Making API requests to self-signed servers

If we wanted to do the same as in the previous example but in Drupal 7, it would be with code below, which makes use of drupal_http_request. The difference is that this method does not throw an exception as Guzzle did, but returns information about the error in $result array:

$result = drupal_http_request('https://selfsigned.webfutura.eu/demo-items.json');
print_r($result);

// Results in 
stdClass Object
(
    [code] => 0
    [error] => Error opening socket ssl://selfsigned.webfutura.eu:443
)

The solution is again to add the additional settings as an argument to drupal_http_request as in following code:

$opts['context'] = stream_context_create([ 'ssl' => [
  'cafile' => 'selfsigned_webfutura_eu.crt',
]]);
$result = drupal_http_request('https://selfsigned.webfutura.eu/demo-items.json', $opts);
print_r(json_decode($result->data));

// Results in 
Array
(
    [0] => stdClass Object
        (
            [name] => item-1
            [label] => Item 1
        )

    [1] => stdClass Object
        (
            [name] => item-2
            [label] => Item 2
        )

)

Drupal 7: Replace core HTTP client with Guzzle

Guzzle is a great and very standardized HTTP client library. Quite an important actor nowadays in the PHP eco-system. We can make a step further in our legacy Drupal 7 sites using it. The same process would be used for any other composer based library, so let’s jump out of the (Drupal) island !!

We may begin adding composer libraries to our Drupal 7 project initializing the project and answering a few questions. Directly we may add here the libraries we want, guzzlehttp/guzzle in our case:

$ composer init
Welcome to the Composer config generator  
This command will guide you through creating your composer.json config.
(..)
Define your dependencies.

Would you like to define your dependencies (require) interactively [yes]? yes
Search for a package: guzzlehttp/guzzle
Enter the version constraint to require (or leave blank to use the latest version): 
Using version ^7.3 for guzzlehttp/guzzle

Then in our custom code, we need to include the composer autoload file and we are ready to use the library !!

require_once 'vendor/autoload.php';

$client = new GuzzleHttp\Client([
  'base_uri' => 'https://selfsigned.webfutura.eu',
]);

$response = $client->get('demo-items.json', ['verify' => 'data/selfsigned_webfutura_eu.crt',]);
$response_body = $response->getBody();
print_r(json_decode($response_body>getContents()));

But wait, what if we have a big amount of code relying in drupal_http_request? Do I need to replace all my requests? Drupal 7 core developers who knows how many years ago provided ways to overcome this situation. We only need to set a custom alternative to the default function in our sites/default/files/settings.php file:

$conf['drupal_http_request_function'] = 'my_custom_http_request';

And declare it somewhere in our custom module code:

/**
 * @param $url
 * @param array $options
 * 
 * @see drupal_http_request()
 */
function my_custom_http_request($url, array $options = array()) {
  $client = new GuzzleHttp\Client();
  $response = $client->request($options['type'], $url, $options);
  $response_body = $response->getBody();
  return $response_body->getContents()
}

Obviously, this function will need to translate parameters and responses if we want to provide a plug-and-play replacement to core drupal_http_request but at least we will be using a state-of-the-art HTTP client in our code.


Comment on this post