rails2.3.5、正確にはrack1.0.1のマルチパート解析処理に問題があり、データの欠落する可能性があります。
問題
「チェックボックスがおかしな状態になる」という問い合わせがあり調査開始しました。ほどなく"ごくまれ"にチェックボックスの入力状態が正しく反映されないケースがある事に気付きました。
切り分けの為、問題箇所を確認した所
- パケットダンパーでデータを確認すると、socket通信上は想定値が確認できた。
- 例外メール上の記録から、railsでのparamsには、おかしな値が入っている。
- 上記メールから、rackより引き継いだ段階でも、おかしな値が入っている。
そこで、Rack::Utils::Multipart.parse_multipartメソッドにログを仕込んで確認した所
- content_lengthは想定どおりの値が確認できた。
- io上入ってきた内容も想定どおり確認できた。
- 処理ループが想定よりもかなり早い段階で抜けている。但し例外は発生していない。
上手くいかない場合は、変数の動きをみていると、buf変数に一度読み込まれるだけで、後続のデータが処理されていません。
解決
buf変数に読み込んだデータを逐次処理していきますが、バウンダリ+EOL(\r\n)で丁度処理が終了した場合、buf.empty?が真となり、bufに読み込まれていないデータがあっても処理終了になっているようです。よって上記をrack1.1.xで修正されているように
break if (buf.empty? && $1 != EOL) || content_length == -1
とする必要があるようです($1 != EOLを追加)。
しかしrack1.1とrails2.3.5は相性が悪いみたいなので、モンキーパッチで対処する必要がありますね。
下記のような内容をconfig/initializers/rack_patch.rbとかで配置して下さい。
module Rack module Utils module Multipart def self.parse_multipart(env) unless env['CONTENT_TYPE'] =~ %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|n nil else boundary = "--#{$1}" params = {} buf = "" content_length = env['CONTENT_LENGTH'].to_i input = env['rack.input'] input.rewind boundary_size = Utils.bytesize(boundary) + EOL.size bufsize = 16384 content_length -= boundary_size read_buffer = '' status = input.read(boundary_size, read_buffer) raise EOFError, "bad content body" unless status == boundary + EOL rx = /(?:#{EOL})?#{Regexp.quote boundary}(#{EOL}|--)/n loop { head = nil body = '' filename = content_type = name = nil until head && buf =~ rx if !head && i = buf.index(EOL+EOL) head = buf.slice!(0, i+2) # First \r\n buf.slice!(0, 2) # Second \r\n filename = head[/Content-Disposition:.* filename=(?:"((?:\\.|[^\"])*)"|([^;\s]*))/ni, 1] content_type = head[/Content-Type: (.*)#{EOL}/ni, 1] name = head[/Content-Disposition:.*\s+name="?([^\";]*)"?/ni, 1] || head[/Content-ID:\s*([^#{EOL}]*)/ni, 1] if content_type || filename body = Tempfile.new("RackMultipart") body.binmode if body.respond_to?(:binmode) end next end # Save the read body part. if head && (boundary_size+4 < buf.size) body << buf.slice!(0, buf.size - (boundary_size+4)) end c = input.read(bufsize < content_length ? bufsize : content_length, read_buffer) raise EOFError, "bad content body" if c.nil? || c.empty? buf << c content_length -= c.size end # Save the rest. if i = buf.index(rx) body << buf.slice!(0, i) buf.slice!(0, boundary_size+2) content_length = -1 if $1 == "--" end if filename == "" # filename is blank which means no file has been selected data = nil elsif filename body.rewind # Take the basename of the upload's original filename. # This handles the full Windows paths given by Internet Explorer # (and perhaps other broken user agents) without affecting # those which give the lone filename. filename =~ /^(?:.*[:\\\/])?(.*)/m filename = $1 data = {:filename => filename, :type => content_type, :name => name, :tempfile => body, :head => head} elsif !filename && content_type body.rewind # Generic multipart cases, not coming from a form data = {:type => content_type, :name => name, :tempfile => body, :head => head} else data = body end Utils.normalize_params(params, name, data) unless data.nil? # break if buf.empty? || content_length == -1 break if (buf.empty? && $1 != EOL) || content_length == -1 } input.rewind params end end end end end
0 件のコメント:
コメントを投稿