Rolandus
Rolandus

Reputation: 778

Rails Responds with 404 on CORS Preflight Options Request

I'm creating a set of services using Rails 4, which I am consuming with a JavaScript browser application. Cross-origin GETS are working fine, but my POSTs are failing the preflight OPTIONS check with a 404 error. At least, I think that's what's happening. Here are the errors as they appear in the console. This is Chrome 31.0.1650.63 on a Mac.

OPTIONS http://localhost:3000/confessor_requests 404 (Not Found) jquery-1.10.2.js:8706
OPTIONS http://localhost:3000/confessor_requests No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. jquery-1.10.2.js:8706
XMLHttpRequest cannot load http://localhost:3000/confessor_requests. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. main.html:1

I've searched high and low for instructions on enabling CORS, and I'm stumped. The usual recommendation seems to be to put something like this in the Application controller, which I did.

before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers

def cors_set_access_control_headers
  headers['Access-Control-Allow-Origin'] = '*'
  headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS'
  headers['Access-Control-Allow-Headers'] = '*'
  headers['Access-Control-Max-Age'] = "1728000"
end

def cors_preflight_check
  if request.method == :options
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS'
    headers['Access-Control-Allow-Headers'] = '*'
    headers['Access-Control-Max-Age'] = '1728000'
    render :text => '', :content_type => 'text/plain'
  end
end

Followed by some kind of route in routes.rb that will redirect to this action when an OPTIONS request comes in.

match "/*all" => "application#cors_preflight_check", :constraints => { :method => "OPTIONS" }

The 'match' directive no longer works in Rails 4, so I fiddled with it, attempting to make it match POSTS directly, like this:

post "/*all" => "application#cors_preflight_check", :constraints => { :method => :options }

But it still doesn't work. Since the GET requests are working, I'm assuming that what I'm missing is the correct route for the OPTIONS request. However, I've tried every route I can think of, and nothing seems to let the request through.

I also tried installing cyu/rack-cors, and this gives the same result.

Anyone know what I'm doing wrong?

Upvotes: 47

Views: 28377

Answers (8)

jibai31
jibai31

Reputation: 1885

Here's a Rails 5 solution with the gem rack-cors.

0) What are we trying to do?

Here's a super clear article on CORS (Cross-Origin Resource Sharing).

Your app needs to accept the preflighted requests, which are OPTIONS requests made with special headers like below. In this case the browser is checking the authorization for a POST request from https://some-site.com, with a given Request-Headers that could be anything.

Origin: https://some-site.com 
Access-Control-Request-Method: POST 
Access-Control-Request-Headers: x-csrf-ump-token

And respond with a simple text content (it doesn't matter) with appropriate headers, like below:

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD
Access-Control-Allow-Headers: x-csrf-ump-token
Access-Control-Max-Age: 1728000

NB: Access-Control-Allow-Headers is optional. We'll set it to any below.

1) Handle the OPTIONS requests

# config/routes.rb
match '*all', to: 'cors#preflight_check', via: [:options]

# app/controllers/cors_controller.rb
class CorsController < ApplicationController
  def preflight_check
    render plain: 'OK!'
  end
end

2) Now add the proper headers with rack-cors

# config/initializers/cors.rb

# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head],
      max_age: 1728000
  end
end

Check this article to further customize this config, for various origins.

Upvotes: 0

Tyler Collier
Tyler Collier

Reputation: 11998

Here's a solution with the rack-cors gem, which you said you tried. As others have mentioned, you didn't give much detail in regards to which front-end framework you're using and what the actual request looks like. So the following may not apply to you, but I hope it helps someone.

In my case, the gem worked fine until I used PUT (or PATCH or DELETE).

If you look in your browser developer console, look at the request headers, and you should have a line like this:

Access-Control-Request-Method: PUT

The important thing to note is that the methods you pass to resource are for the Access-Control-Request-Method, not the Request Method that is to come after the pre-flight check.

Note how I have :methods => [:get, :post, :options, :delete, :put, :patch] that will include all the methods I care about.

Thus your entire config section should look something like this, for development.rb:

