Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Exact Same Request Returns 200 to Postman but 404 to React App

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.

like image 497
CGriffin Avatar asked Sep 20 '17 19:09

CGriffin


1 Answers

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.

like image 63
Voyowsky Avatar answered Nov 07 '22 14:11

Voyowsky