Failed validation returns default error message even though custom messages supplied
Asked Answered
B

5

10

I'm not getting the response I expect.

This is the controller code for a Location web-service request:

<?php
namespace App\Http\Controllers;

use App\Location;
use Illuminate\Http\Request;


class LocationController extends Controller
{


    /**
     * Action method to add a location with the supplied Data
     * 
     * @param \Illuminate\Http\Request $p_oRequest Request
     * 
     * @return JSON
     */
    public function add(Request $p_oRequest)
    {

        try {

            $p_oRequest->validate(
                array(
                    'name' => 'required|alpha_num',
                    'user_id' => 'required|integer',
                ),
                array(
                    'name.required' => 'Name is required',
                    'name.string' => 'Name must be alphanumeric',
                    'user_id.required' => 'Curator User Id is required',
                    'user_id.required' => 'Curator User Id must be an integer',
                )
            );

        } catch (\Exception $ex) {

            $arrResponse = array(
                'result' => 0,
                'reason' => $ex->getMessage(),
                'data' => array(),
                'statusCode' => 404
            );

        } finally {

            return response()->json($arrResponse);

        }

    }

}

The request is http://mydomain/index.php/api/v1/location/add?name=@!^

The response reason I expect is: { "result": 0, "reason": "Name must be alphanumeric", "data": [], "statusCode": 404 }

The actual response I get instead is: { "result": 0, "reason": "The given data was invalid.", "data": [], "statusCode": 404 }

Please help. This is bugging me.

Balance answered 16/9, 2017 at 18:33 Comment(0)
B
17

I've finally discovered why this isn't working. It's not an issue of errors in the implementing code or Laravel, but one of either: (i). writing good PHP code to handle the self-evident result, which clearly I didn't do; (ii). insufficient documentation within Laravel on how to actually use the validation error response. Take your pick.

Laravel's validation throws a Illuminate\Validation\ValidationError. Believe it or not, this actually defaults the error message to "The given data was invalid.", so when you catch an \Exception and retrieve its $e->getMessage(), this default error-message is what you (correctly) get.

What you need to do is capture the \Illuminate\Validation\ValidationError - which I should've done originally, duh! - and then use its methods to help you distill the error messages from it.

Here's the solution I've come up with:

<?php
namespace App\Http\Controllers;

use App\Location;
use Illuminate\Http\Request;


class LocationController extends Controller
{

    /**
     * Action method to add a location with the supplied Data
     * 
     * @param \Illuminate\Http\Request $p_oRequest Request
     * 
     * @return JSON
     */
    public function add(Request $p_oRequest)
    {
        try {
        
            $arrValid = array(
                'name' => 'required|alpha_num',
                'user_id' => 'required|integer',
            );
            $p_oRequest->validate(
                $arrValid,
                array(
                    'name.required' => 'Name is missing',
                    'name.alpha_num' => 'Name must be alphanumeric',
                    'user_id.required' => 'User Id is missing',
                    'user_id.integer' => 'User Id must be an integer',
                )
            );
            
        } catch (\Illuminate\Validation\ValidationException $e ) {
        
            /**
             * Validation failed
             * Tell the end-user why
             */
            $arrError = $e->errors(); // Useful method - thank you Laravel
            /**
             * Compile a string of error-messages
             */
            foreach ($arrValid as $key=>$value ) {
                $arrImplode[] = implode( ', ', $arrError[$key] );
            }
            $message = implode(', ', $arrImplode);
            /**
             * Populate the respose array for the JSON
             */
            $arrResponse = array(
                'result' => 0,
                'reason' => $message,
                'data' => array(),
                'statusCode' => $e->status,
            );

        } catch (\Exception $ex) {

            $arrResponse = array(
                'result' => 0,
                'reason' => $ex->getMessage(),
                'data' => array(),
                'statusCode' => 404
            );

        } finally {

            return response()->json($arrResponse);

        }
        
    }
    
}

So, indeed, Laravel was supplying the correct response, and did what it said on the side of the tin, but I wasn't applying it correctly. Regardless, as a help to future me and other lost PHP-mariners at Laravel-sea, I provide the solution.

In addition, thanks to Marcin for pointing out my buggy coding, which would've caused a problem even if I had implemented the above solution.

