Rails 3.2 f.file_field导致路由错误

时间:2013-03-02 18:43:21

标签: ruby-on-rails forms ruby-on-rails-3.2 passenger rails-routing

在轨道3.2.12和3.2.11上测试。在另一个rails 3.2.11项目中我对f.file_field没有这个问题,但是在当前的一个我做的并且找不到这种奇怪行为的原因,所以这是我的问题。

我对更新操作有一个奇怪的问题。以下是代码的相关部分:

路线:

get "signup" => "users#new", :as => "signup"
get "profile" => "users#profile", :as => "profile"
resources :users do
  member do
    get :activate
  end
end

控制器:

def update
  @user = User.find(params[:id])
  if @user.update_attributes(params[:user])
    redirect_to user_path(@user), :notice => t('users_controller.update.updated')
  else
    render :edit
  end
end

以haml形式(简化但具有相同的行为):

= form_for @user do |f|
  .field
    = f.label :first_name 
    %br
    = f.text_field :first_name, :size => 40
  .actions
    = f.submit

因此,在我按下更新后,所有内容都按预期工作,并且正在更新用户的属性。但是当我添加这样的文件字段时:

= form_for @user do |f|
  .field
    = f.label :first_name 
    %br
    = f.text_field :first_name, :size => 40
  .field
    = f.label :avatar
    %br
    = f.file_field :avatar
  .actions
    = f.submit

然后按更新然后我收到路由错误:

No route matches [PUT] "/1"

我不明白为什么它尝试使用/1方法到达PUT路径。在显示路由错误的页面上,我可以在浏览器的地址栏中看到/users/1

这里是为表格生成的html:

  <form accept-charset="UTF-8" action="/users/1" class="edit_user" enctype="multipart/form-data" id="edit_user_1" method="post"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /><input name="_method" type="hidden" value="put" /><input name="authenticity_token" type="hidden" value="che8VLfDxDAoenma+TXwsA+0IQ7+/jbCIK+Q2xwr8uc=" /></div>
    <div class='field'>
      <label for="user_first_name">First name</label>
      <br>
      <input id="user_first_name" name="user[first_name]" size="40" type="text" value="Anton" />
    </div>
    <div class='field'>
      <label for="user_avatar">Avatar</label>
      <br>
      <input id="user_avatar" name="user[avatar]" type="file" />
    </div>
    <div class='actions'>
      <input name="commit" type="submit" value="Update User" />
    </div>
  </form>

所以,这是最有趣的事情。当我将表单更改为:

= form_for @user do |f|
  .field
    = f.label :first_name 
    %br
    = f.text_field :first_name, :size => 40
  .field
    = f.label :avatar
    %br
    %input{:id => "user_avatar", :name => "user[avatar]", :type => "file"}
  .actions
    = f.submit

然后生成的html与之前的情况相同(唯一的区别是我可以看到,对于文件字段属性使用单引号而不是双引号):

  <form accept-charset="UTF-8" action="/users/1" class="edit_user" id="edit_user_1" method="post"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /><input name="_method" type="hidden" value="put" /><input name="authenticity_token" type="hidden" value="che8VLfDxDAoenma+TXwsA+0IQ7+/jbCIK+Q2xwr8uc=" /></div>
    <div class='field'>
      <label for="user_first_name">First name</label>
      <br>
      <input id="user_first_name" name="user[first_name]" size="40" type="text" value="Anton" />
    </div>
    <div class='field'>
      <label for="user_avatar">Avatar</label>
      <br>
      <input id='user_avatar' name='user[avatar]' type='file'>
    </div>
    <div class='actions'>
      <input name="commit" type="submit" value="Update User" />
    </div>
  </form>

但在提交此表单后,没有路由错误,一切正常。

更新

实际上它不能正常工作。我只是看了params哈希,看到:avatar键存在,但我错过了后一种情况,html中的表单open标签中没有enctype="multipart/form-data"属性,所以文件不会上传。添加enctype=multipart/form-data属性会导致路由错误再次发生。

