Debugging Guzzle HTTP request errors in Drupal

In the modern days of the internet, many websites are integrated with other websites, whether to pull data, share data, or authorize (sign-in) users. It's rare to find a website of a level of complexity that does not have at least one integration with other systems. Historically, in Drupal 6 and 7, the function drupal_http_request() was provided. This was powerful tool, that allowed for making dynamic requests to other websites. Drupal 8 however, being a paradigm re-think, tries to act like a hub, using other code that has been fully developed, rather than providing its own code which may not receive as much attention. So Drupal 8 dropped drupal_http_request() and instead incorporated the Guzzle PHP HTTP client. This is a full-fledged library for making HTTP requests to other systems, and is very powerful.

6 June 2019

Guzzle makes HTTP requests easy. When they work, it's like magic. However, as with all coding, getting something to work requires debugging, and this is where the Drupal implementation of Guzzle has a major usability problem - any returned messages are truncated, meaning that with the default settings, error messages that can help debug an issue are not accessible to the developer. This article will show developers how they can re-structure their Guzzle queries to log the full error to the Drupal log, instead of a truncated error that does not help fix the issue.

Standard Methodology

Generally, when making a Guzzle request, it is made using a try/catch paradigm, so that the site does not crash in the case of an error. When not using try/catch, a Guzzle error will result in a WSOD, which is as bad as it gets for usability. So let's take a look at an example of how Guzzle would request a page using a standard try/catch:

try {
  $client = \Drupal::httpClient();
  $result = $client->request('GET', 'https://www.google.com');
}
catch (\Exception $error) {
  $logger = \Drupal::logger('HTTP Client error');
  $logger->error($error->getMessage());
}

This code will request the results of www.google.com, and place them in the $result variable. In the case that the request failed for some reason, the system logs the result of $error->getMessage() to the Drupal log.

The problem, as mentioned in the intro, is that the value returned from $error->getMessage() contains a truncated version of the response returned from the remote website. If the developer is lucky, the text shown will contain enough information to debug the problem, but rarely is that the case. Often the error message will look something along the lines of:

Client error: `POST https://exaxmple.com/3.0/users` resulted in a `400 Bad Request` response: {"type":"http://developer.example.com/documentation/guides/error-glossary/","title":"Invalid Resource","stat (truncated...)

As can be seen, the full response is not shown. The actual details of the problem, and any suggestions as to a solution are not able to be seen. What we want to happen is that the full response details are logged, so we can get some accurate information as to what happened with the request.

Debugging Guzzle Errors

In the code shown above, we used the catch statement to catch \Exception. Generally developers will create a class that extends \Exception, allowing users to catch specific errors, finally catching \Exception as a generic default fallback.

When Guzzle hits an error, it throws the exception GuzzleHttp\Exception\GuzzleException. This allows us to catch this exception first to create our own log that contains the full response from the remote server.

We can do this, because GuzzleException provides the response object from the original request, which we can use to get the actual response body the remote server sent with the error. We then log that response body to the Drupal log.

use Drupal\Component\Render\FormattableMarkup;
use GuzzleHttp\Exception\GuzzleException;
try {
  $response = $client->request($method, $endpoint, $options);
}
// First try to catch the GuzzleException. This indicates a failed response from the remote API.
catch (GuzzleException $error) {
  // Get the original response
  $response = $error->getResponse();
  // Get the info returned from the remote server.
  $response_info = $response->getBody()->getContents();
  // Using FormattableMarkup allows for the use of <pre/> tags, giving a more readable log item.
  $message = new FormattableMarkup('API connection error. Error details are as follows:<pre>@response</pre>', ['@response' => print_r(json_decode($response_info), TRUE)]);
  // Log the error
  watchdog_exception('Remote API Connection', $error, $message);
}
// A non-Guzzle error occurred. The type of exception is unknown, so a generic log item is created. catch (\Exception $error) {
  // Log the error.
  watchdog_exception('Remote API Connection', $error, t('An unknown error occurred while trying to connect to the remote API. This is not a Guzzle error, nor an error in the remote API, rather a generic local error ocurred. The reported error was @error', ['@error' => $error->getMessage()));
}

With this code, we have caught the Guzzle exception, and logged the actual content of the response from the remote server to the Drupal log. If the exception thrown was any other kind of exception than GuzzleException, we are catching the generic \Exception class, and logging the given error message.

By logging the response details, our log entry will now look something like this:

Remote API connection error. Error details are as follows:

stdClass Object (
  [title] => Invalid Resource
  [status] => 400
  [detail] => The resource submitted could not be validated. For field-specific details, see the 'errors' array.
  [errors] => Array (
    [0] => stdClass Object (
      [field] => some_field
      [message] => Data presented is not one of the accepted values: 'Something', 'something else', or another thing'
    )
  )
)

* Note that this is just an example, and that each API will give its own response structure.

This is a much more valuable debug message than the original truncated message, which left us understanding that there had been an error, but without the information required to fix it.

Summary

Drupal 8 ships with Guzzle, an excellent HTTP client for making requests to other servers. However, the standard debugging method doesn't provide a helpful log message from Guzzle. This article shows how to catch Guzzle errors, so that the full response can be logged, making debugging of connection to remote servers and APIs much easier.

Happy Drupaling!