I'm trying to modify my App\Exceptions\Handler to pass the request (and therefore current URL) through to all exceptions. For this reason I need the lowest-level exception class I can get hold of to type-hint to the ->renderable() method.
Laravel/Symfony's HttpException works but only for HTTP errors, leaving out all non-HTTP exceptions. PHP's Exception class works when using getCode() instead of getStatusCode(), but always returns a "0" for both HTTP errors and exceptions. Is there another low-level exception class that will work for my purposes, or otherwise any other way to accomplish what I'm trying to do here?
public function register()
{
$this->renderable(function (Exception $exception, $request) {
$url = $request->fullUrl();
$status = $exception->getCode();
Log::warning("Error $status when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.$status", [
"exception" => $exception
],
$status
);
});
}
}
For what it's worth, I'm using the following web routes to trigger exceptions and HTTP errors for testing:
if (app()->environment('local')) {
Route::get("/exception", function (){
throw new JsonException; // chosen because it's one of the few Laravel exceptions
// that doesn't seem to automatically resolve to a HTTP error
});
}
if (app()->environment('local')) {
Route::get("/fail/{status}", function ($status){
abort($status);
});
}
As requested, this is what I have in my Handler. I use some custom logging, and I want to make sure I grab the right code when it's an HTTP error.
public function report(Throwable $e)
{
$code = match (get_class($e)) {
'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' => 404,
\HttpException::class => $e->getStatusCode(),
default => 'No Code',
};
// more stuff here
}
You can use $e->getCode() for your default as well
You can throw your JsonException and abort like so with a given code and the handler should grab it from getCode like so
// in your controller
throw new \JsonException('Something went wrong', 500);
// or
abort(500, 'Something went wrong')
// in your handler
$status = $e->getCode(); // 500
$message = $e->getMessage(); // "Something went wrong"
That said it's better to keep them as semantically separate as possible in my opinion, and let the handler do the handling depending on what it receives.
I finally managed to figure this out in the end. It's probably not the cleanest solution, but it works perfectly for my needs.
It works by inspecting each instance of the Exception class and using PHP's instanceof() to check whether it's a HTTP exception or not. If it is, it gets logged with the request URL and returns a view with a status code. If it's a generic non-HTTP exception, it gets logged with the request URL and returns another view with no status code (or you can keep the default exception behaviour by removing the return block, which renders a blank screen in production).
public function register()
{
$this->renderable(function (Exception $exception, $request) {
$url = $request->fullUrl();
if ($exception instanceof HttpException) {
$status = $exception->getStatusCode();
Log::warning("Error $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.error", [
"exception" => $exception,
"status" => $status
],
$status
);
} else {
$status = $exception->getCode();
Log::warning("Exception $status occurred when trying to visit $url. Received the following message: " . $exception->getMessage());
return response()->view("errors.exception", [
"exception" => $exception,
"status" => $status
]);
}
});
// Optionally suppress all Laravel's default logging for exceptions, so only your own logs go to the logfile
$this->reportable(function (Exception $e) {
})->stop();
}
I want to make a request to retrieve user info from OAUTH server with Symfony HttpClient but I can't fetch fetch the response directly when encountering an error response, because the client throws an exception.
My UserProvider:
public function loadUserByUsername($username)
{
try {
$response = $this->httpClient->request(
'GET',
$this->baseUrl . '/userinfo',
[
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/json'
],
'auth_bearer' => $username
]
);
var_dump($response->toArray());die;
} catch (\Exception $e) {
var_dump($e->getMessage());die;
throw new UsernameNotFoundException($e->getMessage());
}
}
When I call $response->toArray() or $response->getContent(), an exception is thrown and I get the following error message on Postman
<pre class='xdebug-var-dump' dir='ltr'>
<small>/var/www/html/backend/src/Security/Provider/KeycloakUserProvider.php:50:</small><small>string</small> <font color='#cc0000'>'HTTP/2 401 returned for "https://oauth_server/userinfo".'</font> <i>(length=127)</i>
</pre>
An example of the response received on the browser:
{"error":"invalid_request","error_description":"Token not provided"}
Why can't I access the response directly by calling $response->toArray()?
Whenever the status code of the response is not "good" (e.g. in the 300-599 range), the response is considered exceptional, and you are supposed to handle it.
This is clearly documented here:
When the HTTP status code of the response is in the 300-599 range (i.e. 3xx, 4xx or 5xx), the getHeaders(), getContent() and toArray() methods throw an appropriate exception, all of which implement the HttpExceptionInterface.
To opt-out from this exception and deal with 300-599 status codes on your own, pass false as the optional argument to every call of those methods, e.g. $response->getHeaders(false);.
If you do not want the client to throw an exception when encountering a non-OK response, you need to pass false to getContent() or toArray().
E.g.
$rawResponse = $response->getContents(false);
$arrayResponse = $response->toArray(false);
There is nothing wrong in handling the exception explicitly in your code, and will make your application better express the conditions it encounters. A "non-good" should be treated as anomalous, and handled accordingly.
I have made my custom Exception defined in $this->exception($response['error']); .
So in my service method:
$response = $this->request('POST', $endpoint, $options);
if (array_key_exists('error', $response)) {
$this->exception($response['error']);
}
Which is returning error message if key error exists in response.
And in my controler:
$response = $this->serviceMethod();
if (!empty($response)) {
return $this->apiError(['error' => 'VIOLATION']);
}
If service method pass, response will return empty array which is fine.
Problem occurs that if service method catches error in if() statement and I am dumping controller method response it never gets to that part. It finishes with that custom exception.
How can I accomplish to pass response in both ways so I can handle in in my apiError() method which is made to write error from response.
It's like:
protected function apiError(array $errors = [], $message = ''): void
{
throw new ApiException($errors, $message)
}
Symfony 3.4
PHP 7.4
I am trying to map
Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException
to another response code. Basically like Laravel did: https://github.com/laravel/framework/pull/29000/files
Currently it creates a fatal error and 500 response:
PHP Fatal error: Uncaught Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException: ...
I want to return a 404 as response.
If you want to do like in Laravel, you should take a look at Event Listener.
public function onKernelException(ExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getThrowable();
// Customize your response object to display the exception details
$response = new Response();
if ($exception instanceof SuspiciousOperationException) {
$response->setStatusCode(404);
} else {
$response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR);
}
// sends the modified response object to the event
$event->setResponse($response);
}
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?
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();
}
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.
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);
}
}
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.
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
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.
Guzzle exceptions
libcurl error codes
HTTP status codes
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...
}
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!
}
}
}
For me, this worked with Guzzle inside a Laravel package:
try {
$response = $this->client->get($url);
}
catch(\Exception $e) {
$error = $e->getResponse();
dd($error);
}
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);
}