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 restructure 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!