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

  1. 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.

  1. 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).

  1. 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.