Catching exceptions from Guzzle
Asked Answered
P

9

107

I'm trying to catch exceptions from a set of tests I'm running on an API I'm developing and I'm using Guzzle to consume the API methods. I've got the tests wrapped in a try/catch block but it is still throwing unhandled exception errors. Adding an event listener as described in their docs doesn't seem to do anything. I need to be able to retrieve the responses that have HTTP codes of 500, 401, 400, in fact anything that isn't 200 as the system will set the most appropriate code based on the result of the call if it didn't work.

Current code example

foreach($tests as $test){

        $client = new Client($api_url);
        $client->getEventDispatcher()->addListener('request.error', function(Event $event) {        

            if ($event['response']->getStatusCode() == 401) {
                $newResponse = new Response($event['response']->getStatusCode());
                $event['response'] = $newResponse;
                $event->stopPropagation();
            }            
        });

        try {

            $client->setDefaultOption('query', $query_string);
            $request = $client->get($api_version . $test['method'], array(), isset($test['query'])?$test['query']:array());


          // Do something with Guzzle.
            $response = $request->send();   
            displayTest($request, $response);
        }
        catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {

            $req = $e->getRequest();
            $resp =$e->getResponse();
            displayTest($req,$resp);
        }
        catch (Guzzle\Http\Exception\ServerErrorResponseException $e) {

            $req = $e->getRequest();
            $resp =$e->getResponse();
            displayTest($req,$resp);
        }
        catch (Guzzle\Http\Exception\BadResponseException $e) {

            $req = $e->getRequest();
            $resp =$e->getResponse();
            displayTest($req,$resp);
        }
        catch( Exception $e){
            echo "AGH!";
        }

        unset($client);
        $client=null;

    }

Even with the specific catch block for the thrown exception type I am still getting back

Fatal error: Uncaught exception 'Guzzle\Http\Exception\ClientErrorResponseException' with message 'Client error response [status code] 401 [reason phrase] Unauthorized [url]

and all execution on the page stops, as you'd expect. The addition of the BadResponseException catch allowed me to catch 404s correctly, but this doesn't seem to work for 500 or 401 responses. Can anyone suggest where I am going wrong please.

Pullulate answered 15/7, 2013 at 15:44 Comment(1)
Is this code under a namespace? If so, unless you're useing the exceptions, you may need to prefix them with `` to explicitly state the FQ Class. So, for example, '\Guzzle\Http\Exception\ClientErrorResponseException'Dialectology
S
21

If the Exception is being thrown in that try block then at worst case scenario Exception should be catching anything uncaught.

Consider that the first part of the test is throwing the Exception and wrap that in the try block as well.

Steadfast answered 15/7, 2013 at 15:51 Comment(1)
You are correct, there was a test outside of the try/catch that was throwing the exception. Silly mistake, thanks for the help.Pullulate
W
171

Depending on your project, disabling exceptions for guzzle might be necessary. Sometimes coding rules disallow exceptions for flow control. You can disable exceptions for Guzzle 3 like this:

$client = new \Guzzle\Http\Client($httpBase, array(
  'request.options' => array(
     'exceptions' => false,
   )
));

This does not disable curl exceptions for something like timeouts, but now you can get every status code easily:

$request = $client->get($uri);
$response = $request->send();
$statuscode = $response->getStatusCode();

To check, if you got a valid code, you can use something like this:

if ($statuscode > 300) {
  // Do some error handling
}

... or better handle all expected codes:

if (200 === $statuscode) {
  // Do something
}
elseif (304 === $statuscode) {
  // Nothing to do
}
elseif (404 === $statuscode) {
  // Clean up DB or something like this
}
else {
  throw new MyException("Invalid response from api...");
}

For Guzzle 5.3

$client = new \GuzzleHttp\Client(['defaults' => [ 'exceptions' => false ]] );

Thanks to @mika

For Guzzle 6

$client = new \GuzzleHttp\Client(['http_errors' => false]);
Whirlybird answered 9/2, 2015 at 18:42 Comment(4)
Ever had a strange bug caused by a missing break ;-) But sure, it would be a good solution if you have multiple status codes you have to handle in the same way. I prefer if, cause switch just supports ==.Whirlybird
Thanks for mentioning request.options. Resolved my problem and saved me looking it up properly. :)Rademacher
Or in Guzzle5.3: $client = new \GuzzleHttp\Client(['defaults' => [ 'exceptions' => false ]] );Vanillin
This saved my bacon on an urgent project. Thanks Trendfischer and SO!Outline
D
49

To catch Guzzle errors you can do something like this:

try {
    $response = $client->get('/not_found.xml')->send();
} catch (Guzzle\Http\Exception\BadResponseException $e) {
    echo 'Uh oh! ' . $e->getMessage();
}

... but, to be able to "log" or "resend" your request try something like this:

// Add custom error handling to any request created by this client
$client->getEventDispatcher()->addListener(
    'request.error', 
    function(Event $event) {

        //write log here ...

        if ($event['response']->getStatusCode() == 401) {

            // create new token and resend your request...
            $newRequest = $event['request']->clone();
            $newRequest->setHeader('X-Auth-Header', MyApplication::getNewAuthToken());
            $newResponse = $newRequest->send();

            // Set the response object of the request without firing more events
            $event['response'] = $newResponse;

            // You can also change the response and fire the normal chain of
            // events by calling $event['request']->setResponse($newResponse);

            // Stop other events from firing when you override 401 responses
            $event->stopPropagation();
        }

});

... or if you want to "stop event propagation" you can overridde event listener (with a higher priority than -255) and simply stop event propagation.

