Anyone know what is the best way to handle errors in Laravel, there is any rules or something to follow ?
Currently i'm doing this :
public function store(Request $request)
{
$plate = Plate::create($request->all());
if ($plate) {
return $this->response($this->plateTransformer->transform($plate));
} else {
// Error handling ?
// Error 400 bad request
$this->setStatusCode(400);
return $this->responseWithError("Store failed.");
}
}
And the setStatusCode and responseWithError come from the father of my controller :
public function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
public function responseWithError ($message )
{
return $this->response([
'error' => [
'message' => $message,
'status_code' => $this->getStatusCode()
]
]);
}
But is this a good way to handle the API errors, i see some different way to handle errors on the web, what is the best ?
Thanks.
Try this, i have used it in my project (app/Exceptions/Handler.php)
public function render($request, Exception $exception)
{
if ($request->wantsJson()) { //add Accept: application/json in request
return $this->handleApiException($request, $exception);
} else {
$retval = parent::render($request, $exception);
}
return $retval;
}
Now Handle Api exception
private function handleApiException($request, Exception $exception)
{
$exception = $this->prepareException($exception);
if ($exception instanceof \Illuminate\Http\Exception\HttpResponseException) {
$exception = $exception->getResponse();
}
if ($exception instanceof \Illuminate\Auth\AuthenticationException) {
$exception = $this->unauthenticated($request, $exception);
}
if ($exception instanceof \Illuminate\Validation\ValidationException) {
$exception = $this->convertValidationExceptionToResponse($exception, $request);
}
return $this->customApiResponse($exception);
}
After that custom Api handler response
private function customApiResponse($exception)
{
if (method_exists($exception, 'getStatusCode')) {
$statusCode = $exception->getStatusCode();
} else {
$statusCode = 500;
}
$response = [];
switch ($statusCode) {
case 401:
$response['message'] = 'Unauthorized';
break;
case 403:
$response['message'] = 'Forbidden';
break;
case 404:
$response['message'] = 'Not Found';
break;
case 405:
$response['message'] = 'Method Not Allowed';
break;
case 422:
$response['message'] = $exception->original['message'];
$response['errors'] = $exception->original['errors'];
break;
default:
$response['message'] = ($statusCode == 500) ? 'Whoops, looks like something went wrong' : $exception->getMessage();
break;
}
if (config('app.debug')) {
$response['trace'] = $exception->getTrace();
$response['code'] = $exception->getCode();
}
$response['status'] = $statusCode;
return response()->json($response, $statusCode);
}
Always add Accept: application/json in your api or json request.
Laravel is already able to manage json responses by default.
Withouth customizing the render method in app\Handler.php you can simply throw a Symfony\Component\HttpKernel\Exception\HttpException, the default handler will recognize if the request header contains Accept: application/json and will print a json error message accordingly.
If debug mode is enabled it will output the stacktrace in json format too.
Here is a quick example:
<?php
...
use Symfony\Component\HttpKernel\Exception\HttpException;
class ApiController
{
public function myAction(Request $request)
{
try {
// My code...
} catch (\Exception $e) {
throw new HttpException(500, $e->getMessage());
}
return $myObject;
}
}
Here is laravel response with debug off
{
"message": "My custom error"
}
And here is the response with debug on
{
"message": "My custom error",
"exception": "Symfony\\Component\\HttpKernel\\Exception\\HttpException",
"file": "D:\\www\\myproject\\app\\Http\\Controllers\\ApiController.php",
"line": 24,
"trace": [
{
"file": "D:\\www\\myproject\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\ControllerDispatcher.php",
"line": 48,
"function": "myAction",
"class": "App\\Http\\Controllers\\ApiController",
"type": "->"
},
{
"file": "D:\\www\\myproject\\vendor\\laravel\\framework\\src\\Illuminate\\Routing\\Route.php",
"line": 212,
"function": "dispatch",
"class": "Illuminate\\Routing\\ControllerDispatcher",
"type": "->"
},
...
]
}
Using HttpException the call will return the http status code of your choice (in this case internal server error 500)
In my opinion I'd keep it simple.
Return a response with the HTTP error code and a custom message.
return response()->json(['error' => 'You need to add a card first'], 500);
Or if you want to throw a caught error you could do :
try {
// some code
} catch (Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
You can even use this for sending successful responses:
return response()->json(['activeSubscription' => $this->getActiveSubscription()], 200);
This way no matter which service consumes your API it can expect to receive the same responses for the same requests.
You can also see how flexible you can make it by passing in the HTTP status code.
If you are using Laravel 8+, you can do it simply by adding these lines in Exception/Handler.php on register() method
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
For me, the best way is to use specific Exception for API response.
If you use Laravel version > 5.5, you can create your own exception with report() and render() methods. Use command:
php artisan make:exception AjaxResponseException
It will create AjaxResponseException.php at: app/Exceptions/
After that fill it with your logic. For example:
/**
* Report the exception.
*
* #return void
*/
public function report()
{
\Debugbar::log($this->message);
}
/**
* Render the exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #return JsonResponse|Response
*/
public function render($request)
{
return response()->json(['error' => $this->message], $this->code);
}
Now, you can use it in your ...Controller with try/catch functionality.
For example in your way:
public function store(Request $request)
{
try{
$plate = Plate::create($request->all());
if ($plate) {
return $this->response($this->plateTransformer->transform($plate));
}
throw new AjaxResponseException("Plate wasn't created!", 404);
}catch (AjaxResponseException $e) {
throw new AjaxResponseException($e->getMessage(), $e->getCode());
}
}
That's enough to make your code more easier for reading, pretty and useful.
Best regards!
For Laravel 8+ in file App\Exceptions\Hander.php inside method register() paste this code:
$this->renderable(function (Throwable $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => $e->getMessage(),
'code' => $e->getCode(),
], 404);
}
});
I think it would be better to modify existing behaviour implemented in app/Exceptions/Handler.php than overriding it.
You can modify JSONResponse returned by parent::render($request, $exception); and add/remove data.
Example implementation:
app/Exceptions/Handler.php
use Illuminate\Support\Arr;
// ... existing code
public function render($request, Exception $exception)
{
if ($request->is('api/*')) {
$jsonResponse = parent::render($request, $exception);
return $this->processApiException($jsonResponse);
}
return parent::render($request, $exception);
}
protected function processApiException($originalResponse)
{
if($originalResponse instanceof JsonResponse){
$data = $originalResponse->getData(true);
$data['status'] = $originalResponse->getStatusCode();
$data['errors'] = [Arr::get($data, 'exception', 'Something went wrong!')];
$data['message'] = Arr::get($data, 'message', '');
$originalResponse->setData($data);
}
return $originalResponse;
}
Well, all answers are ok right now, but also they are using old ways.
After Laravel 8, you can simply change your response in register() method by introducing your exception class as renderable:
<?php
namespace Your\Namespace;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
class Handler extends ExceptionHandler
{
/**
* Register the exception handling callbacks for the application.
*
* #return void
*/
public function register()
{
$this->renderable(function (NotFoundHttpException $e, $request) {
if ($request->is('api/*')) {
return response()->json([
'message' => 'Record not found.'
], 404);
}
});
}
}
Using some code from #RKJ best answer I have handled the errors in this way:
Open "Illuminate\Foundation\Exceptions\Handler" class and search for a method named "convertExceptionToArray". This method converts the HTTP exception into an array to be shown as a response. In this method, I have just tweaked a small piece of code that will not affect loose coupling.
So replace convertExceptionToArray method with this one
protected function convertExceptionToArray(Exception $e, $response=false)
{
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(function ($trace) {
return Arr::except($trace, ['args']);
})->all(),
] : [
'message' => $this->isHttpException($e) ? ($response ? $response['message']: $e->getMessage()) : 'Server Error',
];
}
Now navigate to the App\Exceptions\Handler class and paste the below code just above the render method:
public function convertExceptionToArray(Exception $e, $response=false){
if(!config('app.debug')){
$statusCode=$e->getStatusCode();
switch ($statusCode) {
case 401:
$response['message'] = 'Unauthorized';
break;
case 403:
$response['message'] = 'Forbidden';
break;
case 404:
$response['message'] = 'Resource Not Found';
break;
case 405:
$response['message'] = 'Method Not Allowed';
break;
case 422:
$response['message'] = 'Request unable to be processed';
break;
default:
$response['message'] = ($statusCode == 500) ? 'Whoops, looks like something went wrong' : $e->getMessage();
break;
}
}
return parent::convertExceptionToArray($e,$response);
}
Basically, we overrided convertExceptionToArray method, prepared the response message, and called the parent method by passing the response as an argument.
Note: This solution will not work for Authentication/Validation errors but most of the time these both errors are well managed by Laravel with proper human-readable response messages.
In your handler.php This should work for handling 404 Exception.
public function render($request, Throwable $exception ){
if ($exception instanceof ModelNotFoundException) {
return response()->json([
'error' => 'Data not found'
], 404);
}
return parent::render($request, $exception);
}
You don't have to do anything special. Illuminate\Foundation\Exceptions\Handler handles everything for you. When you pass Accept: Application/json header it will return json error response. if debug mode is on you will get exception class, line number, file, trace if debug is off you will get the error message. You can override convertExceptionToArray. Look at the default implementation.
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(function ($trace) {
return Arr::except($trace, ['args']);
})->all(),
] : [
'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
];
As #shahib-khan said,
this happens in debug mode and is handled by Laravel in production mode.
you can see base method code in
\Illuminate\Foundation\Exceptions\Handler::convertExceptionToArray
protected function convertExceptionToArray(Throwable $e)
{
return config('app.debug') ? [
'message' => $e->getMessage(),
'exception' => get_class($e),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => collect($e->getTrace())->map(fn ($trace) => Arr::except($trace, ['args']))->all(),
] : [
'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
];
}
Therefore, I overrode the the convertExceptionToArray function in app/Exceptions/Handler
of course, still in debug mode, if the exception is thrown, you can track it in the Laravel.log.
protected function convertExceptionToArray(Throwable $e)
{
return [
'message' => $this->isHttpException($e) ? $e->getMessage() : 'Server Error',
];
}
Add header to your API endpoint. which works for me. it will handle the error request properly.
Accept: application/json
I have a three different forms that submit to the same controller in Laravel. Each form has its own validation rules stored in a Request. Here's an example of my code:
public function store($id, $type, Request $request)
{
switch ($type) {
case 'daily':
$this->monthly($id, $type, $request);
break;
case 'monthly':
$this->monthly($id, $type, $request);
break;
case 'yearly':
$this->yearly($id, $type, $request);
}
return redirect(route('x.show', $id));
}
private function monthly($id, $type, MonthlyFormRequest $request)
{
//store form
}
However, this doesn't work and throwns an instance error since Request isn't the same type as MonthlyFormRequest in the monthly method. Is there a way to cast the Request to MonthlyFormRequest or is there some other way to do it? I prefer to avoid declaring validation rules in the controller itself. What would be the best way to get a uniform Request type request in the store method and then use MonthlyFormRequest?
You could pass the type trough a request parameter and move the switch case to your request and preform the check there as such:
In your request:
public function rules()
{
switch($this->type){
case 'dailty':
return [
'field': 'required'
];
break;
case 'monthly':
return [
'field': 'required'
];
break;
case 'yearly':
return [
'field': 'required'
];
break;
}
}
In your Controller:
public function store($id, YourCustomRequest $request)
{
return redirect(route('x.show', $id));
}
I try this code to set custom validation messages, but with no effect -
class TestController extends Controller
{
public function submit(Request $request)
{
$this->validate($request,
[
'items' => 'required'
],
[
'items.required' => 'test test'
]
);
}
}
But on error I got this response -
{
"error": "The given data failed to pass validation."
}
What wrong with this code?
UPD:
Earlier I edit App\Exceptions\Handler to put errors(in API response) in specific format -
{
"error": "123"
}
This code is reason that validation errors not shown -
public function render($request, Exception $e)
{
return response([
'error' => $e->getMessage()
], 500);
}
I update Handler::render method regarding to this purpose
public function render($request, Exception $e) {
$response = parent::render($request, $e);
if (isset($response->exception) and !empty($response->exception)) {
return response(['error' => $response->exception->getMessage()], 500);
} else {
return parent::render($request, $e);
}
}
But I think I need to improve this code.
It seems you mixed validation rules and messages.
The validate method takes 3 parameter: request, rules, messages.
Please try this:
public function submit(Request $request)
{
$rules = [
'items' => 'required',
'otheritems' => 'required',
];
$messages = [
'items.required' => 'Error: Please enter something.',
'otheritems.required' => 'Otheritems are also required',
];
$this->validate($request, $rules, $messages);
}
The latest Lumen version always gives back JSON, see documentation:
The $this->validate helper will always return JSON responses with the relevant error messages when validation fails. If you are not building a stateless API that solely sends JSON responses, you should use the full Laravel framework.
Update regarding error:
The given result by Lumen looks like that.
{"items":["Items are required"],"otheritems":["Otheritems are also required"]}
Each item that failed the validation gets an entry in your response. So your error bag, need to be a JSON array.
Custom exception render method:
public function render($request, Exception $e)
{
$response = parent::render($request, $e);
if ($response->getStatusCode() == 422) {
$renderResult = parent::render($request, $e);
$returnResult['error'] = json_decode($renderResult->content(), true);
$returnResult = json_encode($returnResult);
return new Response($returnResult, $response->getStatusCode());
} else {
return parent::render($request, $e);
}
}
My problem is that the method doesn't return a true result.
I want to test if the email of input exists in my entity or not.
Here is the controller:
public function verificationAction(Request $request)
{
if ($this->container->get('request')->isXmlHttpRequest()) {
$email=$request->request->get('email');
$em=$this->getDoctrine()->getEntityManager();
$resp= $em->getRepository("CMSiteBundle:Prospect")->findBy(array('email'=>$email));
$response =new Response(json_encode($resp));
$response->headers->set('Content-Type', 'application/json');
return $response;
}
}
You could try an old-trick. Since in Symfony Controller Actions, You must return a Response why not fake a DEAD RESPONSE like so:
<?php
class ABCController {
public function verificationAction(Request $request) {
if ($this->container->get('request')->isXmlHttpRequest()) {
$email = $request->request->get('email');
$em = $this->getDoctrine()->getEntityManager();
$resp = $em->getRepository("CMSiteBundle:Prospect")
->findBy(array('email' => $email));
//$response = new Response(json_encode($resp));
//$response->headers->set('Content-Type', 'application/json');
// THE TRICK IS THAT DIE RUNS FIRST
// THUS SENDS YOUR RESPONSE YOU THEREBY
// STOPPING THE RETURN FROM FIRING... ;-)
return die(json_encode($resp));
}
}
}
Perhaps this very Old Trick still works for you... ;-)
I'm using codeigniter, for make an api rest, with the library that provide the oficial web site.
The problem is: the file routes.php doesn't redirect well. When i put localhost/API/1 into my browser apear the 404 error.
Here my controller "Apicontroller":
public function __construct() { //constructor //no tocar
parent::__construct();
$this -> load -> model("Modelocontrolador");
}
public function index_get() { //get all the info
$datos_devueltos = $this->Modelocontrolador->getPrueba(NULL, "Usuarios");
if(!is_null($datos_devueltos)){
$this->response(array("response" => $datos_devueltos), 200);
}else{
$this->response(array("response" => "No date"), 200);
}
}
public function find_get($id){ //select where
$datos_devueltos = $this->Modelocontrolador->getPrueba($id, "Usuarios");
if($id != NULL){
if(!is_null($datos_devueltos)){
$this->response(array("response" => $datos_devueltos), 200);
}else{
$this->response(array("response" => "No date"), 200);
}
}else{
$this->response(array("response" => "No dates for search"), 200);
}
}
public function index_post() { //insert in la table
if(! $this -> post("dato")){
$this->response(array("response" => "No enought info"), 200);
}else{
$datoID = $this -> Modelocontrolador -> save($this -> post("dato"),"UsuariosJJ");
if(!is_null($datoID)){
$this->response(array("response" => $datoID), 200);
}else{
$this->response(array("response" => "No found it"), 200);
}
}
}
public function index_put($id) { //"update"
if(! $this -> post("dato") || ! $id){
$this->response(array("response" => "No ha mandado informacion correcta para el update"), 200);
}else{
$datoID = $this -> Modelocontrolador -> update("Uid",$id,$this -> post("dato"),"UsuariosJJ");
if(!is_null($datoID)){
$this->response(array("response" => "Dato actualizado"), 200);
}else{
$this->response(array("response" => "Error modify"), 200);
}
}
}
public function index_delete($id) {
if(! $id){
$this->response(array("response" => "Not enought info"), 200);
}else{
$delete = $this-> Modelocontrolador -> delete("Uid",$id,"UsuariosJJ");
}
if(!is_null($delete)){
$this->response(array("response" => "Date delete"), 200);
}else{
$this->response(array("response" => "Error delete"), 200);
}
}}
And my routes file:
$route['default_controller'] = 'Apicontroller';
$route['404_override'] = '';
$route['translate_uri_dashes'] = FALSE;
/*sub-rutas*/
/*---------*/
$route["Apicontroller"]["get"] = "Apicontroller/index"; //basico
$route["Apicontroller/(:num)"]["get"] = "Apicontroller/find"; //select
$route["Apicontroller"]["post"] = "Apicontroller/index"; //insert
$route["Apicontroller/(:num)"]["put"] = "Apicontroller/index/$1"; //update
$route["Apicontroller/(:num)"]["delete"] = "Apicontroller/index/$1"; //delete
If the browser request literally uses /API then routing needs to 'see' exactly that. Also, the route rules must be explicit with the method to be called. (Hopefully the code shown reflects the mapping you had in mind.)
/*sub-rutas*/
/*---------*/
$route["API"]["get"] = "Apicontroller/index_get"; //basico
$route["API/(:num)"]["get"] = "Apicontroller/find_get/$1"; //select
$route["API"]["post"] = "Apicontroller/index_post"; //insert
$route["API/(:num)"]["put"] = "Apicontroller/index_put/$1"; //update
$route["API/(:num)"]["delete"] = "Apicontroller/index_delete/$1"; //delete
Using the above routes I created some test code. Here are those files.
The much simplified Apicontroller.
class Apicontroller extends CI_Controller
{
function __construct()
{
parent::__construct();
}
function index_get()
{
echo "API index";
}
public function find_get($id)
{ //select where
echo "API find_get $id";
}
public function index_post()
{
echo 'API index_post';
}
public function index_put($id)
{ //"update"
echo "API put $id";
}
}
I don't believe that because your Apicontroller is extending a different Class the results would change. That may be a drastic assumption.
In order to test POST calls I used these two files.
First a Testpost.php controller
class Testpost extends CI_Controller
{
public function __construct()
{
parent::__construct();
$this->load->helper('form');
}
public function index()
{
$this->load->view("test");
}
}
The very simple view (test.php) loaded by the above.
<?php
echo form_open("API");
echo form_submit('mysubmit', 'Submit Post!');
echo form_close();
Directing the browser to localhost/testpost shows a page with a single submit button. Pressing the button results in a screen with the text "API index_post".
Sending the browser to localhost/API/3 produces a screen with the text "API find_get 3".
localhost/API produces "API index".
Now the interesting thing (not related to your problem, but interesting).
Given the default
$route['default_controller'] = 'Apicontroller';
and the route
$route["API"]["get"] = "Apicontroller/index_get";
I expected that directing the browser to the home page localhost would produce "API index". But it doesn't. It results in a 404. Due to that behavior it might be wise to be more explicit with default_controller
$route['default_controller'] = 'Apicontroller/index_get';
Or add an index() function to Apicontroller that calls $this->index_get().
I did not test PUT or DELETE as my server isn't setup to handle them. But as GET and POST seem to function, in a righteous world, they will work.
seems like you are using PHil's REST_Controller library with CI 2.x, correct ?
If so, I would recommend you to use what I like to call an "index gateway" because you can't do per-Method routing with CI2:
class Apicontroller extends REST_Controller
{
function index_gateway_get($id){
$this->get_get($id);
}
function index_gateway_put($id){
$this->put_put($id);
}
// This is not a "gateway" method because POST doesn't require an ID
function index_post(){
$this->post_post();
}
function get_get($id = null){
if(!isset($id)){
// Get all rows
}else{
// Get specific row
}
}
function put_put($id = null){
if(!isset($id)){
// a PUT withtout an ID is a POST
$this->post_post();
}else{
// PUT method
}
}
function post_post(){
// POST method
}
}
The routing to make this work is really easy:
$route["API/(:num)"] = "Apicontroller/index_gateway/$1";
That's all you need. Phil's REST Library will redirect to the correct index_gateway_HTTPMETHOD depending on which method is used.
Each index_gateway_HTTPMETHOD will then redirect to the correct method.
As far as I know, this trick is the only way to have CI2 use a single /API/ entry point that works for all HTTP Methods.