Rails 5 - an example of a controller specification - transmission parameters set to zero get a value set to an empty string

I have the following example of a controller specification running in my application for Rails 4.2.0 and Ruby 2.2.1 only APIs

let!(:params) { { user_token: user_token } } context "- and optional address and contact details params value are received as a nil values -" do it "doesn't set the address and contact details and responds with 201 success", check: true do params.merge!( address_street: nil, address_other: nil, city: nil, state: nil, zip_code: nil, phone: nil) post :create, params expect(response).to have_http_status(201) saved_client_id = json_response["id"] saved_client = Client.find_by(id: saved_client_id) expect(saved_client.address_street).to be_nil expect(saved_client.address_other).to be_nil expect(saved_client.city).to be_nil expect(saved_client.state).to be_nil expect(saved_client.zip_code).to be_nil expect(saved_client.phone).to be_nil end end 

However, when evaluating my application against Rails 5 (the latest version) and Ruby 2.2.3 , the same parameter fails with the following error:

  1) Api::V1::ClientsController POST #create when receives valid client details - and optional address and contact details params value are received as nil values - doesn't set the address and contact details and responds with 201 success Failure/Error: expect(saved_client.address_street).to be_nil expected: nil got: "" # ./spec/controllers/api/v1/clients_controller_spec.rb:352:in `block (5 levels) in <top (required)>' # ./spec/rails_helper.rb:61:in `block (3 levels) in <top (required)>' # /home/jignesh/.rvm/gems/ ruby-2.2.3@myapp-on-rails-5 /gems/database_cleaner-1.5.1/lib/database_cleaner/generic/base.rb:16:in `cleaning' # /home/jignesh/.rvm/gems/ ruby-2.2.3@myapp-on-rails-5 /gems/database_cleaner-1.5.1/lib/database_cleaner/base.rb:92:in `cleaning' # /home/jignesh/.rvm/gems/ ruby-2.2.3@myapp-on-rails-5 /gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:86:in `block (2 levels) in cleaning' # /home/jignesh/.rvm/gems/ ruby-2.2.3@myapp-on-rails-5 /gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:87:in `call' # /home/jignesh/.rvm/gems/ ruby-2.2.3@myapp-on-rails-5 /gems/database_cleaner-1.5.1/lib/database_cleaner/configuration.rb:87:in `cleaning' # ./spec/rails_helper.rb:60:in `block (2 levels) in <top (required)>' 

I checked the Rails source code at several points and found that nil values ​​were converted to empty values ​​until the logic of the target controller action was reached.

This modified behavior sets attributes for empty strings when they are expected to be zero.

In my Gemfile application (for using Rails 5), I specified Rails using the following code:

 gem 'rails', git: 'https://github.com/rails/rails.git' gem 'rack', :git => 'https://github.com/rack/rack.git' gem 'arel', :git => 'https://github.com/rails/arel.git' 

and in Gemfile.lock you can see (parts of Gem and Dependencies are truncated to shorten it):

 GIT remote: git://github.com/capistrano/rbenv.git revision: 6f1216cfe0a6b4ac23ca4eaf8acf012e8165d247 specs: capistrano-rbenv (2.0.3) capistrano (~> 3.1) sshkit (~> 1.3) GIT remote: https://github.com/rack/rack.git revision: c393176b0edf3e5d06cabbb6eb9d9c7a07b2afa7 specs: rack (2.0.0.alpha) json GIT remote: https://github.com/rails/arel.git revision: 3c429c5d86e9e2201c2a35d934ca6a8911c18e69 specs: arel (7.0.0.alpha) GIT remote: https://github.com/rails/rails.git revision: 58df2f4b4abcce0b698c2540da215a565c24cbc9 specs: actionmailer (5.0.0.alpha) actionpack (= 5.0.0.alpha) actionview (= 5.0.0.alpha) activejob (= 5.0.0.alpha) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 1.0, >= 1.0.5) actionpack (5.0.0.alpha) actionview (= 5.0.0.alpha) activesupport (= 5.0.0.alpha) rack (~> 2.x) rack-test (~> 0.6.3) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) actionview (5.0.0.alpha) activesupport (= 5.0.0.alpha) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) activejob (5.0.0.alpha) activesupport (= 5.0.0.alpha) globalid (>= 0.3.0) activemodel (5.0.0.alpha) activesupport (= 5.0.0.alpha) builder (~> 3.1) activerecord (5.0.0.alpha) activemodel (= 5.0.0.alpha) activesupport (= 5.0.0.alpha) arel (= 7.0.0.alpha) activesupport (5.0.0.alpha) concurrent-ruby (~> 1.0) i18n (~> 0.7) json (~> 1.7, >= 1.7.7) method_source minitest (~> 5.1) tzinfo (~> 1.1) rails (5.0.0.alpha) actionmailer (= 5.0.0.alpha) actionpack (= 5.0.0.alpha) actionview (= 5.0.0.alpha) activejob (= 5.0.0.alpha) activemodel (= 5.0.0.alpha) activerecord (= 5.0.0.alpha) activesupport (= 5.0.0.alpha) bundler (>= 1.3.0, < 2.0) railties (= 5.0.0.alpha) sprockets-rails (>= 2.0.0) railties (5.0.0.alpha) actionpack (= 5.0.0.alpha) activesupport (= 5.0.0.alpha) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) ... .... 