我发现在提交多部分表单后尝试put ":id" => "users#update"时添加了redirect_to user_path(@user)路由(对于此路由肯定没有PUT的路由错误),那么也是路由错误No route matches [GET] "/users/users/1"

这是完整的routes.rb

Myapp::Application.routes.draw do

  match "oauth/callback" => "oauths#callback"
  match "oauth/callback/:provider" => "oauths#callback"
  match "oauth/:provider" => "oauths#oauth", :as => :auth_at_provider

  resources :countries
  resources :categories
  resources :images
  resources :collections
  resources :items

  put ":id" => "users#update"
  get "signup" => "users#new", :as => "signup"
  get "profile" => "users#profile", :as => "profile"
  resources :users do
    member do
      get :activate
    end
  end

  get "signout" => "sessions#destroy", :as => "signout"
  get "signin" => "sessions#new", :as => "signin"
  resources :sessions

  get "site/index"

  root :to => "site#index"

end

和rake路线

  oauth_callback        /oauth/callback(.:format)           oauths#callback
                        /oauth/callback/:provider(.:format) oauths#callback
auth_at_provider        /oauth/:provider(.:format)          oauths#oauth
       countries GET    /countries(.:format)                countries#index
                 POST   /countries(.:format)                countries#create
     new_country GET    /countries/new(.:format)            countries#new
    edit_country GET    /countries/:id/edit(.:format)       countries#edit
         country GET    /countries/:id(.:format)            countries#show
                 PUT    /countries/:id(.:format)            countries#update
                 DELETE /countries/:id(.:format)            countries#destroy
      categories GET    /categories(.:format)               categories#index
                 POST   /categories(.:format)               categories#create
    new_category GET    /categories/new(.:format)           categories#new
   edit_category GET    /categories/:id/edit(.:format)      categories#edit
        category GET    /categories/:id(.:format)           categories#show
                 PUT    /categories/:id(.:format)           categories#update
                 DELETE /categories/:id(.:format)           categories#destroy
          images GET    /images(.:format)                   images#index
                 POST   /images(.:format)                   images#create
       new_image GET    /images/new(.:format)               images#new
      edit_image GET    /images/:id/edit(.:format)          images#edit
           image GET    /images/:id(.:format)               images#show
                 PUT    /images/:id(.:format)               images#update
                 DELETE /images/:id(.:format)               images#destroy
     collections GET    /collections(.:format)              collections#index
                 POST   /collections(.:format)              collections#create
  new_collection GET    /collections/new(.:format)          collections#new
 edit_collection GET    /collections/:id/edit(.:format)     collections#edit
      collection GET    /collections/:id(.:format)          collections#show
                 PUT    /collections/:id(.:format)          collections#update
                 DELETE /collections/:id(.:format)          collections#destroy
           items GET    /items(.:format)                    items#index
                 POST   /items(.:format)                    items#create
        new_item GET    /items/new(.:format)                items#new
       edit_item GET    /items/:id/edit(.:format)           items#edit
            item GET    /items/:id(.:format)                items#show
                 PUT    /items/:id(.:format)                items#update
                 DELETE /items/:id(.:format)                items#destroy
                 PUT    /:id(.:format)                      users#update
          signup GET    /signup(.:format)                   users#new
         profile GET    /profile(.:format)                  users#profile
   activate_user GET    /users/:id/activate(.:format)       users#activate
           users GET    /users(.:format)                    users#index
                 POST   /users(.:format)                    users#create
        new_user GET    /users/new(.:format)                users#new
       edit_user GET    /users/:id/edit(.:format)           users#edit
            user GET    /users/:id(.:format)                users#show
                 PUT    /users/:id(.:format)                users#update
                 DELETE /users/:id(.:format)                users#destroy
         signout GET    /signout(.:format)                  sessions#destroy
          signin GET    /signin(.:format)                   sessions#new
        sessions GET    /sessions(.:format)                 sessions#index
                 POST   /sessions(.:format)                 sessions#create
     new_session GET    /sessions/new(.:format)             sessions#new
    edit_session GET    /sessions/:id/edit(.:format)        sessions#edit
         session GET    /sessions/:id(.:format)             sessions#show
                 PUT    /sessions/:id(.:format)             sessions#update
                 DELETE /sessions/:id(.:format)             sessions#destroy
      site_index GET    /site/index(.:format)               site#index
            root        /

