passing exception to controller without try/catch - php

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

Related

Low-level exception class to target all exceptions

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

How to remove default Laravel Exception message from Custom Exception message

I have this get organisations method in one project that talks to a central api project that handles all data like so:
public function searchOrganisations()
{
try {
return $this->client->request(.....);
} catch (Exception $ex) {
}
}
Within the api project a method is then hit and if a certain time frame criteria is hit I throw a custom exception like so:
public function searchOrganisations($searchRequest)
{
$experianCutOff = Carbon::createFromFormat('H:i:s', '06:00:00');
$now = Carbon::now()->setTime(02, 0, 0);
if (!$now->lt($experianCutOff)) {
return $data
} else {
throw new ExperianServiceException();
}
}
My custom exeption is as follows:
class ExperianServiceException extends Exception
{
public function render() {
return response()->json([
'message' => 'The Experian Service is currently unavailable, please try again at 0600 GMT'
], 503);
}
}
This works as expected and I catch the exception in the first method listed, I can access the status 503 and can see the message, however the message property of the exception always comes back in this format:
Server error: `POST http://docker.../search-organisations` resulted in a `503 Service Unavailable` response:
{"message":"The Experian Service is currently unavailable, please try again at 0600 GMT"}
It seems as though my supplied custom message has been concatenated with the standard Laravel Exception message (which I dont want). How can I make sure my message only contains what I supplied in my custom exception?
You could have your Exception implement Illuminate\Contracts\Support\Responsable and define the toResponse method to return that response. When you only have the render method the framework is still potentially going to do things to prepare the response. If it is Responsable it calls toResponse on it and returns that directly.
This causes a raw response from toResponse of your Exception to be returned without passing through any other parts of the Handler to be prepared in any way.
Maybe:
return Response::json(array(
'code' => 404,
'message' => $message
), 404);

skip symfony2 kernel exception

I am using symfony2.8 and
We have a KernelExceptionService and I want to skip it if there is any Exception like 500,400 or any and get back to service and continue the work.
The reason
We are hitting multiple url to fetch the data and if there is any exception occurred whole processing get stopped.
public function onKernelException(GetResponseForExceptionEvent $event) {
$exception = $event->getException();
$response = new JsonResponse;
$request = $event->getRequest();
if ($exception instanceof InvalidConfigurationException) {
//500 case
$responseData = return [
'code' => Response::HTTP_NOT_FOUND,
'message' => $exception->getMessage()
];
} else {
// same as aobve if with difference code
}
//Prepare the response
$response->setData($responseData);
$response->setStatusCode($statusCode);
$event->setResponse($response);
}
Just wrap the particular code with a try catch block?
That way your exception listener will never trigger and you can handle the exception differently in that specific part of code.

Symfony and PHPUnit: Exception thrown but not intercepted by setExpectedException

