Angular $http and Rails 4 params - ruby-on-rails

I'm using the rails-api gem to have just a Rails API and using Angular to power my frontend. Whenever I use $http, it will only work if I pass in params instead of data. Here's an example with trying to log in a user and create a new session:
'use strict';
app.controller('LoginCtrl', function($scope, $location, $http, tokenHandler) {
$scope.login = function() {
$http({
url: 'http://localhost:3000/api/admins/sign_in',
method: 'POST',
params: $scope.admin
}).success(function(data) {
if (data.success) {
$scope.ngModel = data.data.data;
tokenHandler.set(data.data.auth_token);
$location.path('/admin/blog');
} else {
$scope.ngModel = data;
$scope.user.errors = data.info;
}
}).error(function(msg) {
$scope.admin.errors = 'Something is wrong. Please try again.';
});
};
});
If instead of params I used data: { admin: $scope.admin }, Rails complains to me that params[:admin] is nil. It seems to not be coming through at all.
However, if I use params, I get this:
Started POST "/api/admins/sign_in?email=raderj89#gmail.com&password=[FILTERED]" for 127.0.0.1 at 2014-09-07 20:08:04 -0400
Processing by Admin::SessionsController#create as HTML
Parameters: {"email"=>"raderj89#gmail.com", "password"=>"[FILTERED]"}
Which I can work with. It's just weird that it seems to only work when the request is processed as HTML. When I use data, I get this:
Started OPTIONS "/api/admins/sign_in" for 127.0.0.1 at 2014-09-07 20:36:24 -0400
Processing by Admin::SessionsController#create as */*
Is it suppose to say processing by */*? I'd think it should understand it's supposed to process by json specifically.
My sessions controller looks like this:
class Admin::SessionsController < Devise::SessionsController
skip_before_filter :verify_authenticity_token
before_filter :authenticate_user!, except: [:create]
respond_to :json
# ...
end
The weird thing is I definitely got it working the first time just using data: { admin: $scope.admin }, but ever since, the params seem to never come through unless I use params: $scope.admin.
ALSO:
I'm using Devise for authentication, and I had to add this to my ApplicationController:
class ApplicationController < ActionController::API
include ActionController::MimeResponds
before_filter :set_cors_headers
before_filter :cors_preflight
private
def set_cors_headers
headers['Access-Control-Allow-Origin'] = AppConfig.client['origin']
headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
headers['Access-Control-Allow-Headers'] = '*'
headers['Access-Control-Max-Age'] = "3628800"
end
def cors_preflight
head(:ok) if request.method == :options
end
end
Anyone ever dealt with this before?

I've finally got it working and while I'm still confused, I think I've got somewhere close to what the problem was: My CORS configuration in my Rails API.
From what I've learned, Angular sends data in JSON format by default. This goes through as "Content-Type:application/json;charset=UTF-8", whereas in jQuery AJAX requests, it goes through as "Content-Type:application/x-www-form-urlencoded; charset=UTF-8", and is converted to a query string using $.param(). I'll admit, I've probably heard this before, but haven't truly registered this fact and its effects until now.
In my application controller, I configured my CORS settings like so:
def set_cors_headers
headers['Access-Control-Allow-Origin'] = AppConfig.client['origin']
headers['Access-Control-Allow-Methods'] = 'GET,POST,PUT,DELETE,OPTIONS'
headers['Access-Control-Allow-Headers'] = '*'
headers['Access-Control-Max-Age'] = "3628800"
end
def cors_preflight
head(:ok) if request.method == :options
end
AppConfig is just an OpenStruct that tells my Rails API what origin to accept requests from. And then everything else was supposed to simply set the CORS headers.
For some reason of which I'm still not sure, this wasn't working for JSON requests. I got the above code from a tutorial using Angular and Rails, and in the case of the tutorial, they manually stripped out the asset pipeline, leaving everything else about Rails in, whereas rails-api strips out some Rails configuration. This may be why setting the CORS headers in ApplicationController wasn't working.
What did work was to use the rack-cors gem and then add this bit to development.rb:
config.middleware.use Rack::Cors do
allow do
origins 'localhost:9000'
resource '*', :headers => :any, :methods => [:get, :post, :options, :delete]
end
end
This tells my app to accept requests from localhost:9000, and to accept any headers. I thought I was accomplishing that with headers['Access-Control-Allow-Headers'] = '*' in my ApplicationController, but I guess not. Once I specified Rails to use those middleware settings, everything worked perfectly. My Rails API can now accept application/json from my Angular app.
If someone could fill in the gaps where I'm still confused, I'd appreciate it. But I hope this helps others.

You can send either :params or :data (or both, I guess). According to the angularjs docs at https://docs.angularjs.org/api/ng/service/$http
params – {Object.} – Map of strings or objects which
will be turned to ?key1=value1&key2=value2 after the url. If the value
is not a string, it will be JSONified.
data – {string|Object} – Data to be sent as the request message data.
The controller is expecting http-type parameters/form data, so passing the object via params works - it gets converted, whilst passing the same via :data doesn't because it doesn't get converted.
I don't know if there is a smart way to unpack the data format at the Rails Controller end, but you can convert the object within your $http request into serialized parameters using $.param(data)http://api.jquery.com/jQuery.param/
data: $.param($scope.your_data_object) e.g. $scope.admin
and then unpack params[:data] at the controller.

Related

Ruby on Rails: How to Permit parameters with "_json"

I am trying to get POST requests to work coming from a React.js application to a Ruby on Rails API.
The parameters are:
Parameters: {"_json"=>"{'Name': 'ExampleSurvey', 'Draft_Status': 'true', 'Active_Status': 'false' }", "survey"=>{}}
My survey_params method is:
def survey_params
params.permit(:Name, :Draft_Status, :Active_Status)
end
My API Call from React is:
const post = (endpoint, body) => {
const url = ANAMNESIS_CONFIG.backend.location + endpoint ?? '';
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Anamnesis-Secret': ANAMNESIS_CONFIG.backend.secret
},
body: JSON.stringify(body)
}).then(response => response.json())
};
const submitHandler = e => {
e.preventDefault();
console.log({ surveyDetails })
post('survey',`{'Name': '${surveyDetails.name}', 'Draft_Status': 'true', 'Active_Status': 'false' }`)
}
The following Curl Request allows me to enter new surveys without any issues:
curl -X POST -d "Name=Example&Draft_Status=true&Active_Status=false" http://localhost:3000/survey
How can I edit Rails or React to get the Post request to work properly in a way that allows the curl request to still work?
Update:
Here is a picture from the logs: The first request is from Curl. The second request is from React. Hope this helps make the error more clear. Thanks for the help so far.
[
Changed the POST body from a string to a JSON object. This caused Rails to accept the params.
post('survey',{Name: surveyDetails.name, Draft_Status: true, Active_Status: false})
Shoutout to max (https://stackoverflow.com/users/544825/max) for coming up with solution!
In your controller you might want to skip forgery protection for json requests. It would be useful if you'd share a part of your logs where POST from React happens, otherwise it's hard to tell what the reason could be. This works when you get Can't verify CSRF token authenticity for your POST requests.
class ApplicationController < ActionController::Base
protect_from_forgery unless: -> { request.format.json? }
end

No route matches [GET] on a post request

I started Ruby on Rails few days ago, and I'm struggling with routing.
Indeed, I would like to make a post request through my routes.rb, but I keep having a
No route matches [GET] "/orders/refresh"
error.
Here is my routes.rb :
# frozen_string_literal: true
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
get '/orders', to: 'orders#index'
get '/orders/active(/:q)', to: 'orders#active'
post '/orders/refresh', to: 'orders#refresh'
end
and here is my controller (orders_controller.rb) :
# frozen_string_literal: true
class OrdersController < ApplicationController
def index
#orders = Order.order(:departure_date).all
render json: #orders.to_json
end
def active
if !params[:q]
#orders = Order.order(:departure_date).where(active: true)
else
#orders = Order.order(:departure_date).where("reference = ? OR client_name = ? OR departure_city = ? OR arrival_city = ?",
params[:q], params[:q], params[:q], params[:q])
.where(active: true)
end
render json: #orders.to_json
end
def refresh
response = RestClient.get 'https://wakeo-technical-test.s3.eu-west-3.amazonaws.com/api.json'
json = JSON.parse response
if !json.nil?
json.each do |order|
old_order = Order.find_by(reference: order["client_number"])
if !old_order.nil?
old_order.update(departure_date: order["dep_time"])
old_order.update(arrival_date: order["arr_time"])
old_order.update(client_name: order["company"])
old_order.update(departure_city: order["dep_city"])
old_order.update(arrival_city: order["arr_city"])
end
end
else
puts "error seeding external API"
end
end
end
From what I have understood, it seems like RoR will try to find a GET request for that specific URL, and since it won't find any, it will throw that error. How could I make that request be a POST for Rails ?
Also, I would appreciate any suggestion about how I should use ActiveRecord Querying, I'm pretty sure I could do it better here.
Thanks, have a great day !
EDIT : Here is the list of different routes my app seems to be capable of, including my POST.
Routes and error
The most common reason you unexpectly get GET requests instead of PUT, PATCH, POST or DELETE is that you are using link_to 'Something', '/some_path', method: :post and you broke the Rails Unobtrusive Javascript Driver (Rails UJS):
Because submitting forms with HTTP methods other than GET and POST
isn't widely supported across browsers, all other HTTP methods are
actually sent over POST with the intended method indicated in the
_method parameter. Rails automatically detects and compensates for this.
Rails does that with a JavaScript event handler attached to any link with the data-method attribute. But if you broke that functionality the browser will just perform its default action which is sending a GET request when the user clicks a link.
This problem usually boils down to one or more of:
Your javascript is throwing an error which halts script execution (use the browser console to find the error, make it suck less).
Rails UJS is not included in your assets pipeline or webpacker packs and thus not in the page.
The quick and easy solution to sidestep the problem is by using button_to which actually creates a form and does not require any JavaScript trickery. After all forms can send POST requests. And by just passing a _METHOD hidden field Rack will treat the request as any other HTTP verb.
button_to 'Something', '/some_path', method: :post
But in the long run you should probably fix the problem if you want to use any of the features of Rails UJS.
Your routes.rb is expecting a POST request to /orders/refresh routes, but apparently you are testing with a GET request.
Try changing your routes.rb:
Rails.application.routes.draw do
# ...
get '/orders/refresh', to: 'orders#refresh'
end
... or change your request to a POST request. If you are using Rails forms, you must do something like this:
form_with(url: "/orders/refresh", method: "post")
Ok, I think I figured it out.
It might be because when I hit /orders/refresh directly in my web browser, it will try to find a GET corresponding to the request.
I managed to make POST using a client like Postman, and everything works fine.
Thank you for your help !

Rails DELETE route not working properly / not calling the controller function

I am using React and Rails API. To make a request from React I am using Axios library and the code looks like this:
const _deleteIssue = () => {
axios.delete(`${ROOT_API}/v1/issue/delete/${props.issue.id}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
}).then(res => {
props.updateProjectData()
}).catch(err => {
console.log(err)
})
}
In order to handle this request I have set up a route for it which looks like this:
Rails.application.routes.draw do
concern :base_api do
# other routes
post 'issues/create', to: 'issues#create'
delete 'issue/delete/:issue_id', to: 'issues#delete_issue'
end
namespace :v1 do
concerns :base_api
end
end
As you can see from the code snippet above, the route is set to call delete_issue function inside of the issues controller. I have created that function and it looks like this:
class V1::IssuesController < ApplicationController
# other functions
def delete_issue
Issue.find_by(id: params[:issue_id]).delete
render json: {}, status: 200
end
end
I am justing trying to find the issue with an id that is passed as params from Axios delete request.
It is supposed to delete it and return nothing with a status code of 200. What happens instead is that in my "Network" tab inside of developer tools in my browser(Firefox) shows the 200 OK request with the OPTIONS method. There is no DELETE method being sent or anything.
I even tried to comment out the delete_issue function from IssuesController and there was not 404 routing error. The result was the same. I can't find out what is wrong with it even though it is probably a pretty obvious error which I can't see.
Note that I am using Rails 6.
It seems you did not configure rack-cors. Simply add this to your cors.rb file:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'localhost:3001' # or you react app domain
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end

ActionController::InvalidCrossOriginRequest exception due to bingbots

I have rails applications where I am loading comments using Ajax after page load.
class CommentsController < ApplicationController
respond_to :js
def index
#comments = Comments.all
respond_with #comments
end
end
It is working as expected. But bingbot is trying to access this url with which it leads to
An ActionController::InvalidCrossOriginRequest occurred in comments#index:
Security warning: an embedded tag on another site requested protected JavaScript. If you know what you're doing, go ahead and disable forgery protection on this action to permit cross-origin JavaScript embedding.
like that it is coming for all url's which are only responding to js format.
I know about rack-cors, but it is for allowing cross side script access, but here it is not.
app/views/comments/index.js.erb
$('.comments_container').html("<%=j render 'comments' %>");
comments.js
jQuery(function() {
return $.ajax({
url: $('.comments_container').data('url')({
dataType: "script"
})
});
});
Assuming you need some help with CORS(Cross-origin resource sharing), You are getting error because your CORS policy is default to "denying" every direct XHR access.
You can use the rack-cors gem https://github.com/cyu/rack-cors to avoid this. Hope this help!

How to implement redirect response with an AJAX request in Rails 3?

I have a simple scenario where I want to request a page. The request format is AJAX. If there is some error in the controller/action logic for that request, I want to redirect to an error page. The issue is that a redirect is not a JavaScript response type, so I am not sure whether it will work.
If there are no errors, then I want the page to be updated via the appropriate JavaScript response type.
What is best practice to achieve redirect responses given that the request format is AJAX?
This blog post enlightened me on what I think is the right way to do this, if your ajax response is ajax; at least, in the unobtrusive javascript paradigm. In essense, the ajax call always returns a standard json package, which can be parsed for information payload or a redirect url.
You can also put this in your ApplicationController to redirect properly for AJAX requests:
class ApplicationController < ActionController::Base
# Allows redirecting for AJAX calls as well as normal calls
def redirect_to(options = {}, response_status = {})
if request.xhr?
render(:update) {|page| page.redirect_to(options)}
else
super(options, response_status)
end
end
end
If you use jQuery, you can use .ajaxError()
$(document).ajaxError(function(event, request, settings){
location.href = '/error.html';
});
or assume you do a ajax post
var jqxhr = $.post("example.php", function() {
// Do what it completed will do
})
.error(function() {
location.href = '/error.html';
})

Resources