Rails post 请求时遇到一个很诡异的问题,求解答

grd0n9 · August 08, 2016 · Last by grd0n9 replied at August 09, 2016 · 3420 hits

与第三方接口对接,他发来一个 upload 的 POST 请求,然后报 500 错误,我看日志,没走到 route 就报错了

Unexpected error while processing request: undefined method `force_encoding' for nil:NilClass
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:195:in `tag_multipart_encoding'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:75:in `block (2 levels) in parse'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:250:in `get_data'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:74:in `block in parse'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:56:in `loop'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb:56:in `parse'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart.rb:25:in `parse_multipart'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/request.rb:375:in `parse_multipart'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/request.rb:207:in `POST'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/methodoverride.rb:39:in `method_override_param'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/methodoverride.rb:27:in `method_override'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/methodoverride.rb:15:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/runtime.rb:18:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/activesupport-4.2.4/lib/active_support/cache/strategy/local_cache_middleware.rb:28:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/lock.rb:17:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/actionpack-4.2.4/lib/action_dispatch/middleware/static.rb:116:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/sendfile.rb:113:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/engine.rb:518:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/application.rb:165:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/content_length.rb:15:in `call'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/connection.rb:86:in `block in pre_process'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/connection.rb:84:in `catch'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/connection.rb:84:in `pre_process'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/connection.rb:53:in `process'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/connection.rb:39:in `receive_data'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/eventmachine-1.2.0.1/lib/eventmachine.rb:194:in `run_machine'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/eventmachine-1.2.0.1/lib/eventmachine.rb:194:in `run'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/backends/base.rb:73:in `start'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/thin-1.6.4/lib/thin/server.rb:162:in `start'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/handler/thin.rb:19:in `run'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/server.rb:286:in `start'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands/server.rb:80:in `start'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:80:in `block in server'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:75:in `tap'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:75:in `server'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
    /Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/railties-4.2.4/lib/rails/commands.rb:17:in `<top (required)>'
    bin/rails:4:in `require'
    bin/rails:4:in `<main>'

报错主要在/Users/HD/.rbenv/versions/2.2.3/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/multipart/parser.rb 的 get_filename 里,

def get_filename(head)
  filename = nil
  case head  #这里没有成功匹配
  when RFC2183
    filename = Hash[head.scan(DISPPARM)]['filename']
    filename = $1 if filename and filename =~ /^"(.*)"$/
  when BROKEN_QUOTED, BROKEN_UNQUOTED
    filename = $1
  end

  return unless filename

  if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
    filename = Utils.unescape(filename)
  end

  scrub_filename filename

  if filename !~ /\\[^\\"]/
    filename = filename.gsub(/\\(.)/, '\1')
  end
  filename
end

case head 的时候没有匹配成功 RFC2183 或是 BROKEN_QUOTED/BROKEN_UNQUOTED 他的 headers 是

"Content-Disposition: form-data;name=\"file\";filename=\"96.jpg\"\r\nContent-Type:application/octet-stream\r\n"

我自己测试的 headers 是

"Content-Disposition: form-data; name=\"file\"; filename=\"96.jpg\"\r\nContent-Type: image/png\r\n"

区别在 filename 前我有一个空格,对方没有空格 rack 的正则 filename 前就直接是一个\s了,只修改 rack 这一个正则的话,就不报 500 错误了,但又导致一些其他的问题,比如获取不到 content-type 的值

/^(?i-mx:Content-Disposition:\s*(?-mix:[^\s()<>,;:\\"\/\[\]?=]+)\s*).*;\sfilename="(.*?)"(?:\s*$|\s*;\s*(?-mix:[^\s()<>,;:\\"\/\[\]?=]+)=)/i

我用 rails、php 原生方法或者 postman 测试,都会自动带上一个空格; 对方用的是 java,沟通后对方觉得不是他的问题,我在谷歌也没有搜到关于这个空格的案例,然后在 stackoverflow 看了看 java 相关的例子,发现有些高票答案是这么写的

request.writeBytes("Content-Disposition: form-data; name=\"" +
    this.attachmentName + "\";filename=\"" + 
    this.attachmentFileName + "\"" + this.crlf);

也是没有带上空格的,难道真的是这个空格导致的问题吗?

恭喜你找到了 rack 的 bug,这里的正则表达式用了\s: \sfilename,可以去提个 pull request 了

#1 楼 @quakewang 难道这么多年来,上传文件的时候都没人发过不带空格的 post 请求吗?😱

看了一下 rfc,是要求有空格的,那就不是 bug 了,是发起方没有遵循 rfc http://www.rfc-base.org/txt/rfc-2183.txt

disposition := "Content-Disposition" ":"
               disposition-type
               *(";" disposition-parm)
------WebKitFormBoundaryDLwh2vBlBPmzILBC
Content-Disposition: form-data; name="aaa"

ccc
------WebKitFormBoundaryDLwh2vBlBPmzILBC
Content-Disposition: form-data; name="bbb"; filename="aaaaa.txt"
Content-Type: text/plain

aaaaaa
------WebKitFormBoundaryDLwh2vBlBPmzILBC--

这是 ie 浏览器的 post 请求,供参考

#4 楼 @weiwei5987 只要请求头不是自己手写字符串拼接的,就没问题。已经确认不是我的问题了,yeah~

You need to Sign in before reply, if you don't have an account, please Sign up first.