How to stack Exceptions in php guzzle - php

I have a site that uses a lot of apis to other places. I've been using curl and a bunch of if(!something) to do it, but I want to switch to a more robust solution, so I'm trying to use guzzle. But I am not sure how to handle the exceptions.
I did this..
class Api {
...
function request ($params) {
tries = 0;
while (tries < 3) {
try {$resp = $client->request(params);
} catch (\GuzzleHttpException $e) {
some stuff
if ($tries > 2) {
LOG error( failure info);
return [];
}
sleep 1;
++$tries;
}
break;
}
return $resp;
}
So my question is how do I better identify the errors here. I want to know why it failed... connection, 500, 404, timeout, etc. Don't know how to "stack" Exceptions. Guzzle has many different except types. (+ I'm on php 8 so errors should be exceptions too I think. ) Some guidance??

Related

Dealing with spatie asynchronous error handling

I'm working on an asynchronous process on a PHP project. I'm using a library named spatie/async. The code snippet is like below :
foreach (range(1, 2) as $i) {
$pool->add(function () use ($i) {
// Do a thing
try {
$result = $i / 0; // This will cause an error
return "Works";
} catch (\Exception $e) {
return -1;
}
})->then(function ($output) {
// Handle success
echo (output . "\n");
})->catch(function ($exception) {
// When an exception is thrown, it's caught and passed here.
echo "Sounds good, but don't work\n";
})
}
$pool->wait();
All I want is when the $result got an error, it will go into the inner catch, but instead, it goes down to the bottom catch which causing a different result from what I want.
The result that I want is :
-1
-1
But instead, the result is :
Sounds good, but don't work
Sounds good, but don't work
Can anyone help me to achieve the result as I want?
The problem of your code is, that it does not throw an Exception in the add method call. A division by 0 is just causing an error, but not ein exception. Instead of changing the whole php error handler, I 'd suggest to extend your logic a little bit in your add method call.
$divisor = 0;
$pool->add(function() use ($i, $divisor) {
try {
if ($divisor === 0) {
throw new \LogicException('Division by zero!');
}
return $i / $divisor;
} catch (\LogicException $e) {
return -1;
}
});
Another solution could be changing the error handling for the pool method call.
set_error_handler(function () {
throw new \LogicException('Ouch!');
});
$pool->add(function() use ($i) {
try {
$result = $i / 0;
} catch (\LogicException $e) {
return -1;
}
});
restore_error_handler();
Caution! Changing the error handler affects all upcoming errors. Even the errors thrown in your used library. Keep in mind, that these are code snippets. This is not tested or thougt to be used in production. Hope that helps out a little bit.

PHP Goutte try and retry