有人有任何想法吗?

UPDATE2

揭示问题是多部分表单有助于找到关于同一问题的帖子 - Routing Error with Post/Put requests (Passenger Headers)但不幸的是没有解决方案......

UPDATE3

我发现了一些有趣的东西。 /path_to_gemset_here/gem/journey-1.0.4/lib/journey/router.rb中有一种方法:

def find_routes env
  req = request_class.new env

  routes = filter_routes(req.path_info) + custom_routes.find_all { |r|
    r.path.match(req.path_info)
  }

  routes.sort_by(&:precedence).find_all { |r|
    r.constraints.all? { |k,v| v === req.send(k) } &&
      r.verb === req.request_method
  }.reject { |r| req.ip && !(r.ip === req.ip) }.map { |r|
    match_data  = r.path.match(req.path_info)
    match_names = match_data.names.map { |n| n.to_sym }
    match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) }
    info = Hash[match_names.zip(match_values).find_all { |_,y| y }]

    [match_data, r.defaults.merge(info), r]
  }
end

我检查了env非多部分和多部分请求,发现了这个:

非多:

"REQUEST_URI"=>"/users/1",
"SCRIPT_NAME"=>"",
"PATH_INFO"=>"/users/1"

多部分:

"REQUEST_URI"=>"/users/1",
"SCRIPT_NAME"=>"/users",
"PATH_INFO"=>"/1",
"SCRIPT_FILENAME"=>"/path_to_project_folder_here/public/users", - there is no such variable in a non-multipart request

所以这就是问题所在。正如我在方法的定义中看到的那样:

match_data  = r.path.match(req.path_info)

PATH_INFO用于查找处理请求的路由,但在后一种情况下,由于将REQUEST_URI分为两部分,因此完全错误。不幸的是,目前我没有时间完成今天的调查,希望明天能够做到。

如果有人有足够的好奇心比我更快找到问题的根源 - 欢迎你:)

UPDATE4(已修改)

所以,这是调查的延续。

方法:parse_native_request 在档案中:/path_to_gemset_here/gems/passenger-3.0.17/lib/phusion_passenger/abstract_request_handler.rb

此次调用后

变量headers_data

headers_data = channel.read_scalar(buffer, MAX_HEADER_SIZE)

包含:

"SERVER_SOFTWARE\x00Apache/2.2.22 (Ubuntu)\x00
SERVER_PROTOCOL\x00HTTP/1.1\x00
SERVER_NAME\x00myapp.loc\x00
SERVER_ADMIN\x00[no address given]\x00
SERVER_ADDR\x00127.0.0.1\x00
SERVER_PORT\x0080\x00
REMOTE_ADDR\x00127.0.0.1\x00
REMOTE_PORT\x0033199\x00
REQUEST_METHOD\x00POST\x00
QUERY_STRING\x00\x00
CONTENT_TYPE\x00multipart/form-data; boundary=----WebKitFormBoundary8HlzQxocoOROMfRV\x00
DOCUMENT_ROOT\x00/path_to_project_folder_here/public\x00
REQUEST_URI\x00/users/1\x00
SCRIPT_NAME\x00\x00
PATH_INFO\x00/users/1\x00
HTTP_HOST\x00myapp.loc\x00
HTTP_CONNECTION\x00keep-alive\x00
HTTP_CONTENT_LENGTH\x00748\x00
HTTP_CACHE_CONTROL\x00max-age=0\x00
HTTP_ACCEPT\x00text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\x00
HTTP_ORIGIN\x00http://myapp.loc\x00
HTTP_USER_AGENT\x00Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.97 Safari/537.22\x00
HTTP_CONTENT_TYPE\x00multipart/form-data; boundary=----WebKitFormBoundary8HlzQxocoOROMfRV\x00
HTTP_REFERER\x00http://myapp.loc/profile\x00
HTTP_ACCEPT_ENCODING\x00gzip,deflate,sdch\x00
HTTP_ACCEPT_LANGUAGE\x00en-US,en;q=0.8\x00
HTTP_ACCEPT_CHARSET\x00ISO-8859-1,utf-8;q=0.7,*;q=0.3\x00
HTTP_COOKIE\x00_myapp_session=BAh7CEkiDHVzZXJfaWQGOgZFRmkGSSIPc2Vzc2lvbl9pZAY7AEZJIiVhMjU2ZjU5N2VmMTE0YTJiOGEwNGJiYzUyYjM2NDg0OQY7AFRJIhBfY3NyZl90b2tlbgY7AEZJIjFjaGU4VkxmRHhEQW9lbm1hK1RYd3NBKzBJUTcrL2piQ0lLK1EyeHdyOHVjPQY7AEY%3D--a6e5daff1334c083e54b2bcafba43b32e546af9c\x00
UNIQUE_ID\x00UTXfEX8AAQEAACWVEMoAAAAB\x00
GATEWAY_INTERFACE\x00CGI/1.1\x00

