Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending an Angular post request with parameter to Rails API

I´m not able to completely integrate an Angular front-end with a Rails back-end API. They both run in differente servers, so I think I have problems with CORS.

My Angular app is running a controller that is calling a service that has resource with a query (GET) and save (POST) methods. The query (GET) is working fine, however the post is not working.

I´m able to send a POST request to the server when I don´t send any parameter. Like this:

Controller:

$scope.createBusiness = function() {

        console.log("Business.name=" + $scope.business.name);           
        $scope.business = Business.save(); 
    };

Service:

 .factory('Business', 

  function($resource){          
    var businesses =
     $resource('http://127.0.0.1\\:3000/:business', {business:'businesses'}, {      
        query: {method:'GET', isArray: true},
        save: {method:'POST', isArray: false}
     });        
     return businesses;           
  }

);

However, I want to post my model parameters, so when I try to send something then I don´t send a POST request anymore, but an OPTIONS request. And I get an error.

Please, see my request data when I send a request without parameters (POST request):

Request URL:http://127.0.0.1:3000/businesses
Request Method:POST
Status Code:200 OK
Request Headersview source
Accept:application/json, text/plain, */*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:es-ES,es;q=0.8
Connection:keep-alive
Content-Length:0
Content-Type:text/plain;charset=UTF-8
Host:127.0.0.1:3000
Origin:http://localhost:1234
Referer:http://localhost:1234/app/index.html
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36
Response Headersview source
Access-Control-Allow-Headers:Origin, X-Requested-With, Content-Type, Accept, Authorization
Access-Control-Allow-Methods:POST, PUT, DELETE, GET, OPTIONS
Access-Control-Allow-Origin:*
Access-Control-Max-Age:1728000
Access-Control-Request-Method:*
Cache-Control:max-age=0, private, must-revalidate
Connection:Keep-Alive
Content-Length:9
Content-Type:application/json; charset=utf-8
Date:Mon, 04 Nov 2013 16:50:33 GMT
Etag:"ccd3d779b6f97e2c24633184cbc8f98c"
Server:WEBrick/1.3.1 (Ruby/2.0.0/2013-06-27)
X-Content-Type-Options:nosniff
X-Frame-Options:SAMEORIGIN
X-Request-Id:e084295e-c7c6-4566-80d1-6e2a8ac2e712
X-Runtime:0.034000
X-Ua-Compatible:chrome=1
X-Xss-Protection:1; mode=block

I reach the server, execute the method and get the response! This is ok.

and, see my request data when I send a request WITH parameters (OPTIONS request):

Request URL:http://127.0.0.1:3000/businesses
Request Method:OPTIONS
Status Code:404 Not Found
Request Headersview source
Accept:*/*
Accept-Encoding:gzip,deflate,sdch
Accept-Language:es-ES,es;q=0.8
Access-Control-Request-Headers:accept, content-type
Access-Control-Request-Method:POST
Connection:keep-alive
Host:127.0.0.1:3000
Origin:http://localhost:1234
Referer:http://localhost:1234/app/index.html
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36
Response Headersview source
Connection:Keep-Alive
Content-Length:131852
Content-Type:text/html; charset=utf-8
Date:Mon, 04 Nov 2013 16:54:04 GMT
Server:WEBrick/1.3.1 (Ruby/2.0.0/2013-06-27)
X-Request-Id:25705159-fbfb-4830-a0f1-6610fa09b70e
X-Runtime:0.371000

UPDATE

I forgot to add my controller when adding a model parameter:

$scope.createBusiness = function() {

        console.log("Business.name=" + $scope.business.name);           
        $scope.business = Business.save($scope.business); 
    };

I have several views, with several forms, so, I don´t want only to post a form, but the business object model that I have in scope (and I filled with the data of all the forms).

UPDATE

This is my Rails Application_Controller (CORS configuration):

class ApplicationController < ActionController::Base


 # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  # OJOJOOJO: Rober: I have commented this line which is provided by default with rails and added all code below in order to 
  # add CSRF protection 
  #protect_from_forgery with: :exception

  protect_from_forgery

  before_filter :cors_preflight_check
  after_filter :cors_set_access_control_headers, :set_csrf_cookie_for_ng

# For all responses in this controller, return the CORS access control headers.

  def cors_set_access_control_headers
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
    headers['Access-Control-Request-Method'] = '*'
    headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
    headers['Access-Control-Max-Age'] = "1728000"
  end

# If this is a preflight OPTIONS request, then short-circuit the
# request, return only the necessary headers and return an empty
# text/plain.

  def cors_preflight_check
    if request.method == :options
      headers['Access-Control-Allow-Origin'] = '*'
      headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
      headers['Access-Control-Request-Method'] = '*'
      headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
      headers['Access-Control-Max-Age'] = '1728000'
      render :text => '', :content_type => 'text/plain'
    end
  end

  def set_csrf_cookie_for_ng
    cookies['XSRF-TOKEN'] = form_authenticity_token if protect_against_forgery?
  end

protected

  def verified_request?
    super || form_authenticity_token == request.headers['X_XSRF_TOKEN']
  end  
end
like image 623
Rober Avatar asked Nov 04 '13 16:11

Rober


2 Answers

I finally made it work.

