summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--.travis.yml30
-rw-r--r--COPYING2
-rw-r--r--HISTORY.md16
-rw-r--r--Rakefile2
-rw-r--r--example/protectedlobster.rb2
-rw-r--r--example/protectedlobster.ru2
-rw-r--r--lib/rack.rb2
-rw-r--r--lib/rack/auth/abstract/request.rb6
-rw-r--r--lib/rack/auth/digest/params.rb5
-rw-r--r--lib/rack/common_logger.rb2
-rw-r--r--lib/rack/deflater.rb2
-rw-r--r--lib/rack/directory.rb22
-rw-r--r--lib/rack/etag.rb4
-rw-r--r--lib/rack/file.rb11
-rw-r--r--lib/rack/handler.rb2
-rw-r--r--lib/rack/handler/webrick.rb8
-rw-r--r--lib/rack/mock.rb3
-rw-r--r--lib/rack/multipart/generator.rb2
-rw-r--r--lib/rack/multipart/parser.rb7
-rw-r--r--lib/rack/query_parser.rb34
-rw-r--r--lib/rack/reloader.rb3
-rw-r--r--lib/rack/request.rb46
-rw-r--r--lib/rack/server.rb3
-rw-r--r--lib/rack/session/abstract/id.rb16
-rw-r--r--lib/rack/static.rb2
-rw-r--r--lib/rack/urlmap.rb6
-rw-r--r--lib/rack/utils.rb21
-rw-r--r--rack.gemspec10
-rw-r--r--test/helper.rb5
-rw-r--r--test/multipart/filename_with_null_byte7
-rw-r--r--test/multipart/unity3d_wwwform11
-rw-r--r--test/spec_auth_basic.rb7
-rw-r--r--test/spec_directory.rb15
-rw-r--r--test/spec_etag.rb4
-rw-r--r--test/spec_events.rb133
-rw-r--r--test/spec_file.rb11
-rw-r--r--test/spec_multipart.rb14
-rw-r--r--test/spec_request.rb5
-rw-r--r--test/spec_response.rb64
-rw-r--r--test/spec_session_abstract_session_hash.rb45
-rw-r--r--test/spec_utils.rb27
-rw-r--r--test/spec_webrick.rb16
42 files changed, 554 insertions, 81 deletions
diff --git a/.travis.yml b/.travis.yml
index fa8db9d0..cf999939 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,30 @@
-before_install: sudo apt-get install lighttpd libfcgi-dev libmemcache-dev memcached
-install:
+language: ruby
+sudo: false
+cache:
+  - bundler
+  - apt
+
+services:
+  - memcached
+
+addons:
+  apt:
+    packages:
+      - lighttpd
+      - libfcgi-dev
+
+before_install:
   - gem env version | grep '^\(2\|1.\(8\|9\|[0-9][0-9]\)\)' || gem update --system
-  - bundle install --jobs=3 --retry=3
+  - gem list -i bundler || gem install bundler
+
 script: bundle exec rake ci
+
 rvm:
-  - 2.2.3
+  - 2.2.5
+  - 2.3.1
   - ruby-head
   - rbx-2
-  - jruby
-  - jruby-9000
+  - jruby-9.0.4.0
   - jruby-head
 
 notifications:
@@ -17,5 +33,5 @@ notifications:
 
 matrix:
   allow_failures:
-    - rvm: jruby
     - rvm: rbx-2
+    - rvm: jruby-head
diff --git a/COPYING b/COPYING
index e1047569..1f5c7013 100644
--- a/COPYING
+++ b/COPYING
@@ -1,4 +1,4 @@
-Copyright (c) 2007-2015 Christian Neukirchen <purl.org/net/chneukirchen>
+Copyright (c) 2007-2016 Christian Neukirchen <purl.org/net/chneukirchen>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to
diff --git a/HISTORY.md b/HISTORY.md
index b375af15..406d1758 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,3 +1,19 @@
+Sun Dec 4 18:48:03 2015  Jeremy Daer <jeremydaer@gmail.com>
+
+        * First-party "SameSite" cookies. Browsers omit SameSite cookies
+        from third-party requests, closing the door on many CSRF attacks.
+
+        Pass `same_site: true` (or `:strict`) to enable:
+            response.set_cookie 'foo', value: 'bar', same_site: true
+        or `same_site: :lax` to use Lax enforcement:
+            response.set_cookie 'foo', value: 'bar', same_site: :lax
+
+        Based on version 7 of the Same-site Cookies internet draft:
+        https://tools.ietf.org/html/draft-west-first-party-cookies-07
+
+        Thanks to Ben Toews (@mastahyeti) and Bob Long (@bobjflong) for
+        updating to drafts 5 and 7.
+
 Tue Nov  3 16:17:26 2015  Aaron Patterson <tenderlove@ruby-lang.org>
 
         * Add `Rack::Events` middleware for adding event based middleware:
diff --git a/Rakefile b/Rakefile
index d0e8b1da..c112f1da 100644
--- a/Rakefile
+++ b/Rakefile
@@ -36,7 +36,7 @@ task :officialrelease_really => %w[SPEC dist gem] do
 end
 
 def release
