179

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?

1

10 Answers 10

343

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();
}
6
  • 16
    For me $response->getBody()->getContents() would return an empty string. I then stumbled across this in the docs: \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, 2016 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, 2016 at 23:55
  • 51
    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, 2017 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, the body can be used by casting it to string. $stringBody = (string) $clientException->getResponse()->getBody();
    – Anthony
    Dec 12, 2017 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, 2019 at 1:52
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.

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);
  }

}
4
  • is JsonResponse a class from Guzzle? May 29, 2020 at 6:06
  • JsonResponse comes from Symfony
    – chap
    Jun 1, 2020 at 4:50
  • Is this answer [still] correct? Shouldn't you catch TransferException if you want both ClientException and ConnectException? See the exception tree [docs.guzzlephp.org/en/stable/quickstart.html#exceptions] here.
    – EML
    Mar 16, 2023 at 13:20
  • You are correct. At the time of writing TransferException will catch ConnectException and all subclasses of RequestException
    – chap
    Mar 5 at 6:07
37

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.

3
  • 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 1, 2019 at 8:22
  • 1
    @pwaring, true. Exactly as the Guzzle exceptions docs say. Updated the answer. Thank you. Oct 1, 2019 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 5, 2020 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

3
  • This question is about handle errors not asking for stoping error exceptions
    – user1805543
    Feb 8, 2020 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, 2020 at 22:13
  • This would be an excellent answer, but you don't get a response to parse for connection errors.
    – EML
    Mar 16, 2023 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.


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!
        }
    }
}
5

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...
}
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);
}
1
  • this catches every exception. not just guzzle exceptions. wouldn't recommend this Jan 16 at 18:22
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);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.