# This handles cross-origin resource sharing.
# See: https://github.com/cyu/rack-cors
config.middleware.insert_before 0, "Rack::Cors" do
  allow do
    # In development, we don't care about the origin.
    origins '*'
    # Reminder: On the following line, the 'methods' refer to the 'Access-
    # Control-Request-Method', not the normal Request Method.
    resource '*', :headers => :any, :methods => [:get, :post, :options, :delete, :put, :patch], credentials: true
  end
end

Upvotes: 28

Nicholas Blasgen
Nicholas Blasgen

Reputation: 872

Yes, as others have pointed out there is a GEM to maybe do this better. But since I very much liked the method pointed out in the original blog post with the cors code, I've found the Rails 4 solution if you're using that code.

In your routes.rb:

match '*all' => 'my_method_name#cor', via: :options

In your my_method_name controller:

def cor
    # blank section for CORR
    render :text => ''
end

As long as you have that plus your other code:

before_filter :cors_preflight_check
after_filter :cors_set_access_control_headers
...

Then you should be set for Rails 4.

Upvotes: 7

Tom Doe
Tom Doe

Reputation: 906

Ran into this issue with Rails 4 and Devise. I ended up using Rack CORS middleware gem by Calvin Yu. A great complimentary blog article.

Upvotes: 2

RonLugge
RonLugge

Reputation: 5184

I ran into the same issue, and am currently evaluating the following routes for any possible security / performance issues. They solve the issue, but...

match '/', via: [:options], 
 to:  lambda {|env| [200, {'Content-Type' => 'text/plain'}, ["OK\n"]]}
match '*unmatched', via: [:options],  
 to:  lambda {|env| [200, {'Content-Type' => 'text/plain'}, ["OK\n"]]}

Despite 'match' supposedly not working in Rails 4, apparently it does work if you restrict it to a specific method.

Upvotes: 2

po3t
po3t

Reputation: 148

I'm not sure what javascript front end framework you are using (or if you are) since you didn't elaborate on what you are doing on the client-side to connect to your Rails 4 API, but I thought I'd add my answer in case it helps anybody.

I ran into the exact same problem while connecting to a Rails 4 API with the Devise gem from an AngularJS front end (both were running on separate localhost ports). I was trying to log in to the back end using a POST request from an AngularJS form but I kept getting a 404 NOT FOUND error because I was sending an OPTIONS request with preflight. It took more than 2 days to figure out how to fix the problem.

Basically you need to set up a proxy server for your front end (Angular, Backbone, whatever) in order to connect to your API so that your front end thinks that the request is using the same origin. There are some straightforward solutions out there for setting up Proxies using GruntJS. I am using Gulp for my project with Gulp-Connect and proxy-middleware with the following setup (based on the solution found here):

var gulp            = require('gulp'),
    connect         = require('gulp-connect');

gulp.task('devServer', function() {
      connect.server({
        root: './dist',
        fallback: './dist/index.html',
        port: 5000,
        livereload: true,
        middleware: function(connect, o) {
            return [ (function() {
                var url = require('url');
                var proxy = require('proxy-middleware');
                var options = url.parse('http://localhost:3000/');
                options.route = '/api';
                return proxy(options);
            })() ];
        }
      });
    });

I hope this helps someone!

Upvotes: 1

dhoelzgen
dhoelzgen

Reputation: 1150

Perhaps this gist can help you: CORS in Rails 4 APIs

It adds the OPTIONS method to the route definition, and adds a filter to the API base controller which directly responds to OPTIONS requests with the correct header, and sets the correct CORS headers for all other actions, too.

Upvotes: 4

AndroC
AndroC

Reputation: 4884

Working on Rails 3.2.11.

I put

match '*path', :controller => 'application', :action => 'handle_options_request', :constraints => {:method => 'OPTIONS'}

in my routes.rb file. The key was to put it as top priority (on top of the routes.rb file). Created that action so that it is publicly available:

  def handle_options_request
    head(:ok) if request.request_method == "OPTIONS"
  end

And a filter in application controller:

 after_filter :set_access_control_headers

  def set_access_control_headers
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
  end

Upvotes: 11

Related Questions