Balance answered 14/10, 2017 at 20:23 Comment(9)
this is messy! laravel should provide easier way to do it. Thanks for this question and anser, and consider checking it as correctVlf
Thanks for the endorsement stackoverflow.com/users/8745014/gileneusz. I feel awkward checking my own answer.Balance
checking your own answer is just a method to show users what works for youVlf
This solution worked for me to find the errors when via tests, thanks.Fijian
The much more general and elegant solution is to stop Laravel's Exception handler from treating validation Exceptions as application Exceptions. See my answer for an example.Cobble
@quinny. I'm not sure if this is a typo on your side but the actual Exception class thrown is \Illuminate\Validation\ValidationException instead.Anatollo
@Anatollo Hi, Steven. The code above was using Laravel 5.4, so I needed to check the namespace for the ValidationException class, and you're right. Thanks for pointing that out. The answer above has been modified with your correction.Balance
Since you're updating this question, @quinny, you should mark my answer as the accepted answer. Yours solves the problem locally using lots of code and mixed concerns whereas mine solves the general case for all validation responses throughout the project at once.Cobble
Just move the validate() call above the try/catch block. The validation method catches the exception and returns a response so you don't need to catch the exceptions explicitlySeldun
C
12

The problem is probably that Laravel's default Exception handler is not prepared to relay detailed validation info back to the user. Instead, it hides Exception details from the user, which is normally the right thing to do because it might form a security risk for other Exceptions than validation ones.

In other words; if the Exception Handler's render function (implemented in /app/Exceptions/Handler.php) catches your validation errors, they will be interpreted as a general application Exception and the general error message relaid to the user will always read 'The given data was invalid'.

Make sure the render method ignores instances of \Illuminate\Validation\ValidationException, and you should get the response you expect:

public function render($request, Exception $exception) {

    if (! $exception instanceof \Illuminate\Validation\ValidationException)) {

        // ... render code for other Exceptions here

    }

}

Another way to make the Exception Handler relay ValidationException details with the response would be to do something like this in the render method:

if ($exception instanceof ValidationException && $request->expectsJson()) {
    return response()->json(['message' => 'The given data was invalid.', 'errors' => $exception->validator->getMessageBag()], 422);
}

Background

Laravel is basically (ab)using Exceptions here. Normally an Exception indicates a (runtime) problem in the code, but Laravel uses them as a mechanism to facilitate request validation and supply feedback to the user. That's why, in this case, it would be incorrect to let your Exception Handler handle the Exception -- it's not an application Exception, it's info meant for the user.

The code in the answer supplied by OP works, because he catches the ValidationException himself, preventing it to be caught by the application's Exception Handler. There is no scenario in which I think that would be wanted as it's a clear mix of concerns and makes for horribly long and unreadable code. Simply ignoring ValidationExceptions or treating them differently in the Exception Handler like I showed above should do the trick just fine.

Cobble answered 17/4, 2018 at 17:25 Comment(0)
S
7

I've only just seen this but all you need to do is move the validate call before the try/catch

$p_oRequest->validate(
    [
        'name'    => 'required|alpha_num',
        'user_id' => 'required|integer',
    ],
    [
        'name.required'    => 'Name is required',
        'name.string'      => 'Name must be alphanumeric',
        'user_id.required' => 'Curator User Id is required',
        'user_id.required' => 'Curator User Id must be an integer',
    ]
);

try {

...

} catch(\Exception $e) {
    return back()->withErrors($e->getMessage())->withInput();
}

Because Laravel catches the validation exception automatically and returns you back with old input and an array of errors which you can output like

@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif
Seldun answered 26/1, 2019 at 18:17 Comment(0)
E
0

Your messages should be validation rules, so instead of:

'name.required' => 'Name is required',
'name.string' => 'Name must be alphanumeric',
'user_id.required' => 'Curator User Id is required',
'user_id.required' => 'Curator User Id must be an integer',

you should have:

'name.required' => 'Name is required',
'name.alpha_num' => 'Name must be alphanumeric',
'user_id.required' => 'Curator User Id is required',
'user_id.integer' => 'Curator User Id must be an integer',
Ewart answered 16/9, 2017 at 19:31 Comment(3)
That still didn't solve the issue. Clearly it is a problem experienced by other Laravel developers. Look at github.com/laravel/framework/issues/21059Balance
No, you set invalid messages. When you used alpha_num validation rule, you should use name.alpha_num to make it work. So either you are doing something wrong or you put wrong code in your questionUlmaceous
Thanks for responding, Marcin. I made the corrections you recommended. All credit to you, you are right - I made errors, and they would create their own problems, however, the underlying cause of the unhelpful response in my JSON still isn't resolved. I have discovered the wider solution, however which I will post as an answer.Balance
D
0

Put the response in a variable and use dd() to print it. You will find it on the "messages" method. Worked for me.

dd($response);

Digitalism answered 21/7, 2020 at 20:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.