$client->getEventDispatcher()->addListener('request.error', function(Event $event) {
if ($event['response']->getStatusCode() != 200) {
        // Stop other events from firing when you get stytus-code != 200
        $event->stopPropagation();
    }
});

thats a good idea to prevent guzzle errors like:

request.CRITICAL: Uncaught PHP Exception Guzzle\Http\Exception\ClientErrorResponseException: "Client error response

in your application.

Drawbar answered 25/9, 2013 at 14:11 Comment(1)
This is no longer possible in Guzzle 6. Any idea how to do this with a middleware?Grenade
M
39

In my case I was throwing Exception on a namespaced file, so php tried to catch My\Namespace\Exception therefore not catching any exceptions at all.

Worth checking if catch (Exception $e) is finding the right Exception class.

Just try catch (\Exception $e) (with that \ there) and see if it works.

Monumental answered 16/2, 2017 at 16:59 Comment(3)
I wish I had scrolled down to this error the first time I had the same question. For me I was using outdated Guzzle Exception names and not catching the generic Exception because I wasn't at the root Namesapce. Adding the backslash before Exception started catching the generic Exception allowing me to see my name mismatch errors on the more specific Guzzle Exceptions. See comments on https://mcmap.net/q/138651/-try-catch-block-in-php-not-catching-exception.Biondo
This was the exact issue I had too. Good answerBedevil
you are a saintGallicize
S
21

If the Exception is being thrown in that try block then at worst case scenario Exception should be catching anything uncaught.

Consider that the first part of the test is throwing the Exception and wrap that in the try block as well.

Steadfast answered 15/7, 2013 at 15:51 Comment(1)
You are correct, there was a test outside of the try/catch that was throwing the exception. Silly mistake, thanks for the help.Pullulate
W
15

You need to add a extra parameter with http_errors => false

$request = $client->get($url, ['http_errors' => false]);
Wilford answered 14/9, 2015 at 7:19 Comment(1)
Docs: docs.guzzlephp.org/en/stable/request-options.html#http-errorsSclerosis
K
11

I want to update the answer for exception handling in Psr-7 Guzzle, Guzzle7 and HTTPClient(expressive, minimal API around the Guzzle HTTP client provided by laravel).

Guzzle7 (same works for Guzzle 6 as well)

Using RequestException, RequestException catches any exception that can be thrown while transferring requests.

try{
  $client = new \GuzzleHttp\Client(['headers' => ['Authorization' => 'Bearer ' . $token]]);
  
  $guzzleResponse = $client->get('/foobar');
  // or can use
  // $guzzleResponse = $client->request('GET', '/foobar')
    if ($guzzleResponse->getStatusCode() == 200) {
         $response = json_decode($guzzleResponse->getBody(),true);
         //perform your action with $response 
    } 
}
catch(\GuzzleHttp\Exception\RequestException $e){
   // you can catch here 400 response errors and 500 response errors
   // You can either use logs here use Illuminate\Support\Facades\Log;
   $error['error'] = $e->getMessage();
   $error['request'] = $e->getRequest();
   if($e->hasResponse()){
       if ($e->getResponse()->getStatusCode() == '400'){
           $error['response'] = $e->getResponse(); 
       }
   }
   Log::error('Error occurred in get request.', ['error' => $error]);
}catch(Exception $e){
   //other errors 
}

Psr7 Guzzle

use GuzzleHttp\Psr7;
use GuzzleHttp\Exception\RequestException;

try {
    $client->request('GET', '/foo');
} catch (RequestException $e) {
    $error['error'] = $e->getMessage();
    $error['request'] = Psr7\Message::toString($e->getRequest());
    if ($e->hasResponse()) {
        $error['response'] = Psr7\Message::toString($e->getResponse());
    }
    Log::error('Error occurred in get request.', ['error' => $error]);
}

For HTTPClient

use Illuminate\Support\Facades\Http;
try{
    $response = Http::get('http://api.foo.com');
    if($response->successful()){
        $reply = $response->json();
    }
    if($response->failed()){
        if($response->clientError()){
            //catch all 400 exceptions
            Log::debug('client Error occurred in get request.');
            $response->throw();
        }
        if($response->serverError()){
            //catch all 500 exceptions
            Log::debug('server Error occurred in get request.');
            $response->throw();
        }
        
    }
 }catch(Exception $e){
     //catch the exception here
 }

Krenn answered 30/10, 2020 at 6:27 Comment(1)
RequestException seems to be a good way to differentiateBalboa
B
6

Old question, but Guzzle adds the response within the exception object. So a simple try-catch on GuzzleHttp\Exception\ClientException and then using getResponse on that exception to see what 400-level error and continuing from there.

Birddog answered 22/9, 2016 at 19:52 Comment(0)
L
2

I was catching GuzzleHttp\Exception\BadResponseException as @dado is suggesting. But one day I got GuzzleHttp\Exception\ConnectException when DNS for domain wasn't available. So my suggestion is - catch GuzzleHttp\Exception\ConnectException to be safe about DNS errors as well.

Lanitalank answered 1/8, 2016 at 8:35 Comment(1)
sounds like you should be catching GuzzleHttp\Exception\RequestException which is the parent of ConnectException, BadResponseException and TooManyRedirectsException.Escurial
A
1

If you are using the latest version say 6^ and you have a JSON parameter, you can add 'http_errors' => false to the array together with the JSON as seen below enter image description here

I was looking out for away to do this i.e with my JSON in there but couldn't find a straight answer.

Adina answered 21/5, 2021 at 9:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.