Ruby on Rails: How to Permit parameters with "_json" - ruby-on-rails

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

Related

Rails use of httparty to parse the JSON to external URL

In a Rails application, a user has to be redirected to an external site for external processing. The controller generates a valid JSON string and a set of headers:
#result = HTTParty.post(
"https://some-test.supertester.com/checkout",
:verify => false,
:body => JSON.parse(#payment).to_json,
headers: {
'Content-Type' => 'application/json',
'X-TimeStamp' => "#{Time.now.to_i}",
'X-API-ID:' => "#{#api_id}",
'X-API-Signature:' => "#{#api_signature}"
},
timeout: 20
)
This action is launched by the user via:
<%= link_to t('proceed'), data_url_cart_path(id: #existing_cart.id), class: 'button' %>
whose controller action generates the above JSON string and call.
However, Rails tries to respond to this action with a view with the same action's name (even generating a blank with format.json { head :no_content }), when the goal is to redirect the user to the aforementioned URL with the defined headers and payload.
How can this be achieved?
HTTParty is making a network request with a json payload. In this situation #response will be the response from the network request. If you call puts #response.parsed_response.inspect you will see the json returned with said response in the terminal window running your Rails server.
I think you may not need to redirect the user to an external site at all, but instead check the response to see if the action you were trying to make was successful and handle it in your own application.

Angular $http and Rails 4 params

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.

WARNING: Can't verify CSRF token authenticity

I'm having trouble making a cross-domain request from my shopify site to my rails app, which is installed as a shopify app. The problem, as stated in the title, is that my server warns me that it Can't verify CSRF token authenticity I'm making the request from a form returned by my rails app, which includes the relevant CSRF token. The request is done with jQuery's ajax method, and the preflight OPTIONS request is being handled by rack-cors.
I've included the X-CSRF-Token in my headers as was suggested in this answer. My post request is being made from a form, so my question is not answered here. The options request (mentioned in this question) is indeed being handled, as I just confirmed by asking this question. I've been stuck on this for a while, and have done a bit of reading.
I'm going to try walking through the process code-snippet by code-snippet, and maybe by the time I finish writing this post I will have discovered the answer to my problem (if that happens, then you won't ever get a chance to read this paragraph).
Here are the new and create methods from my controller.
class AustraliaPostApiConnectionsController < ApplicationController
# GET /australia_post_api_connections/new
# GET /australia_post_api_connections/new.json
def new
# initializing variables
respond_to do |format|
puts "---------------About to format--------------------"
format.html { render layout: false } # new.html.erb
format.json { render json: #australia_post_api_connection }
end
end
# POST /australia_post_api_connections
# POST /australia_post_api_connections.json
def create
#australia_post_api_connection = AustraliaPostApiConnection.new(params[:australia_post_api_connection])
respond_to do |format|
if #australia_post_api_connection.save
format.js { render layout: false }
else
format.js { render layout: false }
end
end
end
end
(I wonder about the respond_to block in the create method, but I don't think that would cause the CSRF token to fail verification.)
Within my app, at /AUSController/index, I have an ajaxified GET request that brings back the form from /AUSController/new. My goal is to be able to make all the same calls from a cross-domain origin as I can from within my app. Right now the GET request works for both, and so I will neglect to include the 'new' form. When the HTML is finally rendered, the form element has the following:
<form method="post" id="new_australia_post_api_connection" data-remote="true" class="new_australia_post_api_connection" action="http://localhost:3000/australia_post_api_connections" accept-charset="UTF-8">
<!-- a bunch more fields here -->
<div class="field hidden">
<input type="hidden" value="the_csrf_token" name="authenticity_token" id="tokentag">
</div>
</div>
</div>
</form>
The CSRF token is generated by a call to form_authenticity_token as detailed in one of the references mentioned above.
The next step is done differently in the two cases:
My app successfully returns the new form to the shop upon an ajax request. I've tested this within the app, that is by making an ajax call to /controller/new from /controller/index, and then submitting the form. This works like a charm. The js that is returned from a successful POST within my app is as follows:
/ this is rendered when someone hits "calculate" and whenever the country select changes
:plain
$("#shipping-prices").html("#{escape_javascript(render(:partial => 'calculations', :object => #australia_post_api_connection))}")
Which renders the following partial,
= form_tag "/shipping_calculations", :method => "get" do
= label_tag :shipping_type
%br
- #service_list.each_with_index do |service, index|
- checked = true if index == 0
= radio_button_tag(:shipping_type, service[:code], checked)
= label_tag(:"shipping_type_#{service[:code]}", service[:name])
= " -- $#{service[:price]}"
%br
When I call it from the same domain, request.header contains the following:
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{
"session_id"=>"db90f199f65554c70a6922d3bd2b7e61",
"return_to"=>"/",
"_csrf_token"=>"the_token_I_expect=",
"shopify"=>#<ShopifyAPI::Session:0x000000063083c8 #url="some-shop.myshopify.com", #token="some_token">
}
And the HTML is rendered and displayed nicely.
From the cross domain source, however, things are understandibly more complicated. This is where CORS and CSRF tokens and routes and all these little details start creeping in. In particular, when I make the ajax call I use the following script (which does not live in my rails app, it lives on the cross-domain server). The action of this ajax request is attached to the submit button by the callback function from the GET request, and I've included the GET request for the sake of completion.
<script>
var host = "http://localhost:3000/"
var action = "australia_post_api_connections"
console.log("start")
$.ajax({
url: host + action,
type: "GET",
data: { weight: 20 },
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function(data) {
console.log("success");
$('#shipping-calculator').html(data);
$('#new_australia_post_api_connection')
.attr("action", host + action);
$('.error').hide();
$(".actions > input").click(function() {
console.log("click")
// validate and process form here
$('.error').hide();
var to_postcode = $("input#australia_post_api_connection_to_postcode").val();
// client side validation
if (to_postcode === "") {
$("#postcode > .error").show();
$("input#australia_post_api_connection_to_postcode").focus();
return false;
}
tokentag = $('#tokentag').val()
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
// included based on an SO answer
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-TOKEN', tokentag);
}
});
$.ajax({
type: "POST",
url: host + action,
data: dataHash,
success: function(data) {
$('#shipping-prices').html(data);
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
return false;
});
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
$(function() {
});
</script>
However, when I call it from this remote location (the distant slopes of Shopify), I find the following in my request headers,
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{ }
And I receive a very unpleasant NetworkError: 500 Internal Server Error rather than the 200 OK! that I would like... On the server side we find the logs complaining that,
Started POST "/australia_post_api_connections" for 127.0.0.1 at 2013-01-08 19:20:25 -0800
Processing by AustraliaPostApiConnectionsController#create as */*
Parameters: {"weight"=>"20", "to_postcode"=>"3000", "from_postcode"=>"3000", "country_code"=>"AUS", "height"=>"16", "width"=>"16", "length"=>"16", "authenticity_token"=>"the_token_I_expect="}
WARNING: Can't verify CSRF token authenticity
Completed 500 Internal Server Error in 6350ms
AustraliaPostApiConnection::InvalidError (["From postcode can't be blank", "The following errors were returned by the Australia Post API", "Please enter Country code.", "Length can't be blank", "Length is not a number", "Height can't be blank", "Height is not a number", "Width can't be blank", "Width is not a number", "Weight can't be blank", "Weight is not a number"]):
app/models/australia_post_api_connection.rb:78:in `save'
The lack of a rack.session seems suspicious like the cause of my misery... but I haven't been able to find a satisfying answer.
Finally I have seen fit to include my rack-cors setup, in case it is useful.
# configuration for allowing some servers to access the aus api connection
config.middleware.use Rack::Cors do
allow do
origins 'some-shop.myshopify.com'
resource '/australia_post_api_connections',
:headers => ['Origin', 'Accept', 'Content-Type', 'X-CSRF-Token'],
:methods => [:get, :post]
end
end
Thank you so much for even reading all of this. I hope the answer has to do with that empty rack.session. That would be satisfying, at least.
Well one of my coworkers figured it out. The problem was, the has I was sending didn't have the same structure as the hash I was expecting in my controller.
In my controller I instantiate a new API connection as follows,
AustraliaPostApiConnection.new(params[:australia_post_api_connection])
I am looking for params[:australia_post_api_connection], but there is no such index in my data hash, which looks like,
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
To fix this I changed the JS file to contain,
var dataHash = {
to_postcode: to_postcode,
}
var params = {
australia_post_api_connection: dataHash,
authenticity_token: tokentag // included based on an SO answer
}
And now it works! Thanks co-worker!

Why does Rails ignore content-type when accepts is different?

I have a RESTful controller:
class Api::V1::DevicesController < ApplicationController
before_filter :require_user
respond_to :json
# PUT /api/v1/devices/1
def update
#device = Device.find(params[:id])
authorize! :update, #device
#device.update_attributes(params[:device])
respond_with #device
end
end
And some JS on the client side:
$("#click-me").on('click', function() {
$.ajax({
type: "PUT",
url: '/api/v1/devices/' + $(this).data('device-id'),
dataType: 'json',
accept: 'application/json',
data: {device: {user_id: null}, format: 'json'},
success: function () {
alert("Booya!");
}
})
});
When the AJAX gets fired, by default, jQuery sends a JS object as a URL encoded string.
And a Content-Type:application/x-www-form-urlencoded which is fine. And because I set dataType to 'json', it sets Accept:application/json, text/javascript, */*; q=0.01, which also seems fine.
However when Rails gets the request, it treats the post body as JSON, even tho content-type is URL Encoded. Is this a bug?
My solution so far is to cast the JS object as JSON (JSON.stringify({device: {user_id: null}, format: 'json'})) and just submit it.
But that doesn't feel very good, surely Rails should handle Content-Type and Accept separately. This app is in 3.0.17. Perhaps it's an issue with earlier versions?
UPDATE:
Turns out the 422 I was receiving was a validation failure, and Rack is smart enough to deal with varying Content-Type and Accept headers. Duh.
This is maybe due to the
dataType: 'json',
line, which informs the server that the content of your request is JSON.

How to post JSON data in rails 3 functional test

I plan to use JSON data in both request and response in my project and having some problems in testing.
After searching for a while, I find the following code which uses curl to post JSON data:
curl -H "Content-Type:application/json" -H "Accept:application/json" \
-d '{ "foo" : "bar" }' localhost:3000/api/new
In the controller I can access the JSON data simply using params[:foo] which is really easy. But for functional testing, I only find post and xhr (alias for xml_http_request).
How can I write functional test in rails to achieve the same effect as using curl? Or should I do test in other ways?
Here's what I've tried. I find the implementation for xhr in action_controller/test_case.rb, and tried to add jhr method simply changing 'Conetent-Type' and 'HTTP_ACCEPT'. (Added in test/test_helpers.rb.)
def json_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
#request.env['Content-Type'] = 'Application/json'
#request.env['HTTP_ACCEPT'] ||= [Mime::JSON, Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
__send__(request_method, action, parameters, session, flash).tap do
#request.env.delete 'Content-Type'
#request.env.delete 'HTTP_ACCEPT'
end
end
alias jhr :json_http_request
I used this in the same way as xhr, but it does not work. I inspected the #response object and sees the body is " ".
I also find one similar question on Stack Overflow but it's for rails 2 and the answer for posting raw data does not work in rails 3.
As of Rails 5, the way to do this is:
post new_widget_url, as: :json, params: { foo: "bar" }
This will also set the Content-type header correctly (to application/json).
I found that this does exactly what I want – post JSON to a controller's action.
post :create, {:format => 'json', :user => { :email => "test#test.com", :password => "foobar"}}
Just specify appropriate content type:
post :index, '{"foo":"bar", "bool":true}', "CONTENT_TYPE" => 'application/json'
Json data should go as a string, not as a Hash.
Looking at stack trace running a test you can acquire more control on request preparation:
ActionDispatch::Integration::RequestHelpers.post => ActionDispatch::Integration::Session.process =>
Rack::Test::Session.env_for
Specifying :format does not work because request go as 'application/x-www-form-urlencoded' and json isn't parsed properly processing a request body.
Assuming you have a controller named api, a method named new, and you're in the test for the api controller:
#request.env["RAW_POST_DATA"] = '{ "foo" : "bar" }'
post :new
did the trick for me.
Here is a snippet that let me post json data to test my own app. rails 3
port = Rails.env.production? ? 80 : 3000
uri = URI.parse( Rails.application.routes.url_helpers.books_url(:host => request.host, :port => port, :format => :json) )
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri)
request.content_type = 'application/json'
request.body = #json_data
response = http.request( request )
#result = response.body
Hope this helps others
As #taro suggests in a comment above, the syntax that works for me in functional and integration tests is:
post :create, {param1: 'value1', param2: 'value2', format: 'json'}
(The curly braces aren't always necessary, but sometimes it doesn't work if they're missing, so I always add them.)
Here's what params and request.format look like for a post of that sort:
params:
{"param1"=>"value1", "param2"=>"value2", "format"=>"json", "controller"=>"things", "action"=>"create"}
request.format:
application/json
The best answer I can come up with to this is you don't
Whether or not it was intentional it s maybe good that rails doesn't implement this for you.
In functional tests you really want to just test your controller and not rails method of deserialization or even that routing and mime detection are all setup correctly, those all fall under an IntegrationTest.
So for your controllers, don't pass JSON just pass your params hash like you normally would. Maybe adding :format as an argument as well if you need to check that and respond differently.
If you want to test the full stack move to an IntegrationTest

Resources