Can someone please let me know what has changed, caused this? I guess this has something to do with changes to Rails 5 or the last rack. Is this some kind of bug that needs to be fixed in the final version or is it a deliberate change.

+5
source share
3 answers

I found the main reason for the above behavior: in Rails 5 this is because the CONTENT_TYPE header is set to 'application/x-www-form-urlencoded' by the ActionController :: TestRequest #assign_parameters method by default, however in Rails 4.2. 0 it is not.

The following are detailed findings on how I came to the conclusion:

In the context of the parameters passed in the specification example (shown in my question post), execution is performed in Rails 5 (and its version of Rack) and Rails 4.2.0 (and its version of Rack) goes as follows:

Rails 5

actionpack / Library / action_dispatch / http_request.rb # form_data? returns true

actionpack / lib / action_dispatch / http_request.rb # The POST method looks like this:

 # Override Rack POST method to support indifferent access def POST fetch_header("action_dispatch.request.request_parameters") do pr = parse_formatted_parameters(params_parsers) do |params| super || {} end self.request_parameters = Request::Utils.normalize_encode_params(pr) end rescue ParamsParser::ParseError # one of the parse strategies blew up self.request_parameters = Request::Utils.normalize_encode_params(super || {}) raise rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}") end alias :request_parameters :POST 

When trying to evaluate fetch_header("action_dispatch.request.request_parameters") , a default block is executed that calls super , which calls the POST Rack Request method (/rack-c393176b0edf/lib/rack/request.rb). I showed this method code below with a few debuggers that I installed:

racks / lib / rack / request.rb # POST

  # Returns the data received in the request body. # # This method support both application/x-www-form-urlencoded and # multipart/form-data. def POST puts ">>>>>>>>>>> DEBUG 2" if get_header(RACK_INPUT).nil? puts ">>>>>>>>>>> DEBUG 2.1" raise "Missing rack.input" elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT) puts ">>>>>>>>>>> DEBUG 2.2" get_header(RACK_REQUEST_FORM_HASH) elsif form_data? || parseable_data? puts ">>>>>>>>>>> DEBUG 2.3" unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart) form_vars = get_header(RACK_INPUT).read # Fix for Safari Ajax postings that always append \0 # form_vars.sub!(/\0\z/, '') # performance replacement: form_vars.slice!(-1) if form_vars[-1] == ?\0 set_header RACK_REQUEST_FORM_VARS, form_vars set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&') get_header(RACK_INPUT).rewind end set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) get_header RACK_REQUEST_FORM_HASH else puts ">>>>>>>>>>> DEBUG 2.4" {} end 

With these debugging operations, the thread ended in "β†’ β†’ β†’ β†’ β†’> DEBUG 2.3" . There I also checked get_header RACK_REQUEST_FORM_HASH and printed

 >>>>>>>>>>> get_header RACK_REQUEST_FORM_HASH: {"address_other"=>"", "address_street"=>"", "city"=>"", "client_residence_type_id"=>"", "name"=>"Test Client 1", "phone"=>"", "provider_id"=>"64", "state"=>"", "zip_code"=>""} 

So this is the parse_query(form_vars, '&') method, which converts nil values ​​to empty strings.

Rails 4.2.0

actionpack / Library / action_dispatch / http_request.rb # form_data? returns false

actionpack / lib / action_dispatch / http_request.rb # The POST method looks like this:

 # Override Rack POST method to support indifferent access def POST @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {})) rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e raise ActionController::BadRequest.new(:request, e) end alias :request_parameters :POST 

This calls super , making the call a switch to the POST Rack Request method (rack-1.6.4 / lib / rack / request.rb). I showed this method code below with a few debuggers that I installed:

rack-mounted 1.6.4 / Library / rack / request.rb # parseable_data? returns false