>>>> here seems to start a kind of redirect <<<<

SERVER_PROTOCOL\x00HTTP/1.1\x00
REQUEST_METHOD\x00POST\x00
QUERY_STRING\x00\x00
REQUEST_URI\x00/users/1\x00
SCRIPT_NAME\x00/users\x00
PATH_INFO\x00/1\x00
PATH_TRANSLATED\x00/path_to_project_folder_here/public/1\x00
HTTP_HOST\x00myapp.loc\x00
HTTP_CONNECTION\x00keep-alive\x00
CONTENT_LENGTH\x00748\x00HTTP_CACHE_CONTROL\x00max-age=0\x00
HTTP_ACCEPT\x00text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\x00
HTTP_ORIGIN\x00http://myapp.loc\x00
HTTP_USER_AGENT\x00Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.97 Safari/537.22\x00CONTENT_TYPE\x00multipart/form-data; boundary=----WebKitFormBoundary8HlzQxocoOROMfRV\x00
HTTP_REFERER\x00http://myapp.loc/profile\x00
HTTP_ACCEPT_ENCODING\x00gzip,deflate,sdch\x00
HTTP_ACCEPT_LANGUAGE\x00en-US,en;q=0.8\x00
HTTP_ACCEPT_CHARSET\x00ISO-8859-1,utf-8;q=0.7,*;q=0.3\x00
HTTP_COOKIE\x00_myapp_session=BAh7CEkiDHVzZXJfaWQGOgZFRmkGSSIPc2Vzc2lvbl9pZAY7AEZJIiVhMjU2ZjU5N2VmMTE0YTJiOGEwNGJiYzUyYjM2NDg0OQY7AFRJIhBfY3NyZl90b2tlbgY7AEZJIjFjaGU4VkxmRHhEQW9lbm1hK1RYd3NBKzBJUTcrL2piQ0lLK1EyeHdyOHVjPQY7AEY%3D--a6e5daff1334c083e54b2bcafba43b32e546af9c\x00
PATH\x00/usr/local/bin:/usr/bin:/bin\x00
SERVER_SIGNATURE\x00<address>Apache/2.2.22 (Ubuntu) Server at myapp.loc Port 80</address>\n\x00
SERVER_SOFTWARE\x00Apache/2.2.22 (Ubuntu)\x00
SERVER_NAME\x00myapp.loc\x00
SERVER_ADDR\x00127.0.0.1\x00
SERVER_PORT\x0080\x00
REMOTE_ADDR\x00127.0.0.1\x00
DOCUMENT_ROOT\x00/path_to_project_folder_here/public\x00
SERVER_ADMIN\x00[no address given]\x00
SCRIPT_FILENAME\x00/path_to_project_folder_here/public/users\x00
REMOTE_PORT\x0033199\x00
PATH_TRANSLATED\x00/bin/runAV\x00
REDIRECT_STATUS\x00302\x00
PASSENGER_CONNECT_PASSWORD\x00EElt7wIBLlliWGCYJJoezPvecsB2brraBWdiIbD4nul\x00_\x00_\x00"