I will explain all my config in Angular and Rails 4 as I would have liked to read it.

Angular

  1. app.js:

    angular.module('myApp', ['myApp.filters', 'myApp.services', 'myApp.directives', 'myApp.controllers', 'myApp.i18n', 'demo']).
    config(['$routeProvider', '$httpProvider', function($routeProvider, $httpProvider) {
    
    $httpProvider.defaults.useXDomain = true;
    delete $httpProvider.defaults.headers.common["X-Requested-With"];           
    

    }]);

Note: Of course, you don´t need to include all my modules.

  1. In my view:

    <form novalidate="" name="createBusinessForm" ng-submit="setBusinessInformation()" class="css-form">            
            <label>* {{'BUSINESS_NAME' | translate}}</label>
            <input type="text" name="name" ng-model="business.name" class="input-xxlarge input-height-large" placeholder="{{'BUSINESS_NAME_PLACEHOLDER' | translate}}" required maxlength="80">
            <span ng-show="createBusinessForm.name.$dirty && createBusinessForm.name.$error.required" class="text-error">Mandatory field.</span>
            <label>* {{'ID' | translate}}</label>    
            <input type="text" ng-model="business.cif_nif" class="input-xlarge input-height-large" placeholder="{{'BUSINESS_ID_PLACEHOLDER' | translate}}" required maxlength="60">        
            <label>* {{'ADDRESS' | translate}}</label>    
    

Note: You can define as many fields as you need and with data bingind assign the values to an object.

  1. in my controller:

    $scope.createBusiness = function() {
    
            $scope.business.type = $scope.type; 
            $scope.business.plan = $scope.plan;     
            $scope.business = Business.save($scope.business);           
            $location.path('/user-dashboard');                              
        }; 
    

Note: All the attributes you need to send in the post request. Apart from the form, you can assign some new attributes to the object before sending to Rails API. We will use a service with a resource object to send the POST request.

  1. In my service:

    .factory('Business',

      function($resource){          
        var businesses =
         $resource('http://127.0.0.1\\:3000/:business', {business:'businesses'}, {      
            query: {method:'GET', isArray: true},
            save: {method:'POST', isArray: false}
         });        
         return businesses;           
      }
    

    );

Note: I have a GET request to get the business from DB through Rails API and the POST one.

Rails 4

IMPORTANT

  1. routes.rb

    match "/businesses" => "application#index", via: :options  
    

Note: New entry to match the OPTIONS request the Angular server will send to pre-negociate the start of sending the POST request.

  1. application_controller.rb

    class ApplicationController < ActionController::Base
    
    
    before_filter :set_headers
    
    def index
      puts "Do nothing."
      render nothing: true
    end
    
    def set_headers
      puts 'ApplicationController.set_headers'
      if request.headers["HTTP_ORIGIN"]     
      # better way check origin
      # if request.headers["HTTP_ORIGIN"] && /^https?:\/\/(.*)\.some\.site\.com$/i.match(request.headers["HTTP_ORIGIN"])
        headers['Access-Control-Allow-Origin'] = request.headers["HTTP_ORIGIN"]
        headers['Access-Control-Expose-Headers'] = 'ETag'
        headers['Access-Control-Allow-Methods'] = 'GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD'
        headers['Access-Control-Allow-Headers'] = '*,x-requested-with,Content-Type,If-Modified-Since,If-None-Match,Auth-User-Token'
        headers['Access-Control-Max-Age'] = '86400'
        headers['Access-Control-Allow-Credentials'] = 'true'
      end
    end               
    end
    
  2. My Rails controller

    def create
      puts 'Businesses_controller.create!!!!!'
      puts business_params.inspect
    
      # business_type object is recovered from db
      businessTypeName = params[:type]    
      businessType = BusinessType.where(:name => businessTypeName).first
      ...
    end
    

Note: Here do whatever you need...

like image 120
Rober Avatar answered Oct 14 '22 07:10

Rober


We had the same problem and took a similar but hopefully simpler/quicker/more flexible approach.

The quick rundown of what I did was use to the ruby library "rack-cors" (https://github.com/cyu/rack-cors) to manage all the CORS headers.

This means I didn't have to stick a bunch of hardcoded header name/values in my code.

The big benefit for me was that this takes care of both simple CORS requests (GET request and response) and preflighted requests that use OPTION requests (OPTIONS request and response and then POST request and response).

Here are the quick steps that I followed:

  1. gem install rack-cors
  2. add the following to Gemfile:

    gem 'rack-cors', :require => 'rack/cors'
    
  3. run "bundle install", which will update Gemfile.lock

  4. edit config/application.rb to add the following block:

    config.middleware.insert_before "ActionDispatch::Static", "Rack::Cors", :debug => true, :logger => Rails.logger do
      allow do
        origins '*'
    
        resource '*',
          :headers => :any,
          :methods => [:get, :post, :delete, :put, :options],
          :max_age => 0
      end
    end
    

Now in my case, I just wanted to open this up to any host, but you could be more restrictive. You can also limit headers and http methods too.

See more details on readme at the github page: https://github.com/cyu/rack-cors (The rack-cors author has example rails apps under examples/rails3 and examples/rails4)

like image 27
Matt Coarr Avatar answered Oct 14 '22 05:10

Matt Coarr