I need to crawl some data from a website. Some of the reasons for the target server, some crawl can not succeed, need to retry.The code is as follows:
private function fetchArchive($id) {
$url = 'xxxx/' . $id;
$attempt = 0;
$base = null;
if (Goutte::request('GET', $url)->filter('#table')->count() < 1) {
do {
try {
$base = Goutte::request('GET', $url)->filter('#table')->text();
} catch (InvalidArgumentException $e) {
$attempt++;
sleep(2);
break;
}
} while ($attempt <= 5);
}
In fact try($base = Goutte::request('GET', $url)->filter('#table')->text()) does not work and I recieve
"production.ERROR: InvalidArgumentException: The current node list is empty."
how do I fixed this?
Try to use \InvalidArgumentException (from the root namespace, yes).
Also consider to retry on the HTTP level, using a Guzzle's middleware (like in this example). It's better, because you handle exactly HTTP related errors in this case.
Because I used Laravel, so:
catch (\InvalidArgumentException $e) {...}

Returning useful error messages with PHP

I don't understand how to properly create and return useful error messages with PHP to the web.
I have a class
class Foo {
const OK_IT_WORKED = 0;
const ERR_IT_FAILED = 1;
const ERR_IT_TIMED_OUT = 3;
public function fooItUp(){
if(itFooed)
return OK_IT_WORKED;
elseif(itFooedUp)
return ERR_IT_FAILED;
elseif(itFooedOut)
return ERR_IT_TIMED_OUT;
}
}
And another class that uses this class to do something useful, then return the result to the user. I am just wondering where I put the string value for all my error messages.
class Bar {
public function doFooeyThings(stuff){
$res = $myFoo->fooItUp();
// now i need to tell the user what happened, but they don't understand error codes
if($res === Foo::OK_IT_WORKED)
return 'string result here? seems wrong';
elseif ($res === Foo::ERR_IT_FAILED)
return Foo::ERR_IT_FAILED_STRING; // seems redundant?
elseif($res === Foo:ERR_IT_TIMED_OUT)
return $res; // return number and have an "enum" in the client (js) ?
}
}
You should avoid returning error states whenever possible. Use exceptions instead. If you've never used exceptions before you can read about them here
There multiple ways you can utilize exceptions in your example. You could create custom exceptions for every error or for every category of error. More on custom exceptions here or you could create an instance of the default Exception class supplying it the error messages as strings.
The code below follows the second approach:
class Foo {
const OK_IT_WORKED = 0;
const ERR_IT_FAILED = 1;
const ERR_IT_TIMED_OUT = 3;
public function fooItUp(){
if(itFooed)
return OK_IT_WORKED;
else if(itFooedUp)
throw new Exception("It failed")
else if(itFooedOut)
throw new Exception("Request timed out");
}
}
I'm sure you can think of some more elegant messages than the ones I used. Anyway, you can then go ahead and handle those exceptions on the caller method using try/catch blocks:
class Bar {
public function doFooeyThings(stuff){
try
{
$res = myFoo->fooItUp();
}
catch(Exception $e)
{
//do something with the error message
}
}
}
Whatever exception is thrown from fooItUp will be "caught" by the catch block and handled by your code.
Two things you should also consider are:
It's best not to show your users detailed information about errors because those information could be used by users with malicious intent
Ideally you should have some kind of global exception handling
One solution is to use exceptions in conjunction with set_exception_handler().
<?php
set_exception_handler(function($e) {
echo "Error encountered: {$e->getMessage()}";
});
class ErrorMessageTest
{
public function isOk()
{
echo "This works okay. ";
}
public function isNotOkay()
{
echo "This will not work. ";
throw new RuntimeException("Violets are red, roses are blue!! Wha!?!?");
}
}
$test = new ErrorMessageTest();
$test->isOk();
$test->isNotOkay();
The set_exception_handler() method takes a callable that will accept an exception as its parameter. This let's you provide your own logic for a thrown exception in the event it isn't caught in a try/catch.
Live Demo
See also: set_exception_handler() documentation

How can I get Guzzle 6 to retry a request upon a 503 error in Laravel

I've written some code in Laravel 5.2 to retrieve results from an unrelible API source. However, it needs to be able to automatically retry the request on failed attempts, as the API call results in a 503 about a third of the time.
I'm use Guzzle to do this, and I think I know where to put the code that will intercept the 503 responses before they are processed; but I'm not sure what to actually write there.
The guzzle documentation doesn't offer much as far as retries go, and all of the examples I've come across of Guzzle 6 only show how to retrieve results (which I can already do), but not how to get it to repeat the request if needed.
I'm by no means asking anyone to do the work for me - but I think I'm approaching the limits of my understanding on this. If anybody can point me in the right direction, it'd be much appreciated :)
EDIT:
I will try and revise. Please consider the following code. In it, I want to send a GET request which should normally yield a JSON response.
DataController.php
$client = new \GuzzleHttp\Client();
$request = $client->request('GET', 'https://httpbin.org/status/503'); // URI is for testing purposes
When the response from this request is a 503, I can intercept it here:
Handler.php
public function render($request, Exception $e)
{
if ($e->getCode() == 503)
{
// Code that would tell Guzzle to retry the request 5 times with a 10s delay before failing completely
}
return parent::render($request, $e);
}
I don't know that that is the best place to put it, but the real problem is I don't know is what to write inside the if ($e->getCode() == 503)
Guzzle by default throws exceptions when a non 2** response is returned. In your case you're seeing a 503 response. Exceptions can be thought of as errors that the application can recover from. The way this works is with try catch blocks.
try {
// The code that can throw an exception will go here
throw new \Exception('A generic error');
// code from here down won't be executed, because the exception was thrown.
} catch (\Exception $e) {
// Handle the exception in the best manner possible.
}
You wrap the code that could throw an exception in the try portion of the block. Then you add your error handling code in the catch portion of the block. You can read the above link for more information on how php handles exceptions.
For your case, lets move the Guzzle call to it's own method in your controller:
public function performLookUp($retryOnError = false)
{
try {
$client = new \GuzzleHttp\Client();
$request = $client->request('GET', 'https://httpbin.org/status/503');
return $request->send();
} catch (\GuzzleHttp\Exception\BadResponseException $e) {
if ($retryOnError) {
return $this->performLookUp();
}
abort(503);
}
}
Now in your controller you can execute, $this->performLookUp(true);.
Just to add some information to clarify a few points that Logan made.
Guzzle "can" throw exceptions on a Response other then 2**/3**. It all depends upon how the GuzzleHttp\HandlerStack is created.
$stack = GuzzleHttp\HandlerStack::create();
$client = new Client(['handler'=> $stack]);
$client = new Client();
// These two methods of generating a client are functionally the same.
$stack = New GuzzleHttp\HandlerStack(new GuzzleHttp\Handler\CurlHandler());
$client = new Client(['handler'=> $stack]);
// This client will not throw exceptions, or perform any of the functions mentioned below.
The create method adds default handlers to the HandlerStack. When the HandlerStack is resolved, the handlers will execute in the following order:
Sending request:
http_errors - No op when sending a request. The response status code is checked in the response processing when returning a response promise up the stack.
allow_redirects - No op when sending a request. Following redirects occurs when a response promise is being returned up the stack.
cookies - Adds cookies to requests.
prepare_body - The body of an HTTP request will be prepared (e.g., add default headers like Content-Length, Content-Type, etc.).
send request with handler
Processing response:
prepare_body - no op on response processing.
cookies - extracts response cookies into the cookie jar.
allow_redirects - Follows redirects.
4.http_errors - throws exceptions when the response status code >= 300.
When provided no $handler argument, GuzzleHttp\HandlerStack::create() will choose the most appropriate handler based on the extensions available on your system. As indicated within the Handler Documentation
By manually creating your GuzzleHttp\HandlerStack, you can add middleware to the application. Given the context of your original question "how do i repeat the request" I believe you are most interested in the Retry Middleware that is provided within Guzzle 6.1. This is a middleware that retries requests based on the result of the provided decision function.
Documentation has yet to catch up with this class.
final class HttpClient extends \GuzzleHttp\Client
{
public const SUCCESS_CODE = 200;
private int $attemptsCount = 3;
private function __construct(array $config = [])
{
parent::__construct($config);
}
public static function new(array $config = []): self
{
return new self($config);
}
public function postWithRetry(string $uri, array $options = []): Response
{
$attemptsCount = 0;
$result = null;
do {
$attemptsCount++;
$isEnd = $attemptsCount === $this->attemptsCount;
try {
$result = $this->post(
$uri,
$options,
);
} catch (ClientException $e) {
$result = $e->getResponse();
} catch (GuzzleException $e) {
if ($isEnd) {
Logger::error($e->getMessage());
$result = $e->getResponse();
}
}
} while ($this->isNeedRetry($result, $attemptsCount));
return $result;
}
private function isNeedRetry(?Response $response, int $attemptsCount): bool
{
return $response === null && $attemptsCount < $this->attemptsCount;
}

Standard PHP error function with __LINE__ __FILE__etc?

so, instead of lots of instances of
if (odbc_exec($sql))
{
}
else
{
myErrorHandlingFunction();
}
I wrap that in a function
function myOdbxExec($sql)
{
if (odbc_exec($sql))
{
}
else
{
myErrorHandlingFunction();
}
}
BUT I would like myErrorHandlingFunction() to report things like __LINE__ __FILE__ etc
Which looks like I have to pass thoses infos to every call of helper functions, e.g. myOdbxExec($sql, __FILE__, __LINE__) which makes my code look messy.
function myErrorHandlingFunction($errorTExt, $fiel, $line)
{
// error reporting code goes here
}
function myOdbxExec($sql, $file, $line)
{
if (odbc_exec($sql))
{
}
else
{
myErrorHandlingFunction();
}
}
$sql = 'select * from ... blah, blah, blah...';
myOdbxExec($sql, __FILE__, __LINE__); // <==== this is *ugly*
In C I would hide it all behind a #define, e.g. #define MY_OFBC_EXEC(sql) myOdbxExec(sql, __FILE__, __LINE__)
1) (how) can I do that in PHP
2) what else is worth outoputting? e.g. error_get_last()? but that has no meaning if odbc_exec() fails ...
To rephrase the question - what's the generic approach to PHP error handling? (especially when set_error_handler() doesn't really apply?
Edit: just to be clear - I do want to handle exceptions, programming errors, etc, but, as my example shows, I also want to handle soemthings the teh PHP interpreter might noit consider to be an error, like odbc_exec() returning false().
Both the above answers mentioning debug_backtrace are answering your _______LINE_____ / _______FILE___ question with the debug_backtrace() function. I've wanted this answer too in the past, so have put together a quick function to handle it.
function myErrorHandlingFunction($message, $type=E_USER_NOTICE) {
$backtrace = debug_backtrace();
foreach($backtrace as $entry) {
if ($entry['function'] == __FUNCTION__) {
trigger_error($entry['file'] . '#' . $entry['line'] . ' ' . $message, $type);
return true;
}
}
return false;
}
You can then call, for instance, myErrorHandlingFunction('my error message');
or myErrorHandlingFunction('my fatal error message', E_USER_ERROR);
or try { ... } catch (Exception $e) { myErrorHandlingFunction($e->getMessage()); }
First off, you might want to consider using exception handling.
If for some reason, that doesn't appeal to you, you can use debug_backtrace inside your generic error handler to figure out where the handler was called from.
EDIT In response to OP's comment:
Exceptions don't have to come from PHP built-ins. You can throw your own. Since an ODBC error generally is an exceptional condition, just throw an exception when you detect one. (And catch it at some higher level).
<?PHP
if (! odbc_exec($sql) )
throw new DatabaseException('odbc_exec returned false, that bastard!');
Use debug_backtrace to figure out where a function was called from in your error handler. Whether you invoke this error handler by manually calling it, by throwing and catching exceptions, PHPs error_handler or any other method is up to the application design and doesn't really differ that much from other languages.

Categories