jlrdw's avatar
Level 75

Ajax XMLHttpRequest returning 200 on failed validation

I have no problem with a successful post or put. However even when validation fails I get a 200.

The exact same put or post in jquery correctly returns the 422. More on this is here https://laracasts.com/discuss/channels/javascript/jquery-to-fetch-js-post

The code

    document.getElementById('submitBtn').addEventListener('click', submitPost);
    function submitPost(e) {
        e.preventDefault();

        var url = "http://localhost/laravel70/pet/petupdate";

        var data = {};
        data.petid = document.getElementById('petid').value;
        data.species = document.getElementById('species').value;
        data._token = document.getElementsByName("_token")[0].value;
        //data._method = document.getElementsByName("_method")[0].value;
        
        var json = JSON.stringify(data);

        var xhr = new XMLHttpRequest();
        xhr.open("POST", url, true);
        xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');
        xhr.onload = function () {
            var users = JSON.parse(xhr.responseText);
            if (xhr.readyState == 4 && xhr.status == "200") {
                console.table(users);
                document.getElementById('msg').innerHTML = xhr.responseText;
                //document.getElementById('msg').innerHTML = "All OK";
            } else if (xhr.status == "422") {
                  document.getElementById('msg').innerHTML = "Validation error";  
                    
            } else {
                console.error(err);
                document.getElementById('msg').innerHTML = "An error occured";
            }
        }
        xhr.send(json);
    }

The else if with 422 does nothing. Why does jquery handle it but a plain XMLHttpRequest does not.

And this is just a small little controller for experimenting with fetch and XMLHttpRequest.

    public function petUpdate(Request $request) {
       $request->validate([
            'species' => 'required'
        ]);   // This works in jquery
        $petid = $request->input('petid');
        $species = $request->input('species');
        $postdata = [
            'species' => $species
        ];
        DB::table('dc_pets')
                ->where('petid', $petid)
                ->update($postdata);
        return Response::json(['success' => 'all okay']);
        
    }

Any idea why it doesn't work? And I tried with put and post, both work fine, just the validation is a problem.

0 likes
17 replies
mabdullahsari's avatar
Level 16

You should listen to the readystatechange event instead of the load event.

xhr.onreadystatechange = function () {
    if (this.readyState === XMLHttpRequest.DONE) {
        if (this.status == 200) // handle success
        if (this.status == 422) // handle validation error
    }
}
1 like
jlrdw's avatar
Level 75

I will try this later and report back, thanks.

jlrdw's avatar
Level 75

@mabdullahsari I changed that section to:

        xhr.onreadystatechange = function () {
            const message = JSON.parse(xhr.response);
            if (this.readyState === XMLHttpRequest.DONE) {
                if (this.status == 200) {
                    var div = document.getElementById('msg');
                    for (var key in message) {
                        div.innerHTML += message[key];
                    }
                }
                if (this.status == 422) {
                    var div = document.getElementById('msg');
                    div.innerHTML += xhr.response;
                    for (var key in xhr.responseText) {
                        div.innerHTML += message[key];
                    }
                }
            }
        }

The 200 works and displays all okay. However the 422 still does nothing.

Note however that in the background laravel still prevents that field from being blank.

And still in network tab it's showing 200 on failed validation.

And fetch js showed 405. I also tried

const message = JSON.parse(xhr.responseText);

Edit: Like I said, jquery gets the 422. And the docs state:

When using the validate method during an AJAX request, Laravel will not generate a redirect response. Instead, Laravel generates a JSON response containing all of the validation errors. This JSON response will be sent with a 422 HTTP status code.

So surely it should work using plain ajax, without special coding to send a 422. This is very hard to troubleshoot. But I am not giving up.

Snapey's avatar

make sure you are telling Laravel that you expect a json response.

Also, check the network traffic with developer tools. Check what was posted and what was returned.

jlrdw's avatar
Level 75

@snapey using jquery I get a 422 as expected. Using plain XMLHttpRequest I get 200 still on validation failure.

And yes I see all this in network tab.

You stated " make sure you are telling Laravel that you expect a json response. "

But it should send a json response automatically according to the docs when using ajax.

How would you tell laravel you expect a json response in that particular case?

And Fetch gives 405 on validation error.