-  "rack-#{File.read("rack.gemspec")[/s.version *= *"(.*?)"/, 1]}"
+  "rack-" + File.read('lib/rack.rb')[/RELEASE += +([\"\'])([\d][\w\.]+)\1/, 2]
 end
 
 desc "Make binaries executable"
diff --git a/example/protectedlobster.rb b/example/protectedlobster.rb
index d904b4ce..26b23661 100644
--- a/example/protectedlobster.rb
+++ b/example/protectedlobster.rb
@@ -4,7 +4,7 @@ require 'rack/lobster'
 lobster = Rack::Lobster.new
 
 protected_lobster = Rack::Auth::Basic.new(lobster) do |username, password|
-  'secret' == password
+  Rack::Utils.secure_compare('secret', password)
 end
 
 protected_lobster.realm = 'Lobster 2.0'
diff --git a/example/protectedlobster.ru b/example/protectedlobster.ru
index b0da62f0..1ba48702 100644
--- a/example/protectedlobster.ru
+++ b/example/protectedlobster.ru
@@ -2,7 +2,7 @@ require 'rack/lobster'
 
 use Rack::ShowExceptions
 use Rack::Auth::Basic, "Lobster 2.0" do |username, password|
-  'secret' == password
+  Rack::Utils.secure_compare('secret', password)
 end
 
 run Rack::Lobster.new
diff --git a/lib/rack.rb b/lib/rack.rb
index 4a02eed1..f1417d2d 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -18,7 +18,7 @@ module Rack
     VERSION.join(".")
   end
 
-  RELEASE = "2.0.0.alpha"
+  RELEASE = "2.0.1"
 
   # Return the Rack release as a dotted string.
   def self.release
diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb
index 80d1c272..b738cc98 100644
--- a/lib/rack/auth/abstract/request.rb
+++ b/lib/rack/auth/abstract/request.rb
@@ -13,7 +13,11 @@ module Rack
       end
 
       def provided?
-        !authorization_key.nil?
+        !authorization_key.nil? && valid?
+      end
+
+      def valid?
+        !@env[authorization_key].nil?
       end
 
       def parts
diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb
index f35a7bab..2b226e62 100644
--- a/lib/rack/auth/digest/params.rb
+++ b/lib/rack/auth/digest/params.rb
@@ -17,7 +17,7 @@ module Rack
         end
 
         def self.split_header_value(str)
-          str.scan( /(\w+\=(?:"[^\"]+"|[^,]+))/n ).collect{ |v| v[0] }
+          str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n)
         end
 
         def initialize
@@ -38,7 +38,7 @@ module Rack
 
         def to_s
           map do |k, v|
-            "#{k}=" + (UNQUOTED.include?(k) ? v.to_s : quote(v))
+            "#{k}=" << (UNQUOTED.include?(k) ? v.to_s : quote(v))
           end.join(', ')
         end
 
@@ -50,4 +50,3 @@ module Rack
     end
   end
 end
-
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index 1ec8266d..ae410430 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -48,7 +48,7 @@ module Rack
         now.strftime("%d/%b/%Y:%H:%M:%S %z"),
         env[REQUEST_METHOD],
         env[PATH_INFO],
-        env[QUERY_STRING].empty? ? "" : "?"+env[QUERY_STRING],
+        env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
         env[HTTP_VERSION],
         status.to_s[0..3],
         length,
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index c656432e..f0fa5e4f 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -22,7 +22,7 @@ module Rack
     # [app] rack app instance
     # [options] hash of deflater options, i.e.
     #           'if' - a lambda enabling / disabling deflation based on returned boolean value
-    #                  e.g use Rack::Deflater, :if => lambda { |*, body| body.map(&:bytesize).reduce(0, :+) > 512 }
+    #                  e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
     #           'include' - a list of content types that should be compressed
     def initialize(app, options = {})
       @app = app
diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb
index 554f9c33..89cfe807 100644
--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -59,13 +59,21 @@ table { width:100%%; }
     def initialize(root, app=nil)
       @root = ::File.expand_path(root)
       @app = app || Rack::File.new(@root)
+      @head = Rack::Head.new(lambda { |env| get env })
     end
 
     def call(env)
+      # strip body if this is a HEAD call
+      @head.call env
+    end
+
+    def get(env)
       script_name = env[SCRIPT_NAME]
       path_info = Utils.unescape_path(env[PATH_INFO])
 
-      if forbidden = check_forbidden(path_info)
+      if bad_request = check_bad_request(path_info)
+        bad_request
+      elsif forbidden = check_forbidden(path_info)
         forbidden
       else
         path = ::File.join(@root, path_info)
@@ -73,6 +81,16 @@ table { width:100%%; }
       end
     end
 
+    def check_bad_request(path_info)
+      return if Utils.valid_path?(path_info)
+
+      body = "Bad Request\n"
+      size = body.bytesize
+      return [400, {CONTENT_TYPE => "text/plain",
+        CONTENT_LENGTH => size.to_s,
+        "X-Cascade" => "pass"}, [body]]
+    end
+
     def check_forbidden(path_info)
       return unless path_info.include? ".."
 
@@ -155,7 +173,7 @@ table { width:100%%; }
         return format % (int.to_f / size) if int >= size
       end
 
-      int.to_s + 'B'
+      "#{int}B"
     end
   end
 end
diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb
index 0ecc55ab..a0041062 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -65,10 +65,10 @@ module Rack
 
         body.each do |part|
           parts << part
-          (digest ||= Digest::MD5.new) << part unless part.empty?
+          (digest ||= Digest::SHA256.new) << part unless part.empty?
         end
 
-        [digest && digest.hexdigest, parts]
+        [digest && digest.hexdigest.byteslice(0, 32), parts]
       end
   end
 end
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 5b755f56..0a257b3d 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -2,6 +2,7 @@ require 'time'
 require 'rack/utils'
 require 'rack/mime'
 require 'rack/request'
+require 'rack/head'
 
 module Rack
   # Rack::File serves files below the +root+ directory given, according to the
@@ -22,17 +23,24 @@ module Rack
       @root = root
       @headers = headers
       @default_mime = default_mime
+      @head = Rack::Head.new(lambda { |env| get env })
     end
 
     def call(env)
+      # HEAD requests drop the response body, including 4xx error messages.
+      @head.call env
+    end
+
+    def get(env)
       request = Rack::Request.new env
       unless ALLOWED_VERBS.include? request.request_method
         return fail(405, "Method Not Allowed", {'Allow' => ALLOW_HEADER})
       end
 
       path_info = Utils.unescape_path request.path_info
-      clean_path_info = Utils.clean_path_info(path_info)
+      return fail(400, "Bad Request") unless Utils.valid_path?(path_info)
 
+      clean_path_info = Utils.clean_path_info(path_info)
       path = ::File.join(@root, clean_path_info)
 
       available = begin
@@ -131,6 +139,7 @@ module Rack
 
     def fail(status, body, headers = {})
       body += "\n"
+
       [
         status,
         {
diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb
index adaac5d6..70a77fa9 100644
--- a/lib/rack/handler.rb
+++ b/lib/rack/handler.rb
@@ -52,7 +52,7 @@ module Rack
       elsif ENV.include?("RACK_HANDLER")
         self.get(ENV["RACK_HANDLER"])
       else
-        pick ['thin', 'puma', 'webrick']
+        pick ['puma', 'thin', 'webrick']
       end
     end
 
diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb
index 95aa8927..d0fcd213 100644
--- a/lib/rack/handler/webrick.rb
+++ b/lib/rack/handler/webrick.rb
@@ -86,10 +86,11 @@ module Rack
         status, headers, body = @app.call(env)
         begin
           res.status = status.to_i
+          io_lambda = nil
           headers.each { |k, vs|
-            next if k.downcase == RACK_HIJACK
-
-            if k.downcase == "set-cookie"
+            if k == RACK_HIJACK
+              io_lambda = vs
+            elsif k.downcase == "set-cookie"
               res.cookies.concat vs.split("\n")
             else
               # Since WEBrick won't accept repeated headers,
@@ -98,7 +99,6 @@ module Rack
             end
           }
 
-          io_lambda = headers[RACK_HIJACK]
           if io_lambda
             rd, wr = IO.pipe
             res.body = rd
diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb
index a2fc2401..4ebc4df1 100644
--- a/lib/rack/mock.rb
+++ b/lib/rack/mock.rb
@@ -128,7 +128,7 @@ module Rack
         end
       end
 
-      empty_str = ''.force_encoding(Encoding::ASCII_8BIT)
+      empty_str = String.new.force_encoding(Encoding::ASCII_8BIT)
       opts[:input] ||= empty_str
       if String === opts[:input]
         rack_input = StringIO.new(opts[:input])
@@ -163,7 +163,6 @@ module Rack
     def initialize(status, headers, body, errors=StringIO.new(""))
       @original_headers = headers
       @errors           = errors.string if errors.respond_to?(:string)
-      @body_string      = nil
 
       super(body, status, headers)
     end
diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb
index 6367135f..f0b70a8d 100644
--- a/lib/rack/multipart/generator.rb
+++ b/lib/rack/multipart/generator.rb
@@ -22,7 +22,7 @@ module Rack
           else
             content_for_other(file, name)
           end
-        end.join + "--#{MULTIPART_BOUNDARY}--\r"
+        end.join << "--#{MULTIPART_BOUNDARY}--\r"
       end
 
       private
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index fe3381b9..d8cb3670 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -8,7 +8,7 @@ module Rack
       BUFSIZE = 16384
       TEXT_PLAIN = "text/plain"
       TEMPFILE_FACTORY = lambda { |filename, content_type|
-        Tempfile.new(["RackMultipart", ::File.extname(filename)])
+        Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
       }
 
       class BoundedIO # :nodoc:
@@ -26,7 +26,7 @@ module Rack
           str = if left < size
                   @io.read left
                 else
-                 @io.read size
+                  @io.read size
                 end
 
           if str
@@ -100,8 +100,6 @@ module Rack
               # Generic multipart cases, not coming from a form
               data = {:type => content_type,
                       :name => name, :tempfile => body, :head => head}
-            elsif !filename && data.empty?
-              return
             end
 
             yield data
@@ -347,6 +345,7 @@ module Rack
               k,v = param.split('=', 2)
               k.strip!
               v.strip!
+              v = v[1..-2] if v[0] == '"' && v[-1] == '"'
               encoding = Encoding.find v if k == CHARSET
             end
           end
diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb
index fe8fe2d7..be74bc06 100644
--- a/lib/rack/query_parser.rb
+++ b/lib/rack/query_parser.rb
@@ -79,16 +79,22 @@ module Rack
       raise RangeError if depth <= 0
 
       name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
-      k = $1 || ''
-      after = $' || ''
+      k = $1 || ''.freeze
+      after = $' || ''.freeze
 
-      return if k.empty?
+      if k.empty?
+        if !v.nil? && name == "[]".freeze
+          return Array(v)
+        else
+          return
+        end
+      end
 
-      if after == ""
+      if after == ''.freeze
         params[k] = v
-      elsif after == "["
+      elsif after == "[".freeze
         params[name] = v
-      elsif after == "[]"
+      elsif after == "[]".freeze
         params[k] ||= []
         raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
         params[k] << v
@@ -96,7 +102,7 @@ module Rack
         child_key = $1
         params[k] ||= []
         raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
-        if params_hash_type?(params[k].last) && !params[k].last.key?(child_key)
+        if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
           normalize_params(params[k].last, child_key, v, depth - 1)
         else
           params[k] << normalize_params(make_params, child_key, v, depth - 1)
@@ -107,7 +113,7 @@ module Rack
         params[k] = normalize_params(params[k], after, v, depth - 1)
       end
 
-      return params
+      params
     end
 
     def make_params
@@ -128,6 +134,18 @@ module Rack
       obj.kind_of?(@params_class)
     end
 
+    def params_hash_has_key?(hash, key)
+      return false if key =~ /\[\]/
+
+      key.split(/[\[\]]+/).inject(hash) do |h, part|
+        next h if part == ''
+        return false unless params_hash_type?(h) && h.key?(part)
+        h[part]
+      end
+
+      true
+    end
+
     def unescape(s)
       Utils.unescape(s)
     end
diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb
index 5f643592..296dd6a1 100644
--- a/lib/rack/reloader.rb
+++ b/lib/rack/reloader.rb
@@ -26,6 +26,7 @@ module Rack
       @last = (Time.now - cooldown)
       @cache = {}
       @mtimes = {}
+      @reload_mutex = Mutex.new
 
       extend backend
     end
@@ -33,7 +34,7 @@ module Rack
     def call(env)
       if @cooldown and Time.now > @last + @cooldown
         if Thread.list.size > 1
-          Thread.exclusive{ reload! }
+          @reload_mutex.synchronize{ reload! }
         else
           reload!
         end
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 50dba475..5bf3eb17 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -16,23 +16,6 @@ module Rack
       super(env)
     end
 
-    # shortcut for <tt>request.params[key]</tt>
-    def [](key)
-      params[key.to_s]
-    end
-
-    # shortcut for <tt>request.params[key] = value</tt>
-    #
-    # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
-    def []=(key, value)
-      params[key.to_s] = value
-    end
-
-    # like Hash#values_at
-    def values_at(*keys)
-      keys.map{|key| params[key] }
-    end
-
     def params
       @params ||= super
     end
@@ -160,7 +143,7 @@ module Rack
 
       def session
         fetch_header(RACK_SESSION) do |k|
-          set_header RACK_SESSION, {}
+          set_header RACK_SESSION, default_session
         end
       end
 
@@ -437,8 +420,35 @@ module Rack
         ip =~ /\A127\.0\.0\.1\Z|\A(10|172\.(1[6-9]|2[0-9]|30|31)|192\.168)\.|\A::1\Z|\Afd[0-9a-f]{2}:.+|\Alocalhost\Z|\Aunix\Z|\Aunix:/i
       end
 
+      # shortcut for <tt>request.params[key]</tt>
+      def [](key)
+        if $verbose
+          warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead")
+        end
+
+        params[key.to_s]
+      end
+
+      # shortcut for <tt>request.params[key] = value</tt>
+      #
+      # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
+      def []=(key, value)
+        if $verbose
+          warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead")
+        end
+
+        params[key.to_s] = value
+      end
+
+      # like Hash#values_at
+      def values_at(*keys)
+        keys.map { |key| params[key] }
+      end
+
       private
 
+      def default_session; {}; end
+
       def parse_http_accept_header(header)
         header.to_s.split(/\s*,\s*/).map do |part|
           attribute, parameters = part.split(/\s*;\s*/, 2)
diff --git a/lib/rack/server.rb b/lib/rack/server.rb
index 690f1096..1f37aacb 100644
--- a/lib/rack/server.rb
+++ b/lib/rack/server.rb
@@ -1,4 +1,5 @@
 require 'optparse'
+require 'fileutils'
 
 
 module Rack
@@ -359,7 +360,7 @@ module Rack
 
       def write_pid
         ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") }
-        at_exit { ::File.delete(options[:pid]) if ::File.exist?(options[:pid]) }
+        at_exit { ::FileUtils.rm_f(options[:pid]) }
       rescue Errno::EEXIST
         check_pid!
         retry
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
index f8536f46..ca1a2628 100644
--- a/lib/rack/session/abstract/id.rb
+++ b/lib/rack/session/abstract/id.rb
@@ -18,6 +18,8 @@ module Rack
         include Enumerable
         attr_writer :id
 
+        Unspecified = Object.new
+
         def self.find(req)
           req.get_header RACK_SESSION
         end
@@ -54,7 +56,15 @@ module Rack
           load_for_read!
           @data[key.to_s]
         end
-        alias :fetch :[]
+
+        def fetch(key, default=Unspecified, &block)
+          load_for_read!
+          if default == Unspecified
+            @data.fetch(key.to_s, &block)
+          else
+            @data.fetch(key.to_s, default, &block)
+          end
+        end
 
         def has_key?(key)
           load_for_read!
@@ -124,10 +134,12 @@ module Rack
         end
 
         def keys
+          load_for_read!
           @data.keys
         end
 
         def values
+          load_for_read!
           @data.values
         end
 
@@ -198,7 +210,7 @@ module Rack
           :sidbits =>       128,
           :cookie_only =>   true,
           :secure_random => ::SecureRandom
-        }
+        }.freeze
 
         attr_reader :key, :default_options, :sid_secure
 
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 90d61009..17f47649 100644
--- a/lib/rack/static.rb
+++ b/lib/rack/static.rb
@@ -93,7 +93,7 @@ module Rack
       # HTTP Headers
       @header_rules = options[:header_rules] || []
       # Allow for legacy :cache_control option while prioritizing global header_rules setting
-      @header_rules.insert(0, [:all, {CACHE_CONTROL => options[:cache_control]}]) if options[:cache_control]
+      @header_rules.unshift([:all, {CACHE_CONTROL => options[:cache_control]}]) if options[:cache_control]
 
       @file_server = Rack::File.new(root)
     end
diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb
index 572b2151..510b4b50 100644
--- a/lib/rack/urlmap.rb
+++ b/lib/rack/urlmap.rb
@@ -47,11 +47,13 @@ module Rack
       server_name = env[SERVER_NAME]
       server_port = env[SERVER_PORT]
 
+      is_same_server = casecmp?(http_host, server_name) ||
+                       casecmp?(http_host, "#{server_name}:#{server_port}")
+
       @mapping.each do |host, location, match, app|
         unless casecmp?(http_host, host) \
             || casecmp?(server_name, host) \
-            || (!host && (casecmp?(http_host, server_name) ||
-                          casecmp?(http_host, "#{server_name}:#{server_port}")))
+            || (!host && is_same_server)
           next
         end
 
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 616edbeb..7b842125 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -248,12 +248,23 @@ module Rack
           rfc2822(value[:expires].clone.gmtime) if value[:expires]
         secure = "; secure"  if value[:secure]
         httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
+        same_site =
+          case value[:same_site]
+          when false, nil
+            nil
+          when :lax, 'Lax', :Lax
+            '; SameSite=Lax'.freeze
+          when true, :strict, 'Strict', :Strict
+            '; SameSite=Strict'.freeze
+          else
+            raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
+          end
         value = value[:value]
       end
       value = [value] unless Array === value
 
       cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
-        "#{path}#{max_age}#{expires}#{secure}#{httponly}"
+        "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
 
       case header
       when nil, ''
@@ -550,6 +561,7 @@ module Rack
       428 => 'Precondition Required',
       429 => 'Too Many Requests',
       431 => 'Request Header Fields Too Large',
+      451 => 'Unavailable for Legal Reasons',
       500 => 'Internal Server Error',
       501 => 'Not Implemented',
       502 => 'Bad Gateway',
@@ -597,5 +609,12 @@ module Rack
     end
     module_function :clean_path_info
 
+    NULL_BYTE = "\0".freeze
+
+    def valid_path?(path)
+      path.valid_encoding? && !path.include?(NULL_BYTE)
+    end
+    module_function :valid_path?
+
   end
 end
diff --git a/rack.gemspec b/rack.gemspec
index 7ec3f607..259ae3ab 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -16,20 +16,18 @@ Also see http://rack.github.io/.
 EOF
 
   s.files           = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
-                        %w(COPYING KNOWN-ISSUES rack.gemspec Rakefile README.rdoc SPEC)
+                        %w(COPYING rack.gemspec Rakefile README.rdoc SPEC)
   s.bindir          = 'bin'
   s.executables     << 'rackup'
   s.require_path    = 'lib'
-  s.extra_rdoc_files = ['README.rdoc', 'KNOWN-ISSUES', 'HISTORY.md']
+  s.extra_rdoc_files = ['README.rdoc', 'HISTORY.md']
   s.test_files      = Dir['test/spec_*.rb']
 
-  s.author          = 'Christian Neukirchen'
-  s.email           = 'chneukirchen@gmail.com'
+  s.author          = 'Aaron Patterson'
+  s.email           = 'tenderlove@ruby-lang.org'
   s.homepage        = 'http://rack.github.io/'
   s.required_ruby_version = '>= 2.2.2'
 
-  s.add_dependency 'json'
-
   s.add_development_dependency 'minitest', "~> 5.0"
   s.add_development_dependency 'minitest-sprint'
   s.add_development_dependency 'concurrent-ruby'
diff --git a/test/helper.rb b/test/helper.rb
index 64f6d30f..aa9c0e0a 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -2,7 +2,10 @@ require 'minitest/autorun'
 
 module Rack
   class TestCase < Minitest::Test
-    if `which lighttpd` && !$?.success?
+    # Check for Lighttpd and launch it for tests if available.
+    `which lighttpd`
+
+    if $?.success?
       begin
         # Keep this first.
         LIGHTTPD_PID = fork {
diff --git a/test/multipart/filename_with_null_byte b/test/multipart/filename_with_null_byte
new file mode 100644
index 00000000..961d44c4
--- /dev/null
+++ b/test/multipart/filename_with_null_byte
@@ -0,0 +1,7 @@
+--AaB03x
+Content-Type: image/jpeg
+Content-Disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
+Content-Description: a complete map of the human genome
+
+contents
+--AaB03x--
diff --git a/test/multipart/unity3d_wwwform b/test/multipart/unity3d_wwwform
new file mode 100644
index 00000000..1089a690
--- /dev/null
+++ b/test/multipart/unity3d_wwwform
@@ -0,0 +1,11 @@
+--AaB03x
+Content-Type: text/plain; charset="utf-8"
+Content-disposition: form-data; name="user_sid"
+
+bbf14f82-d2aa-4c07-9fb8-ca6714a7ea97
+--AaB03x
+Content-Type: image/png; charset=UTF-8
+Content-disposition: form-data; name="file";
+filename="b67879ed-bfed-4491-a8cc-f99cca769f94.png"
+
+--AaB03x
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb
index 1e19bf66..45d28576 100644
--- a/test/spec_auth_basic.rb
+++ b/test/spec_auth_basic.rb
@@ -75,6 +75,13 @@ describe Rack::Auth::Basic do
     end
   end
 
+  it 'return 401 Bad Request for a nil authorization header' do
+    request 'HTTP_AUTHORIZATION' => nil do |response|
+      response.must_be :client_error?
+      response.status.must_equal 401
+    end
+  end
+
   it 'takes realm as optional constructor arg' do
     app = Rack::Auth::Basic.new(unprotected_app, realm) { true }
     realm.must_equal app.realm
diff --git a/test/spec_directory.rb b/test/spec_directory.rb
index 1a97e9e5..42bdea9f 100644
--- a/test/spec_directory.rb
+++ b/test/spec_directory.rb
@@ -63,6 +63,13 @@ describe Rack::Directory do
     assert_match(res, /passed!/)
   end
 
+  it "serve uri with URL encoded null byte (%00) in filenames" do
+    res = Rack::MockRequest.new(Rack::Lint.new(app))
+      .get("/cgi/test%00")
+
+    res.must_be :bad_request?
+  end
+
   it "not allow directory traversal" do
     res = Rack::MockRequest.new(Rack::Lint.new(app)).
       get("/cgi/../test")
@@ -130,4 +137,12 @@ describe Rack::Directory do
     res = mr.get("/script-path/cgi/test+directory/test+file")
     res.must_be :ok?
   end
+
+  it "return error when file not found for head request" do
+    res = Rack::MockRequest.new(Rack::Lint.new(app)).
+      head("/cgi/missing")
+
+    res.must_be :not_found?
+    res.body.must_be :empty?
+  end
 end
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index 03680602..10ee2bd0 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -22,13 +22,13 @@ describe Rack::ETag do
   it "set ETag if none is set if status is 200" do
     app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
+    response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
   end
 
   it "set ETag if none is set if status is 201" do
     app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
+    response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
   end
 
   it "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do
diff --git a/test/spec_events.rb b/test/spec_events.rb
new file mode 100644
index 00000000..7fc7b055
--- /dev/null
+++ b/test/spec_events.rb
@@ -0,0 +1,133 @@
+require 'helper'
+require 'rack/events'
+
+module Rack
+  class TestEvents < Rack::TestCase
+    class EventMiddleware
+      attr_reader :events
+
+      def initialize events
+        @events = events
+      end
+
+      def on_start req, res
+        events << [self, __method__]
+      end
+
+      def on_commit req, res
+        events << [self, __method__]
+      end
+
+      def on_send req, res
+        events << [self, __method__]
+      end
+
+      def on_finish req, res
+        events << [self, __method__]
+      end
+
+      def on_error req, res, e
+        events << [self, __method__]
+      end
+    end
+
+    def test_events_fire
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| events << [app, :call]; ret }
+      se = EventMiddleware.new events
+      e = Events.new app, [se]
+      triple = e.call({})
+      response_body = []
+      triple[2].each { |x| response_body << x }
+      triple[2].close
+      triple[2] = response_body
+      assert_equal ret, triple
+      assert_equal [[se, :on_start],
+                    [app, :call],
+                    [se, :on_commit],
+                    [se, :on_send],
+                    [se, :on_finish],
+      ], events
+    end
+
+    def test_send_and_finish_are_not_run_until_body_is_sent
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| events << [app, :call]; ret }
+      se = EventMiddleware.new events
+      e = Events.new app, [se]
+      triple = e.call({})
+      assert_equal [[se, :on_start],
+                    [app, :call],
+                    [se, :on_commit],
+      ], events
+    end
+
+    def test_send_is_called_on_each
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| events << [app, :call]; ret }
+      se = EventMiddleware.new events
+      e = Events.new app, [se]
+      triple = e.call({})
+      triple[2].each { |x| }
+      assert_equal [[se, :on_start],
+                    [app, :call],
+                    [se, :on_commit],
+                    [se, :on_send],
+      ], events
+    end
+
+    def test_finish_is_called_on_close
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| events << [app, :call]; ret }
+      se = EventMiddleware.new events
+      e = Events.new app, [se]
+      triple = e.call({})
+      triple[2].each { |x| }
+      triple[2].close
+      assert_equal [[se, :on_start],
+                    [app, :call],
+                    [se, :on_commit],
+                    [se, :on_send],
+                    [se, :on_finish],
+      ], events
+    end
+
+    def test_finish_is_called_in_reverse_order
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| events << [app, :call]; ret }
+      se1 = EventMiddleware.new events
+      se2 = EventMiddleware.new events
+      se3 = EventMiddleware.new events
+
+      e = Events.new app, [se1, se2, se3]
+      triple = e.call({})
+      triple[2].each { |x| }
+      triple[2].close
+
+      groups = events.group_by { |x| x.last }
+      assert_equal groups[:on_start].map(&:first), groups[:on_finish].map(&:first).reverse
+      assert_equal groups[:on_commit].map(&:first), groups[:on_finish].map(&:first)
+      assert_equal groups[:on_send].map(&:first), groups[:on_finish].map(&:first)
+    end
+
+    def test_finish_is_called_if_there_is_an_exception
+      events = []
+      ret = [200, {}, []]
+      app = lambda { |env| raise }
+      se = EventMiddleware.new events
+      e = Events.new app, [se]
+      assert_raises(RuntimeError) do
+        e.call({})
+      end
+      assert_equal [[se, :on_start],
+                    [se, :on_error],
+                    [se, :on_finish],
+      ], events
+    end
+  end
+end
diff --git a/test/spec_file.rb b/test/spec_file.rb
index 2d0919a9..3106e629 100644
--- a/test/spec_file.rb
+++ b/test/spec_file.rb
@@ -68,6 +68,11 @@ describe Rack::File do
     assert_match(res, /ruby/)
   end
 
