I'm working on a website analytics tool, and in pursuit of that goal, I wanted to
POST
some data from a series of websites to the one that's doing the tracking. If you've tried to do that before, you've run afoul of the
same origin policy, as I did, so you'll need to specify how your application handles
Cross-Origin Resource Sharing.
I won't go into all the details about why that is the case - for that you can read the Wikipedia links above. Instead, I'm going to show a short example of how I handled this in my Rails 3 app.
First, you need to specify a route that will handle an HTTP OPTIONS method request.
# config/routes.rb
resources :web_hits, :only=>[:create]
match '/web_hits', :controller => 'web_hits', :action => 'options', :constraints => {:method => 'OPTIONS'}
Since this controller only handles incoming requests to create a web hit resource, I've only specified the
POST
method on the it (which will run the
create
method in the controller). However, before the browser will send the
POST
to the tracking website, it first sends an
OPTIONS
request to see if it can do the
POST
. The second line specifies the route for that: it will go to my
web_hits_controller
and use the action
options
.
Next, we'll look at the controller.
# controllers/web_hits_controller.rb
class WebHitsController < ApplicationController
def create
if access_allowed?
set_access_control_headers
head :created
else
head :forbidden
end
end
def options
if access_allowed?
set_access_control_headers
head :ok
else
head :forbidden
end
end
private
def set_access_control_headers
headers['Access-Control-Allow-Origin'] = request.env['HTTP_ORIGIN']
headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
headers['Access-Control-Max-Age'] = '1000'
headers['Access-Control-Allow-Headers'] = '*,x-requested-with'
end
def access_allowed?
allowed_sites = [request.env['HTTP_ORIGIN']] #you might query the DB or something, this is just an example
return allowed_sites.include?(request.env['HTTP_ORIGIN'])
end
end
The key above is checking whether or not the request should be allowed. Here I've set
access_allowed?
to always return true, but you could have some checks in there that inspect the request to determine if you want to allow it or not. If you do, set the headers and respond appropriately. Since I don't need to really return a response, I'm only returning the headers indicating success or access denied, but you could just as easily turn those
head
method calls into
render
s if you need to render some content.
A good resource that helped me figure this out was
Cross-Origin Resource Sharing for JSON and RAILS. He didn't go into detail on restricting access nor routing though, so I felt like this would be a good addendum.
Hey! Why don't you make your life easier and subscribe to the full post
or short blurb RSS feed? I'm so confident you'll love my smelly pasta plate
wisdom that I'm offering a no-strings-attached, lifetime money back guarantee!
Leave a comment
I know the intention of this post is to outline how to do it within Rails, but if you have control over your web server and want to enable this across your entire app (not a specific action/controller, etc), you can handle it there instead. We set it up in nginx with the following:
location / {
# For CORS
if ($request_method = OPTIONS ) {
add_header Access-Control-Allow-Origin "
http://localhost";
add_header Access-Control-Allow-Methods "GET, OPTIONS";
add_header Access-Control-Allow-Headers "Authorization";
add_header Access-Control-Allow-Credentials "true";
add_header Content-Length 0;
add_header Content-Type text/plain;
return 200;
}
...other awesome nginx stuff here
}
So, this will allow GET or OPTIONS requests from only
http://localhost...(we needed the Authorization/Credentials stuff b/c we were doing Basic Auth in our app as well).
This removes all the code changes in your Rails app, but also exposes the entire application. Anyway, just another way to consider.
Posted by
tomkersten
on Oct 28, 2011 at 11:31 AM UTC - 6 hrs
Thanks Tom!
Where would you put that code?
Posted by
Sammy Larbi
on Oct 28, 2011 at 11:59 AM UTC - 6 hrs
Oh. Sorry. You can throw that in your nginx config...inside the "server" block of one of your hosts. Here is my (working) config for one the site we rolled it out on (with domains anonymized, etc):
https://gist.github.com/1323477We are requiring Basic Auth, so, you may not need the highlighted lines to make it work. Ping me @ tom at whitespur dot com if you have any thoughts/questions or tips on improving it...
Posted by
tomkersten
on Oct 28, 2011 at 03:39 PM UTC - 6 hrs
Awesome, thanks for the example Tom.
Posted by
Sammy Larbi
on Oct 31, 2011 at 02:11 PM UTC - 6 hrs
Awesome, thanks!
Posted by
thermistor
on Sep 18, 2012 at 12:13 PM UTC - 6 hrs
I tried this ... get request works but post still suffers from that Allow-access-allow-origin
I can see
Incoming Headers:
Origin:
http://localhostAccess-Control-equest-Method:
Access-Control-Request-Headers:
in unicorn_err log
Any idea..
Posted by rohitrox
on Apr 22, 2013 at 11:47 AM UTC - 6 hrs
This may not be it, but have you double-checked the headers you're setting to ensure they're the right ones? For example, you mention "Allow-access-allow-origin" but the header I think you want is "Access-Control-Allow-Origin"...
Hope that helps!
Posted by
Sammy Larbi
on Apr 22, 2013 at 12:42 PM UTC - 6 hrs
My bad ...
Actually I am seeing
Origin
http://localhost is not allowed by Access-Control-Allow-Origin.
in chrome when I do post request to my remote app.
and in unicorn_err.log
Incoming Headers:
Origin:
http://localhostAccess-Control-equest-Method:
Access-Control-Request-Headers:
appears....are the correct headers not set !!
Posted by rohitrox
on Apr 22, 2013 at 12:48 PM UTC - 6 hrs
now i have an idea. thankyou
Posted by teche
on Jun 22, 2016 at 03:29 AM UTC - 6 hrs
Leave a comment