rack-1.6.4 / lib / rack / request.rb # POST stream ended in "β†’ β†’ β†’ β†’ β†’> DEBUG 2.4"

 def POST puts ">>>>>>>>>>> DEBUG 2" if @env["rack.input"].nil? puts ">>>>>>>>>>> DEBUG 2.1" raise "Missing rack.input" elsif @env["rack.request.form_input"].equal? @env["rack.input"] puts ">>>>>>>>>>> DEBUG 2.2" @env["rack.request.form_hash"] elsif form_data? || parseable_data? puts ">>>>>>>>>>> DEBUG 2.3" unless @env["rack.request.form_hash"] = parse_multipart(env) form_vars = @env["rack.input"].read # Fix for Safari Ajax postings that always append \0 # form_vars.sub!(/\0\z/, '') # performance replacement: form_vars.slice!(-1) if form_vars[-1] == ?\0 @env["rack.request.form_vars"] = form_vars @env["rack.request.form_hash"] = parse_query({ :query => form_vars, :separator => '&' }) @env["rack.input"].rewind end @env["rack.request.form_input"] = @env["rack.input"] @env["rack.request.form_hash"] else puts ">>>>>>>>>>> DEBUG 2.4" {} end end 

This leads to my observation that in Rails 5 the content_mime_type , which is used internally by form_data? , is set, and therefore, the parameters presented in the specification example are analyzed as form parameters.

However, in Rails 4.2.0 content_mime_type no set was found which does not lead to the analysis of parameters that should be analyzed as form_params.

Rails 4.2.0

The content_mime_type method is defined in the ActionDispatch::Http::MimeNegotiation

  def content_mime_type @env["action_dispatch.request.content_type"] ||= begin if @env['CONTENT_TYPE'] =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else nil end end end 

which returns nil

Rails 5

The content_mime_type method is defined in the ActionDispatch::Http::MimeNegotiation

  def content_mime_type fetch_header("action_dispatch.request.content_type") do |k| v = if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ Mime::Type.lookup($1.strip.downcase) else nil end set_header k, v end end 

In this case, if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/ evaluates to true and therefore Mime::Type.lookup($1.strip.downcase)

Rails 4.2.0

CONTENT_TYPE header not set

actionpack / lib / action_controller / test_case.rb # def assign_parameters (routes, controller_path, action, parameters = {}) method

 def assign_parameters(routes, controller_path, action, parameters = {}) parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action) extra_keys = routes.extra_keys(parameters) non_path_parameters = get? ? query_parameters : request_parameters parameters.each do |key, value| if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?)) value = value.map{ |v| v.duplicable? ? v.dup : v } elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? }) value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }] elsif value.frozen? && value.duplicable? value = value.dup end if extra_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) value = value.map(&:to_param) else value = value.to_param end path_parameters[key] = value end end # Clear the combined params hash in case it was already referenced. @env.delete("action_dispatch.request.parameters") # Clear the filter cache variables so they're not stale @filtered_parameters = @filtered_env = @filtered_path = nil params = self.request_parameters.dup %w(controller action only_path).each do |k| params.delete(k) params.delete(k.to_sym) end data = params.to_query @env['CONTENT_LENGTH'] = data.length.to_s @env['rack.input'] = StringIO.new(data) end 

Rails 5

CONTENT_TYPE header is set.

actionpack / lib / action_controller / test_case.rb # assign_parameters (routes, controller_path, action, parameters, generated_package, query_string_keys) method

 def assign_parameters(routes, controller_path, action, parameters, generated_path, query_string_keys) non_path_parameters = {} path_parameters = {} parameters.each do |key, value| if query_string_keys.include?(key) non_path_parameters[key] = value else if value.is_a?(Array) value = value.map(&:to_param) else value = value.to_param end path_parameters[key] = value end end if get? if self.query_string.blank? self.query_string = non_path_parameters.to_query end else if ENCODER.should_multipart?(non_path_parameters) self.content_type = ENCODER.content_type data = ENCODER.build_multipart non_path_parameters else fetch_header('CONTENT_TYPE') do |k| set_header k, 'application/x-www-form-urlencoded' end case content_mime_type.to_sym when nil raise "Unknown Content-Type: #{content_type}" when :json data = ActiveSupport::JSON.encode(non_path_parameters) when :xml data = non_path_parameters.to_xml when :url_encoded_form data = non_path_parameters.to_query else @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters } data = non_path_parameters.to_query end end set_header 'CONTENT_LENGTH', data.length.to_s set_header 'rack.input', StringIO.new(data) end fetch_header("PATH_INFO") do |k| set_header k, generated_path end path_parameters[:controller] = controller_path path_parameters[:action] = action self.path_parameters = path_parameters end 

As you can see from the POST request, the following code is executed, which sets the CONTENT_TYPE header to the default value "application / x-www-form-urlencoded"

  fetch_header('CONTENT_TYPE') do |k| set_header k, 'application/x-www-form-urlencoded' end 

Thanks.

+8
source

This problem appears to be known, but not yet fixed. There is a workaround in this release: https://github.com/rspec/rspec-rails/issues/1655

I tested and used this in my rspec test controller, and it passes the data correctly:

before { request.env['CONTENT_TYPE'] = 'application/json' }

+7
source

The violetaria answer option is to add as: :json to your requests, for example.

 post :create, params: {…}, as: :json 
+2
source

Source: https://habr.com/ru/post/1236621/


All Articles