之后是这个电话:

headers = split_by_null_into_hash(headers_data)

headers包含:

{"SERVER_SOFTWARE"=>"Apache/2.2.22 (Ubuntu)", 
"SERVER_PROTOCOL"=>"HTTP/1.1", 
"SERVER_NAME"=>"myapp.loc", 
"SERVER_ADMIN"=>"[no address given]", 
"SERVER_ADDR"=>"127.0.0.1", 
"SERVER_PORT"=>"80", 
"REMOTE_ADDR"=>"127.0.0.1", 
"REMOTE_PORT"=>"33243", 
"REQUEST_METHOD"=>"POST", 
"QUERY_STRING"=>"", 
"CONTENT_TYPE"=>"multipart/form-data; boundary=----WebKitFormBoundary8HlzQxocoOROMfRV", 
"DOCUMENT_ROOT"=>"/path_to_project_folder_here/public", 
"REQUEST_URI"=>"/users/1", 
"SCRIPT_NAME"=>"/users", 
"PATH_INFO"=>"/1", 
"HTTP_HOST"=>"myapp.loc", 
"HTTP_CONNECTION"=>"keep-alive", 
"HTTP_CONTENT_LENGTH"=>"748", 
"HTTP_CACHE_CONTROL"=>"max-age=0", 
"HTTP_ACCEPT"=>"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 
"HTTP_ORIGIN"=>"http://myapp.loc", 
"HTTP_USER_AGENT"=>"Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.97 Safari/537.22", 
"HTTP_CONTENT_TYPE"=>"multipart/form-data; boundary=----WebKitFormBoundary8HlzQxocoOROMfRV", 
"HTTP_REFERER"=>"http://myapp.loc/profile", 
"HTTP_ACCEPT_ENCODING"=>"gzip,deflate,sdch", 
"HTTP_ACCEPT_LANGUAGE"=>"en-US,en;q=0.8", 
"HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.3", 
"HTTP_COOKIE"=>"_myapp_session=BAh7CEkiDHVzZXJfaWQGOgZFRmkGSSIPc2Vzc2lvbl9pZAY7AEZJIiVhMjU2ZjU5N2VmMTE0YTJiOGEwNGJiYzUyYjM2NDg0OQY7AFRJIhBfY3NyZl90b2tlbgY7AEZJIjFjaGU4VkxmRHhEQW9lbm1hK1RYd3NBKzBJUTcrL2piQ0lLK1EyeHdyOHVjPQY7AEY%3D--a6e5daff1334c083e54b2bcafba43b32e546af9c", 
"UNIQUE_ID"=>"UTXjXn8AAQEAACceEdgAAAAA", 
"GATEWAY_INTERFACE"=>"CGI/1.1", 
"PATH_TRANSLATED"=>"/bin/runAV", 
"CONTENT_LENGTH"=>"748", 
"PATH"=>"/usr/local/bin:/usr/bin:/bin", 
"SERVER_SIGNATURE"=>"<address>Apache/2.2.22 (Ubuntu) Server at myapp.loc Port 80</address>\n", 
"SCRIPT_FILENAME"=>"/path_to_project_folder_here/public/users", 
"REDIRECT_STATUS"=>"302", 
"PASSENGER_CONNECT_PASSWORD"=>"GgEqWssAcbBETWnFI7xzBfWRGibgB34OhfFSUVyOhPn", "_"=>"_"}

所以问题显然在于将标题打包成哈希的方式 - PATH_INFO(以及其他标题)也有两个值,后一个(不正确)重写第一个(实际上)问题在于为什么这些头被发送但我不知道如何处理这个)。在split_by_null_into_hash(headers_data)方法中包含哈希值。现在去那里。