+  it "serve uri with URL encoded null byte (%00) in filenames" do
+    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00")
+    res.must_be :bad_request?
+  end
+
   it "allow safe directory traversal" do
     req = Rack::MockRequest.new(file(DOCROOT))
 
@@ -237,4 +242,10 @@ describe Rack::File do
     res['Content-Type'].must_equal nil
   end
 
+  it "return error when file not found for head request" do
+    res = Rack::MockRequest.new(file(DOCROOT)).head("/cgi/missing")
+    res.must_be :not_found?
+    res.body.must_be :empty?
+  end
+
 end
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 9e8a6140..02b86bed 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -72,6 +72,13 @@ describe Rack::Multipart do
     end
   end
 
+  it "handles quoted encodings" do
+    # See #905
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:unity3d_wwwform))
+    params = Rack::Multipart.parse_multipart(env)
+    params['user_sid'].encoding.must_equal Encoding::UTF_8
+  end
+
   it "raise RangeError if the key space is exhausted" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
 
@@ -88,6 +95,7 @@ describe Rack::Multipart do
     env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR"
     params = Rack::Multipart.parse_multipart(env)
     params['profile']['bio'].must_include 'hello'
+    params['profile'].keys.must_include 'public_email'
   end
 
   it "reject insanely long boundaries" do
