Cross-domain request with Rails + Angular.js
Published:
I was playing around with AngularJs recently and decided to integrate it with Rails. The goal is to have total separation between front-end and back-end. Back-end will act as a RESTful service that spits JSON. Both front-end and back-end will be served from different server and will have different domain, so there are few options to achieve this. Whenever you are doing cross domain request, you will be subjected to same-origin policy. Options
- JSON-P
At first, I did it using JSON-P but I don't like having a callback in the url. I prefer to keep it clean and there is also an issue of security when doing JSON-P like XSS. Fortunately, it was quite easy to implement JSON-P in both Rails and Angular. I don't need to have special treatment whether I'm sending a GET
request or POST
request. Both will be treated the same.
The rails server must be able to receive the callback.
# assuming this is the controller that will receive the request
def index
render :json => @posts, :callback => params[:callback]
end
and in controller.js
in angular app
$http.jsonp('http://ubuntu.dev:3000/posts?callback=JSON_CALLBACK').success(function(data) {
$scope.posts = data;
});
notice that the callback must be JSON_CALLBACK
for it to work.
- CORS
CORS is the way to go, but not all browsers support this and like all thing in web development, there is inconsistency of support (feature).
- Proxy-server
Don't know shit about the implementation.
CORS Implementation
In order to understand CORS better, google and read a lot about CORS. To summarize, CORS can be divided into two - 1. Simple request 2. Not-so-simple/Complex request.
There are criteria regarding how thing fall into either one of these categories. In short, if you're dealing with standard headers, plain/text content-type, and sending a GET
request, chances are it will be simple request and things will be easy. Otherwise, it will be a complex request.
Complex Request
When doing complex request, the browser will send a preflight
request to the server before sending an actual request. If the server is okay with it, browser can proceed with sending an actual request. The preflight
request is sent through HTTP OPTIONS
request. So it is important to react to this request.
Server Side
Because not all servers can respond to OPTIONS
request, you need to implement this. In Rails, you will usually do this in routes.rb
resources :posts
and this will create routes that correspond to RESTful verb like GET, POST, PUT, PATCH, and DELETE. But, it doesn't cover OPTIONS, you will need to this manually. So, add this in routes.rb
to support OPTIONS
verb.
match '/posts' => 'posts#options', :constraints => {:method => 'OPTIONS'}, via: [:options]
Basically, this is saying to Rails that "Hey, if someone send an OPTIONS request to /posts, please goto posts controller and execute the options action". Then, the action in the controller must now act to this request. Usually what we need to do is check if the domain is whitelisted and we can send an OK signal to proceed with the actual request. For simplicity sake, I would just allow everything.
posts_controller.rb
class PostsController < ApplicationController
# we want to set the headers before executing any action
before_filter :set_headers
protect_from_forgery with: :null_session
def create
@post = Post.new
@post.title = params[:title]
@post.text = params[:text]
if @post.save
render :json => { :status => "success", :message => @post}, :status => 200
else
render :json => { :status => "error", :message => "Unable to save" }, :status => 422
end
end
def options
set_headers
# this will send an empty request to the clien with 200 status code (OK, can proceed)
render :text => '', :content_type => 'text/plain'
end
private
# Set CORS
def set_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Expose-Headers'] = 'Etag'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD'
headers['Access-Control-Allow-Headers'] = '*, x-requested-with, Content-Type, If-Modified-Since, If-None-Match'
headers['Access-Control-Max-Age'] = '86400'
end
end
Access-Control-Allow-Origin
is where we control who can have the access. Also note that Access-Control-Allow-Headers
is set to x-requested-with
. This is usually used when using an AJAX request. We do not want the server to reject the request. If we set this here in the server, we won't need to delete the header when sending the request from the client. You'll see this in a client-side code in a bit.
Client-side
For your information, I'm just using the seed project template provided by Angular. Nothing fancy.
controllers.js
angular.module('myApp.controllers', []).
controller('MyCtrl1', ['$scope', '$http',function($scope, $http) {
$http.get('http://ubuntu.dev:3000/posts').success(function(data) {
$scope.posts = data;
});
}])
.controller('MyCtrl2', ['$scope', '$http', function($scope, $http) {
// Don't need this if you have allow 'X-Requested-With' inside
// 'Access-Control-Allow-Headers' header in the server
// $http.defaults.useXDomain = true;
// delete $http.defaults.headers.common['X-Requested-With'];
$scope.submit = function() {
console.log($scope.review);
$http({
method: 'POST',
url: 'http://ubuntu.dev:3000/posts',
// this data is passed as JSON
// I tried with form url encoding, but the server side receive the value as null
// Probably it doesn't know how to read those value, but received the request so assigned as null
data: $scope.review,
})
.success(function(response) {
// Do something when success
});
};
}]);
partial2.html
<form name="review_form" ng-submit="submit()" >
Title: <input type="text" ng-model="review.title"/><br />
Text: <input type="text" ng-model="review.text"/><br />
<input type="submit" />
</form>
Notice that if compared to JSON-P, this method is much cleaner as there is no callback anymore, although a bit more involved.
Gems
Of course like all things in Rails, there is gem for that. You can use this middleware to handle CORS in Rails, Rack-Cors. And if you're developing API only in Rails, you can use Rails-API. It removes all the unnecessary things, just focus on API support.
I believe in understanding the basic of the inner workings of anything before using it. Of course, not trying to re-invent the wheel here but that's how I learn a lot. YMMV.
I'll try to post the link for the project in github later.