{"id":13182,"date":"2020-05-10T17:15:24","date_gmt":"2020-05-10T17:15:24","guid":{"rendered":"https:\/\/alexrusin.com\/?p=13182"},"modified":"2023-09-17T16:27:52","modified_gmt":"2023-09-17T16:27:52","slug":"retrying-and-logging-requests-with-guzzle","status":"publish","type":"post","link":"https:\/\/blog.alexrusin.com\/retrying-and-logging-requests-with-guzzle\/","title":{"rendered":"Retrying and Logging Requests with Guzzle"},"content":{"rendered":"\n
When consuming 3d party API, you may want to do two things:<\/p>\n\n\n\n
In this article we will look at how to implement the above features using Guzzle, a popular PHP library for making API calls.<\/p>\n\n\n\n
Let us scaffold our app<\/p>\n\n\n\n
composer init<\/em><\/p>\n\n\n\n composer require guzzlehttp\/guzzle:~6.0<\/em><\/p>\n\n\n\n composer require monolog\/monolog<\/em><\/p>\n\n\n\n composer require –dev phpunit\/phpunit:^8<\/em><\/p>\n\n\n\n We will be using PHPUnit to run our code. PHPUnit 9 requires PHP7.3. Since I have PHP7.2, I will stick with PHPUnit 8. We also need to create phpunit.xml file in the root of our project:<\/p>\n\n\n\n Let us also define PSR-4 autoloading in composer.json file. Complete composer.json file should look something like this.<\/p>\n\n\n\n Don\u2019t forget to run composer dump-autoload<\/em><\/p>\n\n\n\n Now we are ready to start creating our code. In src\/Api folder we are going to create Api.php file containing Api class.<\/p>\n\n\n\n The logging and retrying logic is defined in the class above. We use handler stack to incorporate this logic. Since this is a demo code, I also turned off ssl verification \u2018verify\u2019 => false. This is not a good practice to use in production. <\/p>\n\n\n\n Now let\u2019s create our class that will actually be used to make calls. We will be using mocky.io. In src\/Api create MockyApi class that will extend abstract Api.<\/p>\n\n\n\n Since we are using the logger in Guzzle stack it will read the body of the response. Response body is a stream (PSR-7). Unfortunately, it does not get rewound back after it is logged. We need to make sure we rewind it before returning response.<\/p>\n\n\n\n Using mocky.io, we created mock responses for getting tasks, creating a task and for failing to get tasks We used generated routes in the class above for getTasks(), createTask(), and failedGetTasks() functions.<\/p>\n\n\n\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n backupStaticAttributes=\"false\"\n bootstrap=\"vendor\/autoload.php\"\n colors=\"true\"\n convertErrorsToExceptions=\"true\"\n convertNoticesToExceptions=\"true\"\n convertWarningsToExceptions=\"true\"\n processIsolation=\"false\"\n stopOnFailure=\"false\">\n <testsuites>\n <testsuite name=\"Unit\">\n <directory suffix=\"Test.php\">.\/tests<\/directory>\n <\/testsuite>\n <\/testsuites>\n \n <filter>\n <whitelist processUncoveredFilesFromWhitelist=\"true\">\n <directory suffix=\".php\">.\/app<\/directory>\n <\/whitelist>\n <\/filter>\n <php>\n <env name=\"APP_ENV\" value=\"testing\"\/>\n <\/php>\n<\/phpunit><\/pre>\n\n\n\n
{\n \"name\": \"apr\/retrying-and-logging\",\n \"description\": \"Retrying and logging Guzzle requests\",\n \"authors\": [\n {\n \"name\": \"Alex Rusin\",\n \"email\": \"alex@email.com\"\n }\n ],\n \"require\": {\n \"guzzlehttp\/guzzle\": \"~6.0\",\n \"monolog\/monolog\": \"^2.0\"\n },\n \"require-dev\": {\n \"phpunit\/phpunit\": \"^8\"\n },\n\n \"autoload\": {\n \"psr-4\": {\n \"App\\\\\": \"src\/\"\n }\n },\n\n \"autoload-dev\": {\n \"psr-4\": {\n \"Tests\\\\\": \"tests\/\"\n }\n }\n}\n<\/pre>\n\n\n\n
<?php\n\nnamespace App\\Api;\n\nuse GuzzleHttp\\Client;\nuse GuzzleHttp\\HandlerStack;\nuse GuzzleHttp\\Middleware;\nuse GuzzleHttp\\MessageFormatter;\nuse GuzzleHttp\\Psr7\\Request;\nuse GuzzleHttp\\Psr7\\Response;\nuse GuzzleHttp\\Exception\\ConnectException;\nuse GuzzleHttp\\Exception\\RequestException;\nuse Psr\\Log\\LoggerInterface;\n\nabstract class Api \n{\n protected $logger;\n protected $client;\n const MAX_RETRIES = 3;\n\n public function __construct(LoggerInterface $logger)\n {\n $this->logger = $logger;\n\n $this->client = new Client([\n 'base_uri' => $this->baseUri,\n 'handler' => $this->createHandlerStack(),\n 'timeout' => 30.0,\n 'headers' => [\n 'Accept' => 'application\/json',\n ],\n\t\t 'verify' => false\n ]);\n }\n\n protected function createHandlerStack()\n {\n $stack = HandlerStack::create();\n $stack->push(Middleware::retry($this->retryDecider(), $this->retryDelay()));\n return $this->createLoggingHandlerStack($stack);\n }\n \n protected function createLoggingHandlerStack(HandlerStack $stack)\n {\n $messageFormats = [\n '{method} {uri} HTTP\/{version}',\n 'HEADERS: {req_headers}',\n 'BODY: {req_body}',\n 'RESPONSE: {code} - {res_body}',\n ];\n\n foreach ($messageFormats as $messageFormat) {\n \/\/ We'll use unshift instead of push, to add the middleware to the bottom of the stack, not the top\n $stack->unshift(\n $this->createGuzzleLoggingMiddleware($messageFormat)\n );\n }\n \n return $stack;\n }\n\n protected function createGuzzleLoggingMiddleware(string $messageFormat)\n {\n return Middleware::log(\n $this->logger,\n new MessageFormatter($messageFormat)\n );\n }\n\n protected function retryDecider()\n {\n return function (\n $retries,\n Request $request,\n Response $response = null,\n RequestException $exception = null\n ) {\n \/\/ Limit the number of retries to MAX_RETRIES\n if ($retries >= self::MAX_RETRIES) {\n return false;\n }\n\n \/\/ Retry connection exceptions\n if ($exception instanceof ConnectException) {\n $this->logger->info('Timeout encountered, retrying');\n return true;\n }\n\n if ($response) {\n \/\/ Retry on server errors\n if ($response->getStatusCode() >= 500) {\n $this->logger->info('Server 5xx error encountered, retrying...');\n return true;\n }\n }\n\n return false;\n };\n }\n\n \/**\n * delay 1s 2s 3s 4s 5s ...\n *\n * @return callable\n *\/\n protected function retryDelay()\n {\n return function ($numberOfRetries) {\n return 1000 * $numberOfRetries;\n };\n }\n}\n<\/pre>\n\n\n\n
<?php\n\nnamespace App\\Api;\n\nclass MockyApi extends Api\n{\n protected $baseUri = 'https:\/\/www.mocky.io\/v2\/';\n\n public function getTasks()\n {\n $response = $this->client->get('5eb81e152d00003e2b357c06');\n $response->getBody()->rewind();\n \n return $response; \n }\n\n public function createTask()\n {\n $response = $this->client->post('5eb81ee82d00005800357c07', [\n 'json' => [\n 'description' => 'Write a blog post'\n ]\n ]);\n $response->getBody()->rewind();\n\n return $response;\n }\n\n public function failedGetTasks()\n {\n $response = $this->client->get('5eb829d42d00003e2b357c26');\n $response->getBody()->rewind();\n \n return $response; \n }\n}\n<\/pre>\n\n\n\n