@@ -297,6 +305,12 @@ describe Rack::Multipart do
     params["files"][:filename].must_equal "bob's flowers.jpg"
   end
 
+  it "parse multipart form with a null byte in the filename" do
+    env = Rack::MockRequest.env_for '/', multipart_fixture(:filename_with_null_byte)
+    params = Rack::Multipart.parse_multipart(env)
+    params["files"][:filename].must_equal "flowers.exe\u0000.jpg"
+  end
+
   it "not include file params if no file was selected" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
     params = Rack::Multipart.parse_multipart(env)
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 2062e246..74f2fa87 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -1305,6 +1305,11 @@ EOF
     req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal nil
   end
 
+  it "sets the default session to an empty hash" do
+    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+    assert_equal Hash.new, req.session
+  end
+
   class MyRequest < Rack::Request
     def params
       {:foo => "bar"}
diff --git a/test/spec_response.rb b/test/spec_response.rb
index de0670da..02e51435 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -115,6 +115,70 @@ describe Rack::Response do
     response["Set-Cookie"].must_equal "foo=bar"
   end
 
+  it "can set SameSite cookies with symbol value :lax" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => :lax}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax"
+  end
+
+  it "can set SameSite cookies with symbol value :Lax" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => :lax}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax"
+  end
+
+  it "can set SameSite cookies with string value 'Lax'" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => "Lax"}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax"
+  end
+
+  it "can set SameSite cookies with boolean value true" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => true}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+  end
+
+  it "can set SameSite cookies with symbol value :strict" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => :strict}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+  end
+
+  it "can set SameSite cookies with symbol value :Strict" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => :Strict}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+  end
+
+  it "can set SameSite cookies with string value 'Strict'" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => "Strict"}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+  end
+
+  it "validates the SameSite option value" do
+    response = Rack::Response.new
+    lambda {
+      response.set_cookie "foo", {:value => "bar", :same_site => "Foo"}
+    }.must_raise(ArgumentError).
+      message.must_match(/Invalid SameSite value: "Foo"/)
+  end
+
+  it "can set SameSite cookies with symbol value" do
+    response = Rack::Response.new
+    response.set_cookie "foo", {:value => "bar", :same_site => :Strict}
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+  end
+
+  [ nil, false ].each do |non_truthy|
+    it "omits SameSite attribute given a #{non_truthy.inspect} value" do
+      response = Rack::Response.new
+      response.set_cookie "foo", {:value => "bar", :same_site => non_truthy}
+      response["Set-Cookie"].must_equal "foo=bar"
+    end
+  end
+
   it "can delete cookies" do
     response = Rack::Response.new
     response.set_cookie "foo", "bar"
diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb
new file mode 100644
index 00000000..76b34a01
--- /dev/null
+++ b/test/spec_session_abstract_session_hash.rb
@@ -0,0 +1,45 @@
+require 'minitest/autorun'
+require 'rack/session/abstract/id'
+
+describe Rack::Session::Abstract::SessionHash do
+  attr_reader :hash
+
+  def setup
+    super
+    store = Class.new do
+      def load_session(req)
+        ["id", {foo: :bar, baz: :qux}]
+      end
+      def session_exists?(req)
+        true
+      end
+    end
+    @hash = Rack::Session::Abstract::SessionHash.new(store.new, nil)
+  end
+
+  it "returns keys" do
+    assert_equal ["foo", "baz"], hash.keys
+  end
+
+  it "returns values" do
+    assert_equal [:bar, :qux], hash.values
+  end
+
+  describe "#fetch" do
+    it "returns value for a matching key" do
+      assert_equal :bar, hash.fetch(:foo)
+    end
+
+    it "works with a default value" do
+      assert_equal :default, hash.fetch(:unknown, :default)
+    end
+
+    it "works with a block" do
+      assert_equal :default, hash.fetch(:unkown) { :default }
+    end
+
+    it "it raises when fetching unknown keys without defaults" do
+      lambda { hash.fetch(:unknown) }.must_raise KeyError
+    end
+  end
+end
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 17a12115..e5d4d244 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -206,6 +206,14 @@ describe Rack::Utils do
     Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3").
       must_equal "x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}
 