档案:/path_to_gemset_here/gems/passenger-3.0.17/lib/phusion_passenger/utils.rb

模块Utils包含以下代码:

if defined?(PhusionPassenger::NativeSupport)
# Split the given string into an hash. Keys and values are obtained by splitting the
  # string using the null character as the delimitor.
  def split_by_null_into_hash(data)
    return PhusionPassenger::NativeSupport.split_by_null_into_hash(data)
  end
else
  NULL = "\0".freeze

  def split_by_null_into_hash(data)
    args = data.split(NULL, -1)
    args.pop
    return Hash[*args]
  end
end

在我的情况下,if - 部分条件正在执行,所以现在问题出现了

PhusionPassenger::NativeSupport.split_by_null_into_hash(data)

这似乎需要我们提交:/path_to_gemset_here/gems/passenger-3.0.17/ext/ruby/passenger_native_support.c

继续......

UPDATE5

实际上我决定不处理那个C - 文件调试地狱,因为我相信这个文件是在乘客安装期间编译并调试它我需要重新安装并重新安装乘客。所以我决定切换到使用else - 部分条件,因为它似乎实现了完全相同的目标,但显然比预编译C - 代码慢一点。但就我而言,这并不重要。所以我通过使用以下代码将文件包含到/path_to_project_folder_here/lib文件夹来覆盖方法的定义:

module PhusionPassenger
  module Utils

    protected

    NULL = "\0".freeze

    def split_by_null_into_hash(data)
      args = data.split(NULL, -1)
      args.pop
      return Hash[*args]
    end

  end
end

我无法改变Hash[*args]行为(更准确地说我可以通过覆盖::[]方法,但我不想确定)所以我会稍微更改一下代码:

module PhusionPassenger
  module Utils

    protected

    NULL = "\0".freeze

    def split_by_null_into_hash(data)
      args = data.split(NULL, -1)
      args.pop
      headers_hash = Hash.new
      args.each_slice(2).to_a.each do |pair|
        headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
      end
      return headers_hash
    end

  end
end

宾果!现在它有效。

但是我不确定我是否通过这样做没有打破任何其他功能所以我不能建议任何人使用这种方法。我将使用它,直到我遇到与此修改相关的任何问题。如果情况确实如此,那么我将尝试找到解决问题的另一种方法。

主要问题仍然是为什么发送错误的标题。

1 个答案:

答案 0 :(得分:4)

使用以下代码在passenger_extension.rb文件夹中创建lib

乘客3

module PhusionPassenger
  module Utils

    protected

    NULL = "\0".freeze

    def split_by_null_into_hash(data)
      args = data.split(NULL, -1)
      args.pop
      headers_hash = Hash.new
      args.each_slice(2).to_a.each do |pair|
        headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
      end
      return headers_hash
    end

  end
end

乘客5

module PhusionPassenger
  module Utils

    # Utility functions that can potentially be accelerated by native_support functions.
    module NativeSupportUtils
      extend self

      NULL = "\0".freeze

      class ProcessTimes < Struct.new(:utime, :stime)
      end

      def split_by_null_into_hash(data)
        args = data.split(NULL, -1)
        args.pop
        headers_hash = Hash.new
        args.each_slice(2).to_a.each do |pair|
          headers_hash[pair.first] = pair.last unless headers_hash.keys.include? pair.first
        end
        return headers_hash
      end

      def process_times
        times = Process.times
        return ProcessTimes.new((times.utime * 1_000_000).to_i,
          (times.stime * 1_000_000).to_i)
      end
    end

  end # module Utils
end # module PhusionPassenger

然后在'config / application.rb'中执行:

class Application < Rails::Application
  ...
  config.autoload_paths += %W(#{config.root}/lib)
  require 'passenger_extension'
end

然后重新启动网络服务器。

注意:我不确定这是否会破坏任何其他功能,因此请自行承担风险,如果您发现此方法有任何损害,请与我们联系。