7

Rails 4.2.0 および Ruby 2.2.1に基づく API のみのアプリケーションで渡す次のコントローラー仕様の例があります。

  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

ただし、 Rails 5 (エッジ バージョン) および Ruby 2.2.3に対してアプリケーションを評価すると、同じ仕様が次のエラーで失敗します。

  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)>'

Rails のソース コードをいくつかの点で調べたところ、コントローラーのターゲット アクション ロジックに到達する前に、nil 値が空白値に変換されていることがわかりました。

この変更された動作は、属性が nil であると予想される場合に、属性を空の文字列に設定することです。

アプリの Gemfile (Rails 5 を使用するため) で、次のコードを使用して Rails を指定しました。

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'

また、Gemfile.lock では次のように表示されます (Gem と依存関係の部分は短くするために切り捨てられています)。

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)
...
....

誰がこれを引き起こした変更を教えてもらえますか? Rails 5または最新のRackの変更と関係があると思います。これは、最終リリース バージョンで修正されるバグのようなものですか、それとも意図的な変更ですか。

4

3 に答える 3

12

上記の動作の根本的な原因を見つけました。Rails 5 では、デフォルトのCONTENT_TYPEヘッダーが'application/x-www-form-urlencoded'ActionController::TestRequest #assign_parameters メソッドによって設定されていることが原因ですが、Rails 4.2.0 ではそうではありません。

どのようにして結論に達したかについて、詳細な調査結果を以下に示します。

仕様の例 ​​(私の質問の投稿に表示) で渡されるパラメーターのコンテキストでは、Rails 5 (およびそのラック バージョン) およびRails 4.2.0 (およびそのラック バージョン) での実行フローは、以下に詳述する方法で行われます。

レール5

actionpack/lib/action_dispatch/http_request.rb#form_data? true を返します

actionpack/lib/action_dispatch/http_request.rb#POST メソッドは次のようになります。

# Override Rack's 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

評価しようとしている間、Rack の Request (/rack-c393176b0edf/lib/rack/request.rb) POST メソッドにfetch_header("action_dispatch.request.request_parameters")呼び出しを行うデフォルト値ブロックが実行されます。super以下に、このメソッドのコードをいくつかのデバッグ ステートメントとともに示しました。

ラック/ライブラリ/ラック/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

これらのデバッグ ステートメントにより、実行フローは">>>>>>>>>>> DEBUG 2.3"で終了しました。そこでも get_header RACK_REQUEST_FORM_HASH を検査し、印刷しました

>>>>>>>>>>> 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"=>""}

したがってparse_query(form_vars, '&')、nil 値を空の文字列に変換するメソッドです。

レール 4.2.0

actionpack/lib/action_dispatch/http_request.rb#form_data? false を返します

actionpack/lib/action_dispatch/http_request.rb#POST メソッドは次のようになります。

# Override Rack's 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

これsuperにより、呼び出しが Rack の Request (rack-1.6.4/lib/rack/request.rb) POST メソッドに送られます。以下に、このメソッドのコードをいくつかのデバッグ ステートメントとともに示しました。

Rack-1.6.4/lib/rack/request.rb#parseable_data? false を返します

rack-1.6.4/lib/rack/request.rb#POST フロー終了">>>>>>>>>>> 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

content_mime_typeこれにより、内部で使用されるRails 5ではform_data?が設定されているため、仕様の例で送信されたパラメータがフォームパラメータとして解析されることに気づきました。

ただし、Rails 4.2.0 では、content_mime_type送信された params が form_params として解析されない is not found セットがあります。

レール 4.2.0

content_mime_typeメソッドはモジュールで定義されてい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

それは nil を返します

レール5

content_mime_typeメソッドはモジュールで定義されてい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

この場合if get_header('CONTENT_TYPE') =~ /^([^,\;]*)/は true と評価されるため、Mime::Type.lookup($1.strip.downcase)が返されます。

レール 4.2.0

ヘッダーCONTENT_TYPEは設定されません

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

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

レール5

ヘッダー CONTENT_TYPE はによって設定されます

actionpack/lib/action_controller/test_case.rb#assign_parameters(routes, controller_path, action, parameters, generated_pa​​th, query_string_keys) メソッド

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

CONTENT_TYPE ヘッダーをデフォルト値「application/x-www-form-urlencoded」に設定するコードが実行される POST リクエストで見られるように

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

ありがとう。

于 2015-11-24T08:33:53.147 に答える
7

この問題は既知のようですが、まだ修正されていません。この問題に記載されている回避策があります: https://github.com/rspec/rspec-rails/issues/1655

私はrspecコントローラーテストでこれをテストして使用しましたが、データを適切に送信します:

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

于 2016-08-11T19:59:37.167 に答える