+    Rack::Utils.parse_nested_query("x[][y]=1&x[][z][w]=a&x[][y]=2&x[][z][w]=b").
+      must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}]
+    Rack::Utils.parse_nested_query("x[][z][w]=a&x[][y]=1&x[][z][w]=b&x[][y]=2").
+      must_equal "x" => [{"y" => "1", "z" => {"w" => "a"}}, {"y" => "2", "z" => {"w" => "b"}}]
+
+    Rack::Utils.parse_nested_query("data[books][][data][page]=1&data[books][][data][page]=2").
+      must_equal "data" => { "books" => [{ "data" => { "page" => "1"}}, { "data" => { "page" => "2"}}] }
+
     lambda { Rack::Utils.parse_nested_query("x[y]=1&x[y]z=2") }.
       must_raise(Rack::Utils::ParameterTypeError).
       message.must_equal "expected Hash (got String) for param `y'"
@@ -223,6 +231,18 @@ describe Rack::Utils do
       message.must_equal "invalid byte sequence in UTF-8"
   end
 
+  it "only moves to a new array when the full key has been seen" do
+    Rack::Utils.parse_nested_query("x[][y][][z]=1&x[][y][][w]=2").
+      must_equal "x" => [{"y" => [{"z" => "1", "w" => "2"}]}]
+
+    Rack::Utils.parse_nested_query(
+      "x[][id]=1&x[][y][a]=5&x[][y][b]=7&x[][z][id]=3&x[][z][w]=0&x[][id]=2&x[][y][a]=6&x[][y][b]=8&x[][z][id]=4&x[][z][w]=0"
+    ).must_equal "x" => [
+        {"id" => "1", "y" => {"a" => "5", "b" => "7"}, "z" => {"id" => "3", "w" => "0"}},
+        {"id" => "2", "y" => {"a" => "6", "b" => "8"}, "z" => {"id" => "4", "w" => "0"}},
+      ]
+  end
+
   it "allow setting the params hash class to use for parsing query strings" do
     begin
       default_parser = Rack::Utils.default_query_parser
