NoMethodError: undefined method `submission' for HTTParty - ruby-on-rails

I'm currently working on a Rails application where I am trying to submit a form to the FormStack API. The request look as follows.
This is what the requests looks like:
POST /api/v2/form/12345/submission.json HTTP/1.1
Host: www.formstack.com
Authorization: Bearer YOUR_APP_OAUTH_TOKEN
Accept: application/json
Content-Type: application/json
field_12345=Example&field_12346=Answer
I'm trying to implement that using Httparty on the library I created to make the requests to this API service.
module FormStack
class Form
include HTTParty
attr_reader :form_id
base_uri "https://www.formstack.com/api/v2"
def initialize
#access_token = ENV.fetch('FORMSTACK_ACCESS_TOKEN')
#form_id = ENV.fetch('FORMSTACK_FORM_ID')
end
def create_form
self.class.get(relative_uri, headers: headers)
end
def submission
self.class.post(create_submission_uri, headers: headers, query: query)
end
private
def relative_uri
"/form/#{#form_id}/field.json"
end
def create_submission_uri
"form/#{#form_id}/submission.json"
end
def headers
{
"Accept" => "application/json",
"Content-Type" => "application/json",
"Authorization" => "Bearer #{#access_token}"
}
end
def query
{
"field_66563890" => "blah",
"field_66563757" => "something"
}
end
end
end
controller
class FormsController < ApplicationController
def display_form
#form = FormStack::Form.new().create_form
end
def create
#form.submission
redirect_to 'localhost:3000'
end
end
This are the routes
get '/forms/display_form', to: 'forms#display_form'
post '/forms/submit', to: "forms#create"

First of all, I've got a couple general ruby things for you:
When you call FormStack::Form.new().create_form you actually don't need the () after .new -- ruby knows to call the method with no arguments even if you exclude the parens.
I'm not quite sure how you're calling FormsController::display_form from FormsController::create, but for now I'll just assume that you're using magic.
Anyways, on to my answer. As your error message states, the error is related to you calling submission on something which does not have a submission method. With that knowledge, we can look at what Object you're calling submission on in this line:
#form.submission
It looks like you're calling submission on #form. Well, let's go and look at where you declare #form:
#form = FormStack::Form.new().create_form
Let's break that declaration down into its parts. First, with FormStack::Form.new(), you're creating a new instance of FormStack::Form. So far so good. FormStack::Form has a submission method defined on it. But then, you call create_form on it. So, let's look at what create_form does:
def create_form
self.class.get(relative_uri, headers: headers)
end
create_form calls a method provided by HTTParty, get. The get method returns a HTTParty::Response Object. So, let's parse through the line where you set #form again. Broken down, what you're doing is this:
#form = FormStack::Form # This line sets the variable to a constant
#form = #form.new # This line sets the variable to be an instance of FormStack::Form
#form = #form.create_form # This line sets #form to be an instance of HTTParty::Reponse
As you can see, at the end we've set #form to an instance of HTTParty::Reponse instead of FormStack::Form, and since there's not submission method for HTTParty::Response that's why you get the error.
Based on this exploration, we can see that the fix would be to set #form to a FormStack::Form object instead, which we can do by changing the display_form action to be:
def display_form
#form = FormStack::Form.new
#form.create_form
end

Related

Overriding the default Content-Type

I have a Rails API which accepts only JSON as input. If I fail to include a header of Content-Type: application/json, then request.headers['Content-Type'] defaults to application/x-www-form-urlencoded and the params do not get parsed properly. The whole json body becomes a key in the params. The result is a 422, which is confusing to API users.
How can I change this to default to parsing as json if no Content-Type header is supplied?
Lots of other questions answer how to do this with the response format. To change this default, you can specify it in the controller with:
request.format = :json
Or in a route namespace with something like:
namespace :api, defaults: {format: :json} do
This, however, changes the default response format and does not change the default request format. What I need to do is to change the default request format for parsing parameters.
Here is my admittedly terrible solution derived from the suggestion in Micael Nussbaumer's answer. I'd love it if some Rubyists could magically turn this ugly hack into a pithy one liner.
module Api
class BaseApiController < ActionController::API
private
# This is an ugly hack needed to make it default to json if you do not
# specify a Content-Type. If you see this and know of a better way please
# say so!
def params
if !#params
if request.headers["Content-Type"]=="application/x-www-form-urlencoded"
body_string = request.body.read
begin
hash = JSON.parse(body_string)
#params = ActionController::Parameters.new(hash)
rescue
# do nothing
end
end
if !#params
#params = super
end
end
#params
end
...
end
I've solved it with middleware this way for Rails API (rails new my_project --api)
config:
# config/application.rb
# ...
require './lib/middleware/consider_all_request_json_middleware'
# ...
module MyApplication
# ...
class Application < Rails::Application
# ...
config.middleware.insert_before(ActionDispatch::Static,ConsiderAllRequestJsonMiddleware)
# ...
middleware:
# lib/middleware/consider_all_request_json_middleware.rb
class ConsiderAllRequestJsonMiddleware
def initialize app
#app = app
end
def call(env)
if env["CONTENT_TYPE"] == 'application/x-www-form-urlencoded'
env["CONTENT_TYPE"] = 'application/json'
end
#app.call(env)
end
end
original: https://blog.eq8.eu/til/content-type-applicationjson-by-default-in-rails-5.html
parsed = JSON.parse(json_body) unless request.headers["Content-Type"] == 'application/json'

Undefined method render in Rails controller - Trying to respond to Sendgrid with a 200 status code

I'm using the Sendgrid Parse API along with the Griddler gem to accept incoming emails. For the most part, this works fine; however, if you don't respond to Sendgrid with a status code 200, they will assume that the app didn't correctly receive the POST request and keep trying to make a POST for 3 days. I'm trying to respond with a status code and am having issues.
In regular RESTful routes, you can do something like...
render :status => 200
However, this must be done in a controller to recognize the render method, I believe. Griddler suggests that you create an EmailProcessor model and use a 'process' method to deal with the email.
From what I understand, you can't use the render method in models. So, I created an EmailProcessorsController class with a class method as seen below.
class EmailProcessor < ActiveRecord::Base
include ApplicationHelper
def initialize(email)
#email = email
#to = email.to # this is an array
#from = email.from
end
def process
# do other stuff
EmailProcessorsController.render_ok
end
end
class EmailProcessorsController < ActionController::Base
def self.render_ok
render :status => 200
end
end
Below is the error I get from my app. It doesn't like the render method :(
NoMethodError (undefined method `render' for EmailProcessorsController:Class):
app/controllers/email_processors_controller.rb:6:in `render_ok'
app/models/email_processor.rb:16:in `process'
I'm a newer dev and this probably something uber simple, but I'm stuck. Any thoughts and comments on the problem as well as design are greatly appreciated. Thanks!
UPDATE!
Per the suggestion of #meagar I moved the render call to the controller as seen below, but now I get a different error, and I'm not sure what to make of it.
class EmailProcessorsController < ApplicationController
def initialize(email)
#email = email
#to = email.to # this is an array
#from = email.from
end
def process
# do other stuff
render :status => 200
end
end
Without the render call, I don't get an error. Here's the error I get when calling render...
Module::DelegationError (ActionController::RackDelegation#status= delegated to #_response.status=, but #_response is nil: #<EmailProcessorsController:0x000001063b1558 #email=#<Griddler::Email:0x00000106378ac8 #params={"headers"=>"Received: by mx-007.sjc1.send
render is an instance method, not a class method. You need to instantiate an instance of your controller, but that won't work regardless.
It's a serious error to try to render from your model. The model has no idea there is an HTTP request involved. Your controller should be creating your model, invoking whatever action on it, waiting for success, and then your controller should be rendering a response. What you're trying to do is fundamentally broken.

Setting Content-Type header for RSpec and Rails-API

I'm using the rails-api gem to build a web service and want to test my API with RSpec. Every request I make, regardless of the HTTP method has the CONTENT_TYPE header set as "application/x-www-form-urlencoded". This isn't really a problem until I try to use wrap_parameters in my controller and it's not have any affect on the params hash:
class ApplicationController < ActionController::API
include ActionController::ParamsWrapper
end
class ProjectsController < ApplicationController
wrap_parameters :project, include: [:name]
# ...
end
This hack no longer works (#request is nil), and none of the other Stack Overflow posts I found work either.
If I make the following request in my RSpec test:
put "/projects/1.json", {name: 'Updated Project 1'}
and put a debugger in my controller I get:
(rdb:1) p params
{ "name"=>"Updated Project 1",
"action"=>"update",
"controller"=>"projects",
"id"=>"5539bbd9-010c-4cfb-88d3-82dadbc99507",
"format"=>"json"
}
(rdb:1) p request.content_type
"application/x-www-form-urlencoded"
I'm expecting to see something like this for the params hash (note the addition of the project key):
{ "name"=>"Updated Project 1",
"action"=>"update",
"controller"=>"projects",
"id"=>"5539bbd9-010c-4cfb-88d3-82dadbc99507",
"format"=>"json",
"project" => {"name" => "Updated Project 1"}
}
Is it possible to set the content type header using just RSpec? Or do I have have to use rack/test for this functionality?
A lot of frustration and variations and that's what worked for me.
Rails 3.2.12 Rspec 2.10
#request.env["HTTP_ACCEPT"] = "application/json"
#request.env["CONTENT_TYPE"] = "application/json"
put :update, :id => 1, "email" => "bing#test.com"
wrap_parameters seems to be working declared this way
wrap_parameters User, format: :json
being used for User model
This worked for me Rails 4.0.3 and Rspec 2.14.1 if anyone is looking for more recent versions.
put '/projects/1.json', {name: 'Updated Project 1'}, {
'HTTP_ACCEPT' => 'application/json',
'CONTENT_TYPE' => 'application/json'
}
and
wrap_parameters Project, format: :json
Using the new Rails v5.0.x API only settings I found that this problem with rails defaulting everything to "application/x-www-form-urlencoded" is still in issue for testing with RSpec-Rails Requests
Here is what I did to fix the problem:
Create support file at ./spec/support/json_requests.rb
Edit it to be something like this to override the behavior for all of your API only JSON requests:
module JsonRequests
def get(*args)
super(*json_args(*args))
end
def post(*args)
super(*json_args(*args))
end
def update(*args)
super(*json_args(*args))
end
def patch(*args)
super(*json_args(*args))
end
def put(*args)
super(*json_args(*args))
end
def delete(*args)
super(*json_args(*args))
end
def json_args(path, params = {}, headers = {})
[path, params.to_json, headers.merge('CONTENT_TYPE' => 'application/json')]
end
end
RSpec.configure do |config|
config.include JsonRequests, type: :request
end
Keep in mind that this will override all Specs within ./spec/requests so if you need to use "application/x-www-form-urlencoded" you could also include this module manually as needed in your Describe 'something' do block.
Rails 5 no hacks:
put(:update,
params: {project_id: 1},
body: {name: 'Updated Project 1'}.to_json,
as: :json)
This sets the content_type correctly. In the controller params will hold both params and body.
Its 2021, Rails 6.1 and I had to use as: :json to fix this wierd mangling of an array of hashes in the params.
put(:update, params: the_params_hash, as: :json)
If you are using Rails 4 (and rspec ~3.7) and don't want to use the inline syntax:
request.headers["CONTENT_TYPE"] = "application/json"
Rails 5
headers = { 'CONTENT_TYPE' => 'application/json' }
params = { user_type: 'tester' }
and after that request like
post '/api/v1/users/test', params.to_json, headers
and also remove .to_json from request route

respond_with won't work on nested routes

I've this code that I'm trying to get working.
class CommitRequestsController < ApplicationController
respond_to :json
def create
#commit_request = CommitRequest.new(params[:commit_request])
respond_with(repository, #commit_request)
end
private
def repository
Repository.find(params[:repository_id])
end
end
I also have this spec
CommitRequest.any_instance.stubs(:valid?).returns(false)
post(:create, {
format: "json",
repository_id: repository.id,
commit_request: {}
})
response.status.should_not eq(201)
The problem is that the spec is always failing.
It returns 201, even tho the created object isn't valid.
Removing the mock line results in the same problem,
even tho the created object is invalid (for real this time).
I'm using Rails 3.2.

Rails 3 Custom Method Location

I am currently trying to add some parsing methods to a controller method in a Rails 3 application.
I have a controller action as follows:
def control
#device = Device.find(params[:id])
<do things>
parse_return(#returned_data)
end
and I added a custom method to the controller as below (this method would not have any routes and would only be accessible to controller actions):
def parse_return
<parse data>
end
but this does not appear to allow the parse_return method to be used. Is there somewhere else in the Rails app that I can put re-usable methods?
Thanks!
At a first glance it seems that you fail to render a response. Is it true that control action doesn't have an associated view?
In this case you have to manually call render in your action. For example, to render JSON response you can do this:
def control
# ...
render :json => parse_return(#returned_data),
:content_type => 'application/json',
:layout => false
end
You should include what the errors are.
What happens if you try this?
def parse_return(returned_data)
<parse data>
end
Perhaps the method is not expecting an parameter to be passed along with it.

Resources