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
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
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.
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.
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.
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
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.
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
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...
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:
add the following to Gemfile:
gem 'rack-cors', :require => 'rack/cors'
run "bundle install", which will update Gemfile.lock
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With