I wrote a test for a controller that saves in the database some data passed by a form.
I wrote the following test method to be sure that if the form is empty an exception is thrown:
public function testRegisterNewMerchantExceptionNoDataSubmitted()
{
$client = static::createClient();
$crawler = $client->request('GET', '/getstarted');
$form = $crawler->selectButton('getStarted[submit]')->form();
$form['getStarted[email]'] = '';
$this->setExpectedException('DomainException');
$client->submit($form);
$this->assertEquals(500, $client->getResponse()->getStatusCode());
//dump($client->getResponse());die;
}
The method i'm testing is the following:
public function endAction(Request $request)
{
$form = $this->createForm(new GetStartedType());
$form->handleRequest($request);
if ($form->isValid()) {
// Get data from form
$data = $form->getData();
} else {
throw new \DomainException('No data submitted.');
}
...
I'm sure that also during tests the exception is thrown because dumping the Response object the page is a 500 error reporting the exact message "No data submitted". More, the assertEquals test on the status code is successful, so there are no doubts that the exception is correctly thrown.
But the $this->setExpectedException() test doesn't intercept it and returns a fail of the test.
Any idea about why this happens?
Using $this->setExcpectedException() tells PHPUnit to expect the given exception type to be thrown from the test method, not just that an exception of that type is thrown at some point during execution.
When you throw an exception in a controller method, the Symfony controller catches
that exception and creates a 500 response. This means the exception will not be thrown from the test method, so the test fails. Your test looks reasonable otherwise, so removing $this->setExpectedException() should solve the problem and test the behavior you intended.

Laravel 5: Handle exceptions when request wants JSON

I'm doing file uploads via AJAX on Laravel 5. I've got pretty much everything working except one thing.
When I try to upload a file that is too big (Bigger than upload_max_filesize and post_max_size I get a TokenMismatchException thrown.
This is to be expected however, because I know that my input will be empty if these limits are being exceeded. Empty input, means no _token is received hence why the middleware responsible for verifying CSRF tokens is kicking up a fuss.
My issue however is not that this exception is being thrown, it is how it is being rendered. When this exception is being caught by Laravel it's spitting out the HTML for the generic Whoops page (With a load of stack tracing since I'm in debug mode).
What's the best way to handle this exception so that JSON is returned over AJAX (Or when JSON is requested) while keeping the default behaviour otherwise?
Edit: This seems to happen regardless of the exception thrown. I've just tried making a request via AJAX (Datatype: JSON) to a 'page' that doesn't exist in an attempt to get a 404 and the same thing happens - HTML is returned, nothing JSON friendly.
I'm going to take a shot at this one myself taking into account the answer given by #Wader and the comments from #Tyler Crompton:
app/Exceptions/Handler.php
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
// If the request wants JSON (AJAX doesn't always want JSON)
if ($request->wantsJson()) {
// Define the response
$response = [
'errors' => 'Sorry, something went wrong.'
];
// If the app is in debug mode
if (config('app.debug')) {
// Add the exception class name, message and stack trace to response
$response['exception'] = get_class($e); // Reflection might be better here
$response['message'] = $e->getMessage();
$response['trace'] = $e->getTrace();
}
// Default response of 400
$status = 400;
// If this exception is an instance of HttpException
if ($this->isHttpException($e)) {
// Grab the HTTP status code from the Exception
$status = $e->getStatusCode();
}
// Return a JSON response with the response array and status code
return response()->json($response, $status);
}
// Default to the parent class' implementation of handler
return parent::render($request, $e);
}
In your application you should have app/Http/Middleware/VerifyCsrfToken.php. In that file you can handle how the middleware runs. So you could check if the request is ajax and handle that how you like.
Alternativly, and probably a better solution, would be to edit the exception handler to return json. See app/exceptions/Handler.php, something like the below would be a starting place
public function render($request, Exception $e)
{
if ($request->ajax() || $request->wantsJson())
{
$json = [
'success' => false,
'error' => [
'code' => $e->getCode(),
'message' => $e->getMessage(),
],
];
return response()->json($json, 400);
}
return parent::render($request, $e);
}
Building on #Jonathon's handler render function, I would just modify the conditions to exclude ValidationException instances.
// If the request wants JSON + exception is not ValidationException
if ($request->wantsJson() && ( ! $exception instanceof ValidationException))
Laravel 5 returns validation errors in JSON already if appropriate.
The full method in App/Exceptions/Handler.php:
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
// If the request wants JSON + exception is not ValidationException
if ($request->wantsJson() && ( ! $exception instanceof ValidationException))
{
// Define the response
$response = [
'errors' => 'Sorry, something went wrong.'
];
// If the app is in debug mode
if (config('app.debug'))
{
// Add the exception class name, message and stack trace to response
$response['exception'] = get_class($exception); // Reflection might be better here
$response['message'] = $exception->getMessage();
$response['trace'] = $exception->getTrace();
}
// Default response of 400
$status = 400;
// If this exception is an instance of HttpException
if ($this->isHttpException($exception))
{
// Grab the HTTP status code from the Exception
$status = $exception->getCode();
}
// Return a JSON response with the response array and status code
return response()->json($response, $status);
}
return parent::render($request, $exception);
}
I have altered several implementations found here to work on Laravel 5.3.
The main difference is that mine will return the correct HTTP status texts
In your render() function in app\Exceptions\Handler.php add this snippet to the top:
if ($request->wantsJson()) {
return $this->renderExceptionAsJson($request, $exception);
}
Contents of renderExceptionAsJson:
/**
* Render an exception into a JSON response
*
* #param $request
* #param Exception $exception
* #return SymfonyResponse
*/
protected function renderExceptionAsJson($request, Exception $exception)
{
// Currently converts AuthorizationException to 403 HttpException
// and ModelNotFoundException to 404 NotFoundHttpException
$exception = $this->prepareException($exception);
// Default response
$response = [
'error' => 'Sorry, something went wrong.'
];
// Add debug info if app is in debug mode
if (config('app.debug')) {
// Add the exception class name, message and stack trace to response
$response['exception'] = get_class($exception); // Reflection might be better here
$response['message'] = $exception->getMessage();
$response['trace'] = $exception->getTrace();
}
$status = 400;
// Build correct status codes and status texts
switch ($exception) {
case $exception instanceof ValidationException:
return $this->convertValidationExceptionToResponse($exception, $request);
case $exception instanceof AuthenticationException:
$status = 401;
$response['error'] = Response::$statusTexts[$status];
break;
case $this->isHttpException($exception):
$status = $exception->getStatusCode();
$response['error'] = Response::$statusTexts[$status];
break;
default:
break;
}
return response()->json($response, $status);
}
In Laravel 8.x, you could do
app/Http/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
if ($request->wantsJson()) {
return parent::prepareJsonResponse($request, $exception);
}
return parent::render($request, $exception);
}
and if you want to always return JSON for all exceptions, just always call parent::prepareJsonResponse and remove parent::render.
When the JSON is rendered with APP_DEBUG=true, you will get a full error report and stack trace. When APP_DEBUG=false, you will get a generic message so that you do not accidentally expose application details.
Using #Jonathon's code, here's a quick fix for Laravel/Lumen 5.3 :)
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $e
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
{
// If the request wants JSON (AJAX doesn't always want JSON)
if ($request->wantsJson())
{
// Define the response
$response = [
'errors' => 'Sorry, something went wrong.'
];
// If the app is in debug mode
if (config('app.debug'))
{
// Add the exception class name, message and stack trace to response
$response['exception'] = get_class($e); // Reflection might be better here
$response['message'] = $e->getMessage();
$response['trace'] = $e->getTrace();
}
// Default response of 400
$status = 400;
// If this exception is an instance of HttpException
if ($e instanceof HttpException)
{
// Grab the HTTP status code from the Exception
$status = $e->getStatusCode();
}
// Return a JSON response with the response array and status code
return response()->json($response, $status);
}
// Default to the parent class' implementation of handler
return parent::render($request, $e);
}
My way:
// App\Exceptions\Handler.php
public function render($request, Throwable $e) {
if($request->is('api/*')) {
// Setting Accept header to 'application/json', the parent::render
// automatically transform your request to json format.
$request->headers->set('Accept', 'application/json');
}
return parent::render($request, $e);
}
you can easily catch err.response like this:
axios.post().then().catch(function(err){
console.log(err.response); //is what you want
};

Categories