46

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?

Rolandus
  • 718
  • 1
  • 6
  • 10
  • 1
    `match` should still work, but it complains if you don't pass a `via` option. Doing it with `post` is definitely not going to work. As somebody who made this exact mistake before with `rack-cors`, I'm also going to ask: did you restart the server after configuring the `rack-cors` middleware? I spent an hour+ chasing that a while back myself. :) – Matt Jones Jan 01 '14 at 04:59
  • Same here. I've tried both the custom methods and the gem: everything works fine until an `ActiveRecord::RecordNotFound` exception is raised. In that case CORS doesn't work. – collimarco Mar 10 '16 at 20:41
  • I have further investigated the issue: in my case the preflight response headers are correct, while the actual request (when an exception is raised) produces a response without the CORS headers – collimarco Mar 11 '16 at 09:12

8 Answers8

26

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
Tyler Collier
  • 9,964
  • 9
  • 63
  • 74
11

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
ancajic
  • 4,504
  • 1
  • 41
  • 62
  • 5
    This answer is for Rails 3, but the question is specifically about Rails 4. – dmur Jul 16 '14 at 17:11
  • @ancajic Thanks for your answer. Is there any security implication with this that you know of? – Sebastialonso Dec 08 '14 at 15:01
  • 1
    @Sebastialonso... Obviously it can cause problems if your HTTP API has endpoints that respond to requests with method=OPTIONS. Other than that, I don't see any security implications that would not occur already by enabling CORS. More experienced Rails developers might be of more help ;) – ancajic Dec 08 '14 at 15:26
6

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.

Nicholas Blasgen
  • 642
  • 6
  • 13
  • Nice solution, instead of setting the headers manually in the application controller I set them per environment. For development in `config/environments/development.rb` you can configure `config.action_dispatch.default_headers = {}` to include the COR headers. – rramsden Sep 21 '16 at 04:59
3

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.

dhoelzgen
  • 1,138
  • 7
  • 13
2

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.

Tom Doe
  • 785
  • 1
  • 9
  • 16
1

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!

po3t
  • 138
  • 1
  • 9
  • 1
    I'm not sure implementing a proxy server is really an answer to this. What if someone else wants to interact with your API? Don't you think Rails should respond properly to the options request? – mpowered Nov 19 '14 at 18:11
  • @AdamRobertson - You're probably right. My solution is not for publicly consumed APIs if anything other than GET requests are required. This solution should only be used for private APIs. That said, I was running into this problem regardless of what I did on the Rails end. No matter what kind of CORS solution I implemented, I would always make an OPTIONS request if I made anything other than a GET request. Chalk it up to lack of experience with Rails on my end. – po3t Nov 19 '14 at 21:32
1

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.

RonLugge
  • 4,856
  • 5
  • 30
  • 55
0

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.

jibai31
  • 1,349
  • 13
  • 21