With the release of Drupal 8 comes a new way of making web requests, available via the Drupal::httpClient. This is simply a wrapper for the wonderful Guzzle HTTP Client. In this post, we'll take a look at how we can use the Drupal::httpClient class for making HTTP requests in a module. This is particularly useful when you wish to communicate with external websites or web services.
In Drupal 7, you would have used the drupal_http_request function for sending HTTP requests. This functionality now exists in Drupal::httpClient for Drupal 8.
Drupal and Guzzle (in short)
According to the Guzzle project page, "Guzzle is a PHP HTTP client and framework for building RESTful web service clients."
Guzzle utilizes PSR-7 as the HTTP message interface. PSR-7 describes common interfaces for representing HTTP messages. This allows Guzzle to work with any other library that utilizes PSR-7 message interfaces.
You can check the version of Guzzle that you’re using by taking a look at the composer.lock file in your Drupal project directory.
Drupal 8.0.1 comes with Guzzle 6.1.0:
{
"name": "guzzlehttp/guzzle",
"version": "6.1.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "66fd14b4d0b8f2389eaf37c5458608c7cb793a81"
},
// ...
},
The Guzzle documentation is available here.
Drupal::httpClient in a module
Data.gov provides a catalog of data via CKAN, a powerful open source data platform that includes a robust API. We're going to take a look at some examples using the CKAN API, full documentation is available here.
First, let's take a quick look at how we make requests in Drupal. You can initialize a client like so:
$client = \Drupal::httpClient();
Pass the full URL in your request:
$client->request('GET', 'http://demo.ckan.org/api/3/action/package_list');
Guzzle also provides a list of synchronous methods for making requests, a full list is available here:
You can make GET requests as follows:
$request = $client->get('http://demo.ckan.org/api/3/action/package_list');
$response = $request->getBody();
Next, let's POST some JSON to a remote API:
$client = \Drupal::httpClient();
$request = $client->post('http://demo.ckan.org/api/3/action/group_list', [
'json' => [
'id'=> 'data-explorer'
]
]);
$response = json_decode($request->getBody());
In the client->post()
method above, we pass in a URL string, and an array of request options. In this case, 'json', and an array of the properties we'd like to send as JSON. Guzzle takes care of adding a 'Content-Type','application/json' header, as well as json_encoding
the 'json' array. We then call json_decode
to decode the response of our request.
A full list of request options is available on the project's website: Guzzle Request Options.
Example: HTTP basic authentication
What about handling HTTP basic authentication with GitHub's API, for example?
$client = \Drupal::httpClient();
$request = $client->get('https://api.github.com/user', [
'auth' => ['username','password']
]);
$response = $request->getBody();
Exception handling
When using Drupal::httpClient, you should always wrap your requests in a try/catch block, to handle any exceptions. Here is an example of logging Drupal::httpClient request exceptions via watchdog_exception
.
$client = \Drupal::httpClient();
try {
$response = $client->get('http://demo.ckan.org/api/3/action/package_list');
$data = $response->getBody();
}
catch (RequestException $e) {
watchdog_exception('my_module', $e->getMessage());
}
You can get a full list of Exception types simply by listing the contents of <drupal_root>/vendor/guzzlehttp/guzzle/src/Exception. Utilizing this list allows you to provide different behavior based on exception type.
At the time of writing, the contents of that is as follows:
BadResponseException.php
ClientException.php
ConnectException.php
GuzzleException.php
RequestException.php
SeekException.php
ServerException.php
TooManyRedirectsException.php
TransferException.php
Guzzle clients use a handler and middleware system to send HTTP requests. You can refer to the documentation for more information about creating your own handlers and middleware to allow for more fine grained control of your HTTP workflow.
Custom Http Client
Changing the properties of an Http Client, like base_uri can be done by using the ClientFactory class, and creating your own Http Client.
We do this by creating a module. Let's call our module custom_http_client.
Create a custom_http_client.info.yml:
name: Custom Http Client
type: module
description: A custom HTTP client
core: 8.x
package: Custom
Create a custom_http_client.services.yml and add the following content:
# Service definition in YAML.
services:
custom_http_client.client:
class: GuzzleHttp\Client
factory: custom_http_client.client.factory:get
custom_http_client.client.factory:
class: Drupal\custom_http_client\ClientFactory
Then we create the following class at 'custom_http_client/src/ClientFactory.php':
namespace Drupal\custom_http_client;
use GuzzleHttp\Client;
class ClientFactory {
/**
* Return a configured Client object.
*/
public function get() {
$config = [
'base_uri' => 'https://example.com',
];
$client = new Client($config);
return $client;
}
}
You can then load this service to use your custom http client anywhere you need to.
You should load your service via a container, like so:
/**
* GuzzleHttp\Client definition.
*
* @var GuzzleHttp\Client
*/
protected $custom_http_client_client;
public function __construct(
Client $custom_http_client_client
) {
parent::__construct();
$this->custom_http_client_client = $custom_http_client_client;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('custom_http_client.client')
);
}
Then you can access it via:
$this->custom_http_client_client
Update: Thanks to @Mediacurrent for pointing out this alternative approach in the comments:
It should be noted that Drupal 8 core provides another service "http_client_factory", which returns a
ClientFactory
object whosefromOptions()
method can be used to create a Client with any custom properties like 'base_uri', 'headers', etc. See this Drupal Answers post for an example: https://drupal.stackexchange.com/a/219203/50788
The generation of a module and service can be completed using Drupal Console; check out the documentation links below.
Comments
What's the best way to instantiate Drupal::httpClient ? Does
$client = \Drupal::httpClient();
create a dependency that's going to make your method difficult to unit test?
Like with any service, the best way is to use dependency injection. Check out this link for more info: https://knpuniversity.com/screencast/drupal8-under-the-hood/get-service…
Totally agree with @willwh comment. Is a good practice to inject a service whenever is possible.
I was trying to add a reply but my answer got too long, I decided to post on a gist file, you can find my answer about injecting the service into a class and some code examples at this link:
https://gist.github.com/jmolivas/ca258d7f2742d9e1aae4
Thanks jmolivas :)
You can also search for services here, https://api.drupal.org/api/drupal/services, for example, typing 'http', you'll see the 'http_client' service name returned in the results!
Why is there so much swapping between $client = \Drupal::httpClient(); and $client = new Client('https://api.github.com');? is there a set way we're supposed to make the calls?
I've also found that I cannot pass a base url into httpClient. I can add one to Client but it has to be in the form of 'base_uri' => 'http://some.url' and not base_url. Have there been some changes in core since this post?
Just wanting to know what best practise is for making these calls to a remote server.
Yeah the switching Client call methods isn't clear why its done. Was about to ask the same question as Mark A.
A few blog posts knocking around on the 'net - no definitive answers and most aren't even using the Drupal::httpClient method either -- just hitting Guzzle direct - is that a problem ?
Hi Guys!
In terms of the base_uri property, you're correct, this is indeed 'base_uri' and not 'base_url'. I've updated the post to reflect that.
I've updated my post to just be using the Drupal HTTP client. This class is simply a wrapper for Guzzle, so you could use Guzzle directly if you wish. I hope this clears things up.
Please let me know if you have any other questions!
Hi.
Thanks for updating the post. I still can't get the base_uri to work. I can use it going straight through Guzzle but I get a cURL malformed error when trying to use httpClient with anything other than the full url as the endpoint. I want to keep all my code using the Drupal wrappers for things so I guess I'll just send the whole thing through every time.
Sorry it took me a little while to get back to you on this Mark. I've updated the blog post with some instructions on creating your own custom httpClient using the ClientFactory service.
Please let me know if that helps!
Look, a cutesy new name for existing functionality! How useful!
This post was super helpful. One issue I had was an unexpected error when dealing with the Mailchimp API. It was hard to debug because the $e (or $e->getMessage()) returns a truncated response.
Client error: `GET https://us13.api.mailchimp.com/3.0/lists/f041370629/interest-categories…` resulted in a `401 Unauthorized` response:
{"type":"http://developer.mailchimp.com/documentation/mailchimp/guides/error-glo…","title":"API Key Invalid","statu (truncated...)
Is there a way I can get at that full error message? It's obviously telling me some valuable information.
Does the exception have a
$e->getResponse()
method that can be used to retrieve the full response? Maybe something likeprint $e->getResponse()->getBody()->getContents();
.Ah! Perfect. I was trying a form of that, but I didn't properly chain it together after ->getResponse().
Thank you for this answer. I knew there had to be a way!
I think this is where my struggle is. I was looking all over RequestException.php in the guzzle package trying to find a way to get that information. And now you have solved it, but embarrassingly, even after knowing that answer, I'm not sure how I would have discovered that myself. It's just my ignorance at this point, but I'm just a bit lost how I could figure out that I needed getBody() and then getContents().
Generally I would say the best way to figure these things out is to use a debugger. Personally, I like Xdebug + phpStorm. https://drupalize.me/videos/debugging-phpstorm?p=2017
You can set a breakpoint in your code where the exception is getting caught, and then you'll be able to poke around in real time at the various objects that have been created the methods/properties they contain.
Additionally you can start reading through the source code. Start with the Exception class, which has the `getResponse()` method, figure out what that method does, and what it returns, then look at that class, and so on, and so on until you find the method/data you need.
Thanks for this most useful post!
The original post mentions:
"Changing the properties of an Http Client, like base_uri can be done by using the ClientFactory class, and creating your own Http Client."
It should be noted that Drupal 8 core provides another service "http_client_factory", which returns a ClientFactory object whose fromOptions() method can be used to create a Client with any custom properties like base_uri, headers, etc. See this Drupal Answers post for an example: https://drupal.stackexchange.com/a/219203/50788
Oh nice. Thanks for the tip. I'll update the post with information about this as well.
The try/catch block did not properly catch the error for me until I added the following use statement to my PHP file:
use GuzzleHttp\Exception\RequestException;
After that, it worked perfectly. I have that issue a lot with try/catch, so I always make sure to include the exception type class.
Hello.
How I can submit a file from Drupal to an external endpoint with HttpClient?
probably should take this down and provide an alternative - the project pages says it is obsolete and unsupported
Thank you Phil
Add new comment