175

I would like to handle errors from Guzzle when the server returns 4xx and 5xx status codes. I make a request like this:

$client = $this->getGuzzleClient();
$request = $client->post($url, $headers, $value);
try {
    $response = $request->send();
    return $response->getBody();
} catch (\Exception $e) {
    // How can I get the response body?
}

$e->getMessage returns code info but not the body of the HTTP response. How can I get the response body?

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
domos
  • 1,954
  • 2
  • 12
  • 12
  • 1
    This question is related to this question http://stackoverflow.com/questions/17658283/catching-exceptions-from-guzzle/28416973#28416973 and the answers there might be of some help, too. – Trendfischer Mar 16 '16 at 10:31

10 Answers10

332

Guzzle 6.x

Per the docs, the exception types you may need to catch are:

  • GuzzleHttp\Exception\ClientException for 400-level errors
  • GuzzleHttp\Exception\ServerException for 500-level errors
  • GuzzleHttp\Exception\BadResponseException for both (it's their superclass)

Code to handle such errors thus now looks something like this:

$client = new GuzzleHttp\Client;
try {
    $client->get('http://google.com/nosuchpage');    
}
catch (GuzzleHttp\Exception\ClientException $e) {
    $response = $e->getResponse();
    $responseBodyAsString = $response->getBody()->getContents();
}
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
  • 15
    For me `$response->getBody()->getContents()` would return an empty string. I then stumbled across this in [the docs](https://github.com/guzzle/guzzle/blob/master/docs/quickstart.rst): `\GuzzleHttp\Psr7\str($e->getResponse())` Casting the response as a Psr7 String got me a nicely formatted and complete error message. – Andy Place Dec 20 '16 at 23:29
  • 3
    @AndyPlace after taking a glance at PSR 7 (which wasn't referenced by the section of the docs that I link to at the time that I wrote this answer, but is now) it's not immediately obvious to me why calling `Psr7\str()` would have different results to `->getContents()`. Do you have a minimal example demonstrating this, that might let me understand this and perhaps update this answer? – Mark Amery Dec 20 '16 at 23:55
  • 48
    Worth mentioning that the `'http_errors' => false` option can be passed in the Guzzle request which disables throwing exceptions. You can then get the body with `$response->getBody()` no matter what the status code is, and you can test the status code if necessary with `$response->getStatusCode()`. – tremby Aug 23 '17 at 22:38
  • 4
    As @AndyPlace, `$response->getBody()->getContents()` gives me an empty string in one case, I don't understand why. But using `\GuzzleHttp\Psr7\str()` returns all the HTTP response as a string, and I would only the HTTP body. As said in the [documentation](https://github.com/guzzle/guzzle/blob/master/docs/quickstart.rst#using-responses), the body can be used by casting it to string. `$stringBody = (string) $clientException->getResponse()->getBody();` – Anthony Dec 12 '17 at 15:20
  • 1
    This did it for me, although I was getting a `\GuzzleHttp\Exception\RequestException` instead that returned a `400` status code. try { $request->api('POST', 'endpoint.json'); } catch (RequestException $e) { print_r($e->getResponse()->getBody()->getContents()); } – jpcaparas Apr 10 '19 at 01:52
  • 1
    @AndyPlace The body is stored in a stream, so the same instruction $response->getBody()->getContents() returns different results when called multiple times: the real body string, when called for the first time, an empty string all subsequent times. str() doesn't empty the stream, so that gives consistent results. – Vito Meuli Oct 20 '22 at 15:20
101

Guzzle 3.x

Per the docs, you can catch the appropriate exception type (ClientErrorResponseException for 4xx errors) and call its getResponse() method to get the response object, then call getBody() on that:

use Guzzle\Http\Exception\ClientErrorResponseException;

...

try {
    $response = $request->send();
} catch (ClientErrorResponseException $exception) {
    $responseBody = $exception->getResponse()->getBody(true);
}

Passing true to the getBody function indicates that you want to get the response body as a string. Otherwise you will get it as instance of class Guzzle\Http\EntityBody.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
sebbo
  • 2,929
  • 2
  • 20
  • 37
69

While the answers above are good they will not catch network errors. As Mark mentioned, BadResponseException is just a super class for ClientException and ServerException. But RequestException is also a super class of BadResponseException. RequestException will be thrown for not only 400 and 500 errors but network errors and infinite redirects too. So let's say you request the page below but your network is playing up and your catch is only expecting a BadResponseException. Well your application will throw an error.

It's better in this case to expect RequestException and check for a response.

try {
  $client->get('http://123123123.com')
} catch (RequestException $e) {

  // If there are network errors, we need to ensure the application doesn't crash.
  // if $e->hasResponse is not null we can attempt to get the message
  // Otherwise, we'll just pass a network unavailable message.
  if ($e->hasResponse()) {
    $exception = (string) $e->getResponse()->getBody();
    $exception = json_decode($exception);
    return new JsonResponse($exception, $e->getCode());
  } else {
    return new JsonResponse($e->getMessage(), 503);
  }

}
Mark Amery
  • 143,130
  • 81
  • 406
  • 459
chap
  • 1,860
  • 5
  • 24
  • 39
  • is `JsonResponse` a class from Guzzle? – ᴍᴇʜᴏᴠ May 29 '20 at 06:06
  • `JsonResponse` comes from Symfony – chap Jun 01 '20 at 04:50
  • Is this answer [still] correct? Shouldn't you catch `TransferException` if you want *both* `ClientException` and `ConnectException`? See the exception tree [https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions] here. – EML Mar 16 '23 at 13:20
34

As of 2020 here is what I elaborated from the answers above and Guzzle docs to handle the exception, get the response body, status code, message and the other sometimes valuable response items.

try {
    /**
     * We use Guzzle to make an HTTP request somewhere in the
     * following theMethodMayThrowException().
     */
    $result = theMethodMayThrowException();
} catch (\GuzzleHttp\Exception\RequestException $e) {
    /**
     * Here we actually catch the instance of GuzzleHttp\Psr7\Response
     * (find it in ./vendor/guzzlehttp/psr7/src/Response.php) with all
     * its own and its 'Message' trait's methods. See more explanations below.
     *
     * So you can have: HTTP status code, message, headers and body.
     * Just check the exception object has the response before.
     */
    if ($e->hasResponse()) {
        $response = $e->getResponse();
        var_dump($response->getStatusCode()); // HTTP status code;
        var_dump($response->getReasonPhrase()); // Response message;
        var_dump((string) $response->getBody()); // Body, normally it is JSON;
        var_dump(json_decode((string) $response->getBody())); // Body as the decoded JSON;
        var_dump($response->getHeaders()); // Headers array;
        var_dump($response->hasHeader('Content-Type')); // Is the header presented?
        var_dump($response->getHeader('Content-Type')[0]); // Concrete header value;
    }
}
// process $result etc. ...

Voila. You get the response's information in conveniently separated items.

Side Notes:

With catch clause we catch the inheritance chain PHP root exception class \Exception as Guzzle custom exceptions extend it.

This approach may be useful for use cases where Guzzle is used under the hood like in Laravel or AWS API PHP SDK so you cannot catch the genuine Guzzle exception.

In this case, the exception class may not be the one mentioned in the Guzzle docs (e.g. GuzzleHttp\Exception\RequestException as the root exception for Guzzle).

So you have to catch \Exception instead but bear in mind it is still the Guzzle exception class instance.

Though use with care. Those wrappers may make Guzzle $e->getResponse() object's genuine methods not available. In this case, you will have to look at the wrapper's actual exception source code and find out how to get status, message, etc. instead of using Guzzle $response's methods.

If you call Guzzle directly yourself you can catch GuzzleHttp\Exception\RequestException or any other one mentioned in their exceptions docs with respect to your use case conditions.

Valentine Shi
  • 6,604
  • 4
  • 46
  • 46
  • 4
    You shouldn't call methods on your `$response` object when handling exceptions unless you have checked `$e->hasResponse()`, otherwise `$response` may be `null` and any method calls will cause a fatal error. – pwaring Oct 01 '19 at 08:22
  • 1
    @pwaring, true. Exactly as the Guzzle exceptions docs say. Updated the answer. Thank you. – Valentine Shi Oct 01 '19 at 14:24
  • 2
    ... but this is still problematic after the fix. You're catching all exceptions, not just Guzzle ones, but then you're calling `$e->hasResponse` on the result, a method which, of course, doesn't exist for non-Guzzle exceptions. So if you raise a non-Guzzle exception from `theMethodMayThrowException()`, this code will catch it, try to call a non-existent method, and crash because of the non-existent method, effectively hiding the true cause of the error. Preferable would be to catch `GuzzleHttp\Exception\RequestException` instead of `Exception` to avoid this. – Mark Amery Jan 05 '20 at 18:59
11

if put 'http_errors' => false in guzzle request options, then it would stop throw exception while get 4xx or 5xx error, like this: $client->get(url, ['http_errors' => false]). then you parse the response, not matter it's ok or error, it would be in the response for more info

aldora372
  • 192
  • 2
  • 12
  • This question is about handle errors not asking for stoping error exceptions –  Feb 08 '20 at 11:02
  • 1
    @Dlk I think you misunderstood the answer. The approach allows you to process responses manually and, for example, check the HTTP code yourself and handle those with an HTTP error code appropriately. – Leukipp Aug 12 '20 at 22:13
  • This would be an excellent answer, but you don't get a response to parse for connection errors. – EML Mar 16 '23 at 13:23
11

The question was:

I would like to handle errors from Guzzle when the server returns 4xx and 5xx status codes

While you can handle 4xx or 5xx status codes specifically, in practice it makes sense to catch all exceptions and handle the results accordingly.

The question is also, whether you just want to handle the errors or get the body? I think in most cases it would be sufficient to handle the errors and not get the message body or only get the body in the case of a non-error.

I would look at the documentation to check how your version of Guzzle handles it because this may change: https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions

Also see this page in the official documentation on "Working with errors", which states:

Requests that receive a 4xx or 5xx response will throw a Guzzle\Http\Exception\BadResponseException. More specifically, 4xx errors throw a Guzzle\Http\Exception\ClientErrorResponseException, and 5xx errors throw a Guzzle\Http\Exception\ServerErrorResponseException. You can catch the specific exceptions or just catch the BadResponseException to deal with either type of error.

Guzzle 7 (from the docs):

. \RuntimeException
└── TransferException (implements GuzzleException)
    └── RequestException
        ├── BadResponseException
        │   ├── ServerException
        │   └── ClientException
        ├── ConnectException
        └── TooManyRedirectsException

So, your code might look like this:

use GuzzleHttp\Exception\TooManyRedirectsException;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use GuzzleHttp\Exception\ConnectException;

// ...

try {
    $response = $client->request('GET', $url);
    if ($response->getStatusCode() >= 300) {
       // is HTTP status code (for non-exceptions) 
       $statusCode = $response->getStatusCode();
       // handle error 
    } else {
      // is valid URL
    }
            
} catch (TooManyRedirectsException $e) {
    // handle too many redirects
} catch (ClientException | ServerException $e) {
    // ClientException is thrown for 400 level errors if the http_errors request option is set to true.
    // ServerException is thrown for 500 level errors if the http_errors request option is set to true.
    if ($e->hasResponse()) {
       // is HTTP status code, e.g. 500 
       $statusCode = $e->getResponse()->getStatusCode();
    }
} catch (ConnectException $e) {
    // ConnectException is thrown in the event of a networking error.
    // This may be an error reported by lowlevel functionality 
    // (e.g.  cURL error)
    $handlerContext = $e->getHandlerContext();
    if ($handlerContext['errno'] ?? 0) {
       // this is the lowlevel error code, not the HTTP status code!!!
       // for example 6 for "Couldn't resolve host" (for libcurl)
       $errno = (int)($handlerContext['errno']);
    } 
    // get a description of the error
    $errorMessage = $handlerContext['error'] ?? $e->getMessage();
         
} catch (\Exception $e) {
    // fallback, in case of other exception
}

If you really need the body, you can retrieve it as usual:

https://docs.guzzlephp.org/en/stable/quickstart.html#using-responses

$body = $response->getBody();

Under the hood, by default cURL is used or PHP stream wrapper, see Guzzle docs, so the error codes and messages may reflect that:

Guzzle no longer requires cURL in order to send HTTP requests. Guzzle will use the PHP stream wrapper to send HTTP requests if cURL is not installed. Alternatively, you can provide your own HTTP handler used to send requests. Keep in mind that cURL is still required for sending concurrent requests.


Sybille Peters
  • 2,832
  • 1
  • 27
  • 49
6

The exception should be an instance of BadResponseException which has a getResponse method. You can then cast the response body to a string. Reference: https://github.com/guzzle/guzzle/issues/1105

use GuzzleHttp\Exception\BadResponseException;

$url = $this->baseUrl . "subnet?section=$section";
try {
    $response = $this->client->get($url);
    $subnets = json_decode($response->getBody(), true);
    return $subnets['subnets'];
} catch (BadResponseException $ex) {
    $response = $ex->getResponse();
    $jsonBody = (string) $response->getBody();
    // do something with json string...
}
5

None of the above responses are working for error that has no body but still has some describing text. For me, it was SSL certificate problem: unable to get local issuer certificate error. So I looked right into the code, because doc does't really say much, and did this (in Guzzle 7.1):

try {
    // call here
} catch (\GuzzleHttp\Exception\RequestException $e) {
    if ($e->hasResponse()) {
        $response = $e->getResponse();
        // message is in $response->getReasonPhrase()
    } else {
        $response = $e->getHandlerContext();
        if (isset($response['error'])) {
            // message is in $response['error']
        } else {
            // Unknown error occured!
        }
    }
}
Erik
  • 303
  • 1
  • 3
  • 12
3

For me, this worked with Guzzle inside a Laravel package:

try {
    $response = $this->client->get($url);
}
catch(\Exception $e) {
    $error = $e->getResponse();
    dd($error);
}
shasi kanth
  • 6,987
  • 24
  • 106
  • 158
0

You can get the whole error message (not truncated). Please try the following code:

try {
    ...
} catch (GuzzleHttp\Exception\RequestException $e) {
    $error = \GuzzleHttp\Psr7\str($e->getResponse());
    print_r($error);
}
Rafael Xavier
  • 956
  • 13
  • 13