14

Using Laravel's resource routes, I've set up an API to serve as the back-end of a React JS application. I'm attempting to access the 'update' method currently. I'm using Javascript's fetch() to accomplish this, so its making one OPTIONS request first, then making the POST request (the form has a method spoof in it, setting _method to PATCH instead - this obviously doesn't affect the initial OPTIONS call). This same page is also making a GET request to the same endpoint via the same method, which works fine.

The fetch() call is below. Of course, this being React, it's called through a Redux Saga process, but the actual fetch is there.

function postApi(values, endpoint, token) { // <-- values and endpoint are sent by the component, token is sent by a previous Saga function
    return fetch(apiUrl + endpoint, { // <-- apiUrl is defined as a constant earlier in the file
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + token
        },
        body: JSON.stringify(
            values
        )
    }).then(handleApiErrors)
      .then(response => response.json())
      .catch((error) => {throw error})
}

And the Laravel routes:

Route::group(['middleware' => 'auth:api'], function() {
    Route::resource('users', 'UserController');
}

I was encountering an error where the initial OPTIONS request to the URL was returning a 404 error, which right away is strange, since the endpoint obviously exists, the exact same endpoint having just been queried seconds ago, but I assumed maybe Laravel was returning the wrong error, and I had used the wrong method. I did some digging and debugging trying to get the request to be correct before giving up and making the request in Postman. The thing is: it works fine in Postman.

Here are the response headers from the server (note that any access origin is permitted):

Access-Control-Allow-Origin:*
Cache-Control:no-cache, private
Connection:close
Content-Length:10
Content-Type:text/html; charset=UTF-8
Date:Thu, 21 Sep 2017 13:29:08 GMT
Server:Apache/2.4.27 (Unix) OpenSSL/1.0.2l PHP/7.0.22 mod_perl/2.0.8-dev Perl/v5.16.3
X-Powered-By:PHP/7.0.22

Here's the request headers for the request as made from the React JS app (the one that receives a 404 error):

Accept:*/*
Accept-Encoding:gzip, deflate, br
Accept-Language:en-US,en;q=0.8,fr;q=0.6,ga;q=0.4
Access-Control-Request-Headers:authorization,content-type
Access-Control-Request-Method:POST
Cache-Control:no-cache
Connection:keep-alive
Host:localhost
Origin:http://localhost:3000
Pragma:no-cache
Referer:http://localhost:3000/employees/edit/13
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36

In Postman, I set up those exact same request headers and made the exact same OPTIONS request to the server. And it worked fine! I received an empty 200 response.

Just to be sure, I double-checked the Apache access log. And sure enough:

...
::1 - - [20/Sep/2017:15:33:24 -0400] "OPTIONS /the/path/to/api/users/13 HTTP/1.1" 200 -
::1 - - [20/Sep/2017:15:40:26 -0400] "OPTIONS /the/path/to/api/users/13 HTTP/1.1" 404 10
...

Request method the exact same, request URL the exact same, except one returned 200, the other returned 404, for no discernable reason.

Additionally, I should add that another POST request, to the create method, works just fine.

What could be causing this?


ATTEMPTED SOLUTIONS

1. I saw this question (React Native + fetch + API : DELETE request fails in App, works in Postman), and even though I'm running Apache, not Nginx, I thought I'd try adding a trailing slash to the request URL. The OPTIONS request now returns a 301 error (moved permanently).

2. I removed the trailing slash and continued trying to fix. Per comment suggestion, I removed the automatic route generation and created my own:

Route::get('/users', 'UserController@index');
Route::post('/users', 'UserController@create');
Route::put('/users/{user}', 'UserController@update');
Route::patch('/users/{user}', 'UserController@update');
Route::get('/users/{user}', 'UserController@show');

The Postman request still returns 200 OK, and the React request still returns 404 Not Found.

3. Eureka! Kind of. Per another comment suggestion, I exported the request from Chrome as cURL and imported it into Postman directly - maybe I missed something when copying the headers over. It seems I did, because now the Postman request also returns 404!

After playing around with disabling and/or enabling the imported headers, I've determined that the issue is the combination of the Origin and Access-Control-Request-Method headers. If only one is present the request returns 200, but if both are present I receive a 404.

This does still leave me with the question of how to fix the problem, however. At this point I wonder if the question might become more of a Laravel question - IE, why an OPTIONS request to a perfectly valid Resource route would return 404. I assume because those resources routes are listening for PUT or PATCH but not OPTIONS.

CGriffin
  • 1,406
  • 15
  • 35
  • Great question but adding the actual `fetch` call to the question might help to get a better understanding. – bennygenel Sep 20 '17 at 20:07
  • Post your Laravel routes as well. – Samsquanch Sep 20 '17 at 20:08
  • Added the fetch() example and the Laravel routes. – CGriffin Sep 20 '17 at 20:12
  • 1
    What about your CORS settings for your server? Can you show us those? Also, in dev tools in your browser, do you see any errors in the console other than the 404? – tptcat Sep 21 '17 at 00:26
  • Sadly no, no errors in the console at all, aside from the 404 itself. Added response headers from the server. – CGriffin Sep 21 '17 at 13:30
  • you could try to make your routes by yourself (i.e without resource) and see if you can reproduce this error. How about that? – Oluwatobi Samuel Omisakin Sep 21 '17 at 17:30
  • Gave it a try - no dice, unfortunately. – CGriffin Sep 21 '17 at 18:08
  • You can export a network request from Chrome by right clicking on it in the network tab and choosing "Copy as cURL". Themnyou can import it in Postman. That should make sure they are exactly the same. – Matthew Daly Sep 21 '17 at 18:12
  • That's a really good tip Matthew, and led to a breakthrough! Though not a solve. I've successfully reached the point where they are _both_ returning 404 errors for no discernable reason! Progress! – CGriffin Sep 21 '17 at 18:21
  • Sounds like the problem is now just a bog-standard CORS header issue, for which there are off the shelf packages. – Matthew Daly Sep 21 '17 at 18:25
  • As an aside: the original scope of this question was 'why are these identical requests returning different outcomes?` Since the question now looks to have shifted to 'why does Laravel return 404 on an `OPTIONS` request to a resource route', would it be better to expand this question, or post a new one? – CGriffin Sep 21 '17 at 18:26
  • You are correct Matthew, however I have long since solved the CORS header issue. The second thing I did when I set up this API was require the CORS middleware. So while it _is_ probably a CORS header issue, not sure if its a 'bog-standard' CORS header issue. – CGriffin Sep 21 '17 at 18:27
  • (and in any case, I believe a CORS issue would return a relevant error, not a 404, no?) – CGriffin Sep 21 '17 at 18:33
  • In any case this question needs an answer - now that I've identified the problem I can try to solve it myself first. Matthew, your suggestion pushed it over the edge, if you can phrase that as an answer I'll accept it. – CGriffin Sep 21 '17 at 18:43
  • What does your controller method look like? Is it returning JSON in a conditional scenario? – simonhamp Jun 20 '18 at 12:02
  • In the above code you didn't define options route for the resources – user3647971 Oct 21 '18 at 00:01

1 Answers1

2

Since you have your CORS set up, all you need to do next is handle the 'preflight' OPTIONS request. You can do this using a middleware:

PreflightRequestMiddleware:

if ($request->getMethod() === $request::METHOD_OPTIONS) {
    return response()->json([],204);
}

return $next($request);

Add the above code in the handle() method of the newly created middleware. Add the middleware in the global middleware stack.

Also, do not forget to add the OPTIONS method to Access-Control-Allow-Methods in your CORS setup.

For more options, check this out.

Answer:

Read this article. When the browser sends OPTIONS request to your application, the application has no way of handling it since you only defined a GET/POST/DELETE/PUT/PATCH route for the given endpoint.

So, in order for this route to work with preflight requests:

Route::get('/users', 'UserController@index');

it would need a corresponding OPTIONS route:

Route::options('/users', 'UserController@options');

Note: You would use a middleware to handle all OPTIONS requests in one place. If, however, you are using OPTIONS requests for other purposes - check the first link in this answer for more options.

Voyowsky
  • 126
  • 2
  • 7