Have you successfully used XMLHttpRequest put or post? So far I can post or put just fine, just can't get the 422 returned correctly. I have looked over at least 50 stackoverflow post as well.

Edit: I searched again for have laravel return json response after failed validation and found: https://medium.com/@donaldkagunila/get-a-json-response-after-laravel-validation-4200e3119049

Will try after supper.

jlrdw's avatar
Level 75

@snapey I will check out that article, and I'm in the middle of trying both, fetch also. As I said there I get 405 instead of 422.

Do you have any idea why jQuery gets 422 as expected, but Fetch and plain Ajax you have to do special things to get it to work.

Thanks for replying.

mabdullahsari's avatar

I think I know why. Can you add this line after you callopen on the xhr instance?

xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
jlrdw's avatar
Level 75

@mabdullahsari I tried that header, no luck.

@snapey I changed

        $request->validate([
            'species' => 'required'
        ]);
      

To

        $validator = \Illuminate\Support\Facades\Validator::make($request->all(), [
                    'species' => 'required',
        ]);

        if ($validator->fails()) {
            return response()->json($validator->errors(), 422);
        }

It works now! So some how laravel doesn't always send the failed validation correctly, unless you tell it to.

I wish I could give 2 Best Answers, you both got me on the right track.

I appreciate both. @snapey I'll go ahead and give this to @mabdullahsari he stated:

You should listen to the readystatechange event instead of the load event.

But your:

make sure you are telling Laravel that you expect a json response.

Helped so much also. Now to test Fetch JS some more.

Edit: If you see this, where do I now tack on another generic error handler? One to console.log other possible errors.

mabdullahsari's avatar

Well, I think I really do know now.

You should add an Accept request header with the value application/json. This way, you'll force Laravel to return a JSON response and won't need that "ugly" if check.

(edited)

jlrdw's avatar
Level 75

@mabdullahsari I had this:

xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

What would I change it to? And what about The fetch js, it's now working?

In fetch I have:

            headers: {
                'Content-Type': 'application/json',
            },
mabdullahsari's avatar

You should additionally send an Accept header so Laravel knows in what format to respond back. So, basically add this as well:

xhr.setRequestHeader('Accept', 'application/json');
mabdullahsari's avatar

After doing what I said above, you can revert your change and keep your original code:

$request->validate([
    'species' => 'required'
]);

It should keep working. IIRC, Laravel uses a method called wantsJson in the exception handler and checks if you want a JSON as response. It determines this by checking the Accept header.

I may be wrong though, as I'm on mobile right now and don't have access to the vendor codebase. But I'm pretty sure that's how it works.

jlrdw's avatar
Level 75

When you get time, here's new problem:

First I changed it back and added the header:

            headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json',
            },
       

I now get a 422, so that's good. But now to display it: I have:

                .catch((error) => {
                    var div2 = document.getElementById('msg');
                    for (var k in error) {
                        div2.innerHTML += error[k];
                    }
                    console.error('Error:', error);
                    document.getElementById('msg').innerHTML = "An error occured";
                });

With the Validator::make my loop gave:

The species field is required.

But now it gives:

The given data was invalid.[object Object]

But I do see the 422.

mabdullahsari's avatar

That's because of the default response of the framework. Laravel responds like this:

{
    "message": "The given data was invalid",
    "errors": {
        "species": [
            "The field is required or some other message"
        ]
    }
}

So what you need to do in your js code is looping over the keys of the errors object inside of your "original" error object (the one that you catch) if that makes sense.

You can either flatten the array and concatenate or do some other fancy stuff or just simply display the first element in the array.

1 like
Snapey's avatar

Do you have any idea why jQuery gets 422 as expected, but Fetch and plain Ajax you have to do special things to get it to work.

Yes, because the jquery method includes the header already.

content type is what data you are sending

accept header is what you want to receive

1 like
jlrdw's avatar
Level 75

Yeah I got that when I read that article you mentioned, I've been using jQuery for so long now.

Fetch is fine, only problem I was new to it and rather than finding one good tutorial or example I actually had to look at quite a few from the web to piece it all together.

That last bit of help you two guys gave me really helped finalize things , and I got it all working now.

All of my error messages including not authorized works great now. Later I will probably do a GitHub gist so it might help others.

1 like

Please or to participate in this conversation.