@@ -300,13 +320,15 @@ describe Rack::Utils do
       must_equal 'x[y][][z]=1&x[y][][z]=2'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => 'a' }, { 'z' => '2', 'w' => '3' }] }).
       must_equal 'x[y][][z]=1&x[y][][w]=a&x[y][][z]=2&x[y][][w]=3'
+    Rack::Utils.build_nested_query({"foo" => ["1", ["2"]]}).
+      must_equal 'foo[]=1&foo[][]=2'
 
     lambda { Rack::Utils.build_nested_query("foo=bar") }.
       must_raise(ArgumentError).
       message.must_equal "value must be a Hash"
   end
 
-  should 'perform the inverse function of #parse_nested_query' do
+  it 'performs the inverse function of #parse_nested_query' do
     [{"foo" => nil, "bar" => ""},
       {"foo" => "bar", "baz" => ""},
       {"foo" => ["1", "2"]},
@@ -323,7 +345,8 @@ describe Rack::Utils do
       {"x" => {"y" => [{"v" => {"w" => "1"}}]}},
       {"x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}},
       {"x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}},
-      {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}}
+      {"x" => {"y" => [{"z" => "1", "w" => "a"}, {"z" => "2", "w" => "3"}]}},
+      {"foo" => ["1", ["2"]]},
     ].each { |params|
       qs = Rack::Utils.build_nested_query(params)
       Rack::Utils.parse_nested_query(qs).must_equal params
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
index 8e0360d2..4a10c1ca 100644
--- a/test/spec_webrick.rb
+++ b/test/spec_webrick.rb
@@ -17,6 +17,19 @@ describe Rack::Handler::WEBrick do
     Rack::Lint.new(TestRequest.new)
   @thread = Thread.new { @server.start }
   trap(:INT) { @server.shutdown }
+  @status_thread = Thread.new do
+    seconds = 10
+    wait_time = 0.1
+    until is_running? || seconds <= 0
+      seconds -= wait_time
+      sleep wait_time
+    end
+    raise "Server never reached status 'Running'" unless is_running?
+  end
+  end
+
+  def is_running?
+    @server.status == :Running
   end
 
   it "respond" do
@@ -158,7 +171,7 @@ describe Rack::Handler::WEBrick do
     Rack::Lint.new(lambda{ |req|
       [
         200,
-        {"rack.hijack" => io_lambda},
+        [ [ "rack.hijack", io_lambda ] ],
         [""]
       ]
     })
@@ -188,6 +201,7 @@ describe Rack::Handler::WEBrick do
   end
 
   after do
+  @status_thread.join
   @server.shutdown
   @thread.join
   end