47

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.

Jonathon
  • 15,873
  • 11
  • 73
  • 92
  • So to clarify, debug mode and production mode should produce the same result? – Tyler Crompton Mar 09 '15 at 15:07
  • Via AJAX, production should produce a response indicating that there was a token mismatch exception without any more information. Debug mode, ideally would return a bunch of extra detail about the exception but I could live with it being just the same. – Jonathon Mar 09 '15 at 15:12

8 Answers8

106

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);
}
Jonathon
  • 15,873
  • 11
  • 73
  • 92
  • 2
    You could shorten the lines setting status code to: `$status = method_exists($e, 'getStatusCode') ? $e->getStatusCode() : 400;` – Justin Mar 04 '16 at 16:20
  • This works well except when it's a validation exception it does not return the validation errors. – Youssef Lourayad Aug 17 '16 at 09:56
  • @YoussefLourayad When using Laravel's validation functionality, validation errors are returned as JSON over AJAX anyway (With a 422 HTTP status code). However, if you really wanted to you could adjust the above to check the type of the exception and add validation errors to the response. `if ($e instanceof ValidationException) {` – Jonathon Aug 17 '16 at 11:36
  • I remember trying that with no success, I'll try again. Thanks – Youssef Lourayad Aug 17 '16 at 13:05
  • No problem, Laravel usually handles validation errors itself though. Take a look at the `ValidatesRequest` trait, in particular the `buildFailedValidationResponse` method. – Jonathon Aug 17 '16 at 14:17
  • I wanted to add `success: false` to the validation response. However `ValidationException` wasn't what the Handler saw an instance of, it was `HttpResponseException`. – James Wagoner Nov 04 '16 at 05:14
  • You'll want to add `Illuminate\Http\JsonResponse` to the @return field in your PHPDoc – Symphony0084 Jan 28 '20 at 03:08
  • Add Headers: $headers = []; if ($this->isHttpException($exception)) { ... $headers = $exception->getHeaders(); } ....return response()->json($response, $status, $headers); – Solo.dmitry Apr 23 '20 at 08:26
14

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);
}
Wader
  • 9,427
  • 1
  • 34
  • 38
  • Why assume that if the request is in JSON that the response should be in JSON? – Tyler Crompton Mar 09 '15 at 15:08
  • Cheers for your reply. I managed to get something like that working myself within the `Handler.php` file. I also added the check of the type of exception by doing `if ($e instanceof TokenMismatchException ....)` – Jonathon Mar 09 '15 at 15:15
  • This should probably return a 500, not a 400. Your controller should validate input and throw a 400 if the input is not sensible, but the exception handler is for cases where some sort of exceptional (developer?) error has occurred. – Daniel Buckmaster Jan 30 '17 at 00:40
9

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);
}
pubudeux
  • 178
  • 1
  • 9
  • I believe the second-to-last line should read $status = $exception->getStatusCode(), the getCode() method returns 0 which is not accepted as an HTTP return code. Maybe this is something that happens only in newer Laravel versions. I am using 5.6. – Wilbo Baggins Apr 08 '18 at 22:21
8

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);
}
Joe Alamo
  • 133
  • 1
  • 7
5

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.

Graham S.
  • 1,480
  • 1
  • 20
  • 28
2

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);
}
juliosch
  • 76
  • 1
  • 4
1

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);
    }
Pablo Papalardo
  • 1,224
  • 11
  • 9
-1

you can easily catch err.response like this:

axios.post().then().catch(function(err){


 console.log(err.response);  //is what you want

};
Mostafa
  • 95
  • 1
  • 10