summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--.travis.yml31
-rw-r--r--COPYING2
-rw-r--r--Gemfile4
-rw-r--r--HISTORY.md64
-rw-r--r--NEWS.md (renamed from NEWS)0
-rw-r--r--README.rdoc4
-rw-r--r--Rakefile2
-rw-r--r--SECURITY_POLICY.md2
-rw-r--r--SPEC4
-rw-r--r--example/protectedlobster.rb2
-rw-r--r--example/protectedlobster.ru2
-rw-r--r--lib/rack.rb3
-rw-r--r--lib/rack/auth/abstract/request.rb6
-rw-r--r--lib/rack/auth/digest/params.rb5
-rw-r--r--lib/rack/builder.rb11
-rw-r--r--lib/rack/chunked.rb2
-rw-r--r--lib/rack/common_logger.rb9
-rw-r--r--lib/rack/deflater.rb70
-rw-r--r--lib/rack/directory.rb22
-rw-r--r--lib/rack/etag.rb9
-rw-r--r--lib/rack/events.rb154
-rw-r--r--lib/rack/file.rb13
-rw-r--r--lib/rack/handler.rb2
-rw-r--r--lib/rack/handler/scgi.rb3
-rw-r--r--lib/rack/handler/webrick.rb8
-rw-r--r--lib/rack/lint.rb4
-rw-r--r--lib/rack/lock.rb15
-rw-r--r--lib/rack/method_override.rb3
-rw-r--r--lib/rack/mock.rb21
-rw-r--r--lib/rack/multipart/generator.rb2
-rw-r--r--lib/rack/multipart/parser.rb27
-rw-r--r--lib/rack/query_parser.rb34
-rw-r--r--lib/rack/reloader.rb3
-rw-r--r--lib/rack/request.rb97
-rw-r--r--lib/rack/response.rb46
-rw-r--r--lib/rack/runtime.rb18
-rw-r--r--lib/rack/sendfile.rb8
-rw-r--r--lib/rack/server.rb5
-rw-r--r--lib/rack/session/abstract/id.rb23
-rw-r--r--lib/rack/session/cookie.rb4
-rw-r--r--lib/rack/static.rb2
-rw-r--r--lib/rack/urlmap.rb6
-rw-r--r--lib/rack/utils.rb71
-rw-r--r--rack.gemspec10
-rw-r--r--test/helper.rb41
-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_body_proxy.rb3
-rw-r--r--test/spec_builder.rb21
-rw-r--r--test/spec_cgi.rb10
-rw-r--r--test/spec_chunked.rb2
-rw-r--r--test/spec_conditional_get.rb4
-rw-r--r--test/spec_content_length.rb8
-rw-r--r--test/spec_content_type.rb2
-rw-r--r--test/spec_deflater.rb81
-rw-r--r--test/spec_directory.rb15
-rw-r--r--test/spec_etag.rb6
-rw-r--r--test/spec_events.rb133
-rw-r--r--test/spec_fastcgi.rb14
-rw-r--r--test/spec_file.rb30
-rw-r--r--test/spec_lint.rb4
-rw-r--r--test/spec_lock.rb12
-rw-r--r--test/spec_media_type.rb2
-rw-r--r--test/spec_method_override.rb21
-rw-r--r--test/spec_mime.rb2
-rw-r--r--test/spec_mock.rb93
-rw-r--r--test/spec_multipart.rb27
-rw-r--r--test/spec_request.rb83
-rw-r--r--test/spec_response.rb168
-rw-r--r--test/spec_sendfile.rb35
-rw-r--r--test/spec_server.rb4
-rw-r--r--test/spec_session_abstract_session_hash.rb45
-rw-r--r--test/spec_session_cookie.rb26
-rw-r--r--test/spec_session_memcache.rb10
-rw-r--r--test/spec_session_pool.rb2
-rw-r--r--test/spec_static.rb4
-rw-r--r--test/spec_utils.rb117
-rw-r--r--test/spec_webrick.rb26
79 files changed, 1507 insertions, 402 deletions
diff --git a/.travis.yml b/.travis.yml
index fa8db9d0..016b8829 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,31 @@
-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.7
+  - 2.3.4
+  - 2.4.1
   - ruby-head
   - rbx-2
-  - jruby
-  - jruby-9000
+  - jruby-9.0.4.0
   - jruby-head
 
 notifications:
@@ -17,5 +34,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/Gemfile b/Gemfile
index 2a767746..8741019e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -19,3 +19,7 @@ group :extra do
   gem 'memcache-client'
   gem 'thin', :platforms => c_platforms
 end
+
+group :doc do
+  gem 'rdoc'
+end
diff --git a/HISTORY.md b/HISTORY.md
index 934cca49..406d1758 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,3 +1,50 @@
+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:
+        middleware that does not care about the response body, but only cares
+        about doing work at particular points in the request / response
+        lifecycle.
+
+Thu Oct  8 14:58:46 2015  Aaron Patterson <tenderlove@ruby-lang.org>
+
+        * Add `Rack::Request#authority` to calculate the authority under which
+        the response is being made (this will be handy for h2 pushes).
+
+Tue Oct  6 13:19:04 2015  Aaron Patterson <tenderlove@ruby-lang.org>
+
+        * Add `Rack::Response::Helpers#cache_control` and `cache_control=`.
+        Use this for setting cache control headers on your response objects.
+
+Tue Oct  6 13:12:21 2015  Aaron Patterson <tenderlove@ruby-lang.org>
+
+        * Add `Rack::Response::Helpers#etag` and `etag=`.  Use this for
+        setting etag values on the response.
+
+Sun Oct 3 18:25:03 2015  Jeremy Daer <jeremydaer@gmail.com>
+
+        * Introduce `Rack::Response::Helpers#add_header` to add a value to a
+        multi-valued response header. Implemented in terms of other
+        `Response#*_header` methods, so it's available to any response-like
+        class that includes the `Helpers` module.
+
+        * Add `Rack::Request#add_header` to match.
+
 Fri Sep  4 18:34:53 2015  Aaron Patterson <tenderlove@ruby-lang.org>
 
         * `Rack::Session::Abstract::ID` IS DEPRECATED.  Please switch to
@@ -31,6 +78,17 @@ Thu Aug 27 15:43:48 2015  Aaron Patterson <tenderlove@ruby-lang.org>
         * Tempfiles are automatically closed in the case that there were too
         many posted.
 
+Thu Aug 27 11:00:03 2015  Aaron Patterson <tenderlove@ruby-lang.org>
+
+        * Added methods for manipulating response headers that don't assume
+        they're stored as a Hash. Response-like classes may include the
+        Rack::Response::Helpers module if they define these methods:
+
+          * Rack::Response#has_header?
+          * Rack::Response#get_header
+          * Rack::Response#set_header
+          * Rack::Response#delete_header
+
 Mon Aug 24 18:05:23 2015  Aaron Patterson <tenderlove@ruby-lang.org>
 
         * Introduce Util.get_byte_ranges that will parse the value of the
@@ -55,10 +113,12 @@ Thu Aug 20 16:20:58 2015  Aaron Patterson <tenderlove@ruby-lang.org>
         data set as CGI parameters, and just any arbitrary data the user wants
         to associate with a particular request.  New methods:
 
-          * Rack::Request#get_header
-          * Rack::Request#set_header
           * Rack::Request#has_header?
+          * Rack::Request#get_header
+          * Rack::Request#fetch_header
           * Rack::Request#each_header
+          * Rack::Request#set_header
+          * Rack::Request#delete_header
 
 Thu Jun 18 16:00:05 2015  Aaron Patterson <tenderlove@ruby-lang.org>
 
diff --git a/NEWS b/NEWS.md
index a643ddb9..a643ddb9 100644
--- a/NEWS
+++ b/NEWS.md
diff --git a/README.rdoc b/README.rdoc
index bedcda99..8c1e2f01 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -41,9 +41,11 @@ These frameworks include Rack adapters in their distributions:
 * Coset
 * Espresso
 * Halcyon
+* Hanami
 * Mack
 * Maveric
 * Merb
+* Padrino
 * Racktools::SimpleApplication
 * Ramaze
 * Ruby on Rails
@@ -295,7 +297,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 == Links
 
-Rack:: <http://rack.github.io/>
+Rack:: <https://rack.github.io/>
 Official Rack repositories:: <https://github.com/rack>
 Rack Bug Tracking:: <https://github.com/rack/rack/issues>
 rack-devel mailing list:: <https://groups.google.com/group/rack-devel>
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/SECURITY_POLICY.md b/SECURITY_POLICY.md
index 1ae1a95f..844d6969 100644
--- a/SECURITY_POLICY.md
+++ b/SECURITY_POLICY.md
@@ -64,4 +64,4 @@ No one outside the core team, the initial reporter or vendor-sec will be notifie
 
 ## Comments on this Policy
 
-If you have any suggestions to improve this policy, please send an email the core teamat [rack-core@googlegroups.com](https://groups.google.com/group/rack-core).
+If you have any suggestions to improve this policy, please send an email the core team at [rack-core@googlegroups.com](https://groups.google.com/group/rack-core).
diff --git a/SPEC b/SPEC
index 7e3af40a..9b278846 100644
--- a/SPEC
+++ b/SPEC
@@ -237,10 +237,10 @@ consisting of lines (for multiple header values, e.g. multiple
 The lines must not contain characters below 037.
 === The Content-Type
 There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-204, 205 or 304.
+204 or 304.
 === The Content-Length
 There must not be a <tt>Content-Length</tt> header when the
-+Status+ is 1xx, 204, 205 or 304.
++Status+ is 1xx, 204 or 304.
 === The Body
 The Body must respond to +each+
 and must only yield String values.
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 c66bc641..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
@@ -43,6 +43,7 @@ module Rack
   SET_COOKIE        = 'Set-Cookie'.freeze
   TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
   HTTP_COOKIE       = 'HTTP_COOKIE'.freeze
+  ETAG              = 'ETag'.freeze
 
   # HTTP method verbs
   GET     = 'GET'.freeze
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/builder.rb b/lib/rack/builder.rb
index 5250aff3..f11c66bc 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -51,7 +51,7 @@ module Rack
     end
 
     def initialize(default_app = nil, &block)
-      @use, @map, @run, @warmup = [], nil, default_app, nil
+      @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
       instance_eval(&block) if block_given?
     end
 
@@ -141,10 +141,17 @@ module Rack
       @map[path] = block
     end
 
+    # Freeze the app (set using run) and all middleware instances when building the application
+    # in to_app.
+    def freeze_app
+      @freeze_app = true
+    end
+
     def to_app
       app = @map ? generate_map(@run, @map) : @run
       fail "missing run or map statement" unless app
-      app = @use.reverse.inject(app) { |a,e| e[a] }
+      app.freeze if @freeze_app
+      app = @use.reverse.inject(app) { |a,e| e[a].tap { |x| x.freeze if @freeze_app } }
       @warmup.call(app) if @warmup
       app
     end
diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb
index 4b8f270e..3076931c 100644
--- a/lib/rack/chunked.rb
+++ b/lib/rack/chunked.rb
@@ -24,7 +24,7 @@ module Rack
           size = chunk.bytesize
           next if size == 0
 
-          chunk = chunk.dup.force_encoding(Encoding::BINARY)
+          chunk = chunk.b
           yield [size.to_s(16), term, chunk, term].join
         end
         yield TAIL
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index 1ec8266d..7855f0c3 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -29,7 +29,7 @@ module Rack
     end
 
     def call(env)
-      began_at = Time.now
+      began_at = Utils.clock_time
       status, header, body = @app.call(env)
       header = Utils::HeaderHash.new(header)
       body = BodyProxy.new(body) { log(env, status, header, began_at) }
@@ -39,20 +39,19 @@ module Rack
     private
 
     def log(env, status, header, began_at)
-      now = Time.now
       length = extract_content_length(header)
 
       msg = FORMAT % [
         env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
         env["REMOTE_USER"] || "-",
-        now.strftime("%d/%b/%Y:%H:%M:%S %z"),
+        Time.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,
-        now - began_at ]
+        Utils.clock_time - began_at ]
 
       logger = @logger || env[RACK_ERRORS]
       # Standard library logger doesn't support write but it supports << which actually
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index 62a11243..d1fb73ab 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
 require "zlib"
 require "time"  # for Time.httpdate
 require 'rack/utils'
@@ -8,7 +9,6 @@ module Rack
   # Currently supported compression algorithms:
   #
   #   * gzip
-  #   * deflate
   #   * identity (no transformation)
   #
   # The middleware automatically detects when compression is supported
@@ -22,13 +22,18 @@ 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 { |env, status, headers, body| body.length > 512 }
+    #                  e.g use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }
     #           'include' - a list of content types that should be compressed
+    #           'sync' - determines if the stream is going to be flused after every chunk.
+    #                    Flushing after every chunk reduces latency for
+    #                    time-sensitive streaming applications, but hurts
+    #                    compression and throughput. Defaults to `true'.
     def initialize(app, options = {})
       @app = app
 
       @condition = options[:if]
       @compressible_types = options[:include]
+      @sync = options[:sync] == false ? false : true
     end
 
     def call(env)
@@ -41,7 +46,7 @@ module Rack
 
       request = Request.new(env)
 
-      encoding = Utils.select_best_encoding(%w(gzip deflate identity),
+      encoding = Utils.select_best_encoding(%w(gzip identity),
                                             request.accept_encoding)
 
       # Set the Vary HTTP header.
@@ -53,37 +58,33 @@ module Rack
       case encoding
       when "gzip"
         headers['Content-Encoding'] = "gzip"
-        headers.delete(CONTENT_LENGTH)
-        mtime = headers.key?("Last-Modified") ?
-          Time.httpdate(headers["Last-Modified"]) : Time.now
-        [status, headers, GzipStream.new(body, mtime)]
-      when "deflate"
-        headers['Content-Encoding'] = "deflate"
-        headers.delete(CONTENT_LENGTH)
-        [status, headers, DeflateStream.new(body)]
+        headers.delete('Content-Length')
+        mtime = headers["Last-Modified"]
+        mtime = Time.httpdate(mtime).to_i if mtime
+        [status, headers, GzipStream.new(body, mtime, @sync)]
       when "identity"
         [status, headers, body]
       when nil
         message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
         bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
-        [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
+        [406, {'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s}, bp]
       end
     end
 
     class GzipStream
-      def initialize(body, mtime)
+      def initialize(body, mtime, sync)
+        @sync = sync
         @body = body
         @mtime = mtime
-        @closed = false
       end
 
       def each(&block)
         @writer = block
         gzip  =::Zlib::GzipWriter.new(self)
-        gzip.mtime = @mtime
+        gzip.mtime = @mtime if @mtime
         @body.each { |part|
           gzip.write(part)
-          gzip.flush
+          gzip.flush if @sync
         }
       ensure
         gzip.close
@@ -95,39 +96,8 @@ module Rack
       end
 
       def close
-        return if @closed
-        @closed = true
-        @body.close if @body.respond_to?(:close)
-      end
-    end
-
-    class DeflateStream
-      DEFLATE_ARGS = [
-        Zlib::DEFAULT_COMPRESSION,
-        # drop the zlib header which causes both Safari and IE to choke
-        -Zlib::MAX_WBITS,
-        Zlib::DEF_MEM_LEVEL,
-        Zlib::DEFAULT_STRATEGY
-      ]
-
-      def initialize(body)
-        @body = body
-        @closed = false
-      end
-
-      def each
-        deflator = ::Zlib::Deflate.new(*DEFLATE_ARGS)
-        @body.each { |part| yield deflator.deflate(part, Zlib::SYNC_FLUSH) }
-        yield fin = deflator.finish
-      ensure
-        deflator.finish unless fin
-        deflator.close
-      end
-
-      def close
-        return if @closed
-        @closed = true
         @body.close if @body.respond_to?(:close)
+        @body = nil
       end
     end
 
@@ -137,13 +107,13 @@ module Rack
       # Skip compressing empty entity body responses and responses with
       # no-transform set.
       if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
-          headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
+          headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
          (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
         return false
       end
 
       # Skip if @compressible_types are given and does not include request's content type
-      return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
+      return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
 
       # Skip if @condition lambda is given and evaluates to false
       return false if @condition && !@condition.call(env, status, headers, body)
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 88973131..5a8c6452 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -1,4 +1,5 @@
-require 'digest/md5'
+require 'rack'
+require 'digest/sha2'
 
 module Rack
   # Automatically sets the ETag header on all String bodies.
@@ -11,7 +12,7 @@ module Rack
   # used when Etag is absent and a directive when it is present. The first
   # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
   class ETag
-    ETAG_STRING = 'ETag'.freeze
+    ETAG_STRING = Rack::ETAG
     DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
 
     def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
@@ -64,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/events.rb b/lib/rack/events.rb
new file mode 100644
index 00000000..3782a22e
--- /dev/null
+++ b/lib/rack/events.rb
@@ -0,0 +1,154 @@
+require 'rack/response'
+require 'rack/body_proxy'
+
+module Rack
+  ### This middleware provides hooks to certain places in the request /
+  #response lifecycle.  This is so that middleware that don't need to filter
+  #the response data can safely leave it alone and not have to send messages
+  #down the traditional "rack stack".
+  #
+  # The events are:
+  #
+  # * on_start(request, response)
+  #
+  #   This event is sent at the start of the request, before the next
+  #   middleware in the chain is called.  This method is called with a request
+  #   object, and a response object.  Right now, the response object is always
+  #   nil, but in the future it may actually be a real response object.
+  #
+  # * on_commit(request, response)
+  #
+  #   The response has been committed.  The application has returned, but the
+  #   response has not been sent to the webserver yet.  This method is always
+  #   called with a request object and the response object.  The response
+  #   object is constructed from the rack triple that the application returned.
+  #   Changes may still be made to the response object at this point.
+  #
+  # * on_send(request, response)
+  #
+  #   The webserver has started iterating over the response body and presumably
+  #   has started sending data over the wire. This method is always called with
+  #   a request object and the response object.  The response object is
+  #   constructed from the rack triple that the application returned.  Changes
+  #   SHOULD NOT be made to the response object as the webserver has already
+  #   started sending data.  Any mutations will likely result in an exception.
+  #
+  # * on_finish(request, response)
+  #
+  #   The webserver has closed the response, and all data has been written to
+  #   the response socket.  The request and response object should both be
+  #   read-only at this point.  The body MAY NOT be available on the response
+  #   object as it may have been flushed to the socket.
+  #
+  # * on_error(request, response, error)
+  #
+  #   An exception has occurred in the application or an `on_commit` event.
+  #   This method will get the request, the response (if available) and the
+  #   exception that was raised.
+  #
+  # ## Order
+  #
+  # `on_start` is called on the handlers in the order that they were passed to
+  # the constructor.  `on_commit`, on_send`, `on_finish`, and `on_error` are
+  # called in the reverse order.  `on_finish` handlers are called inside an
+  # `ensure` block, so they are guaranteed to be called even if something
+  # raises an exception.  If something raises an exception in a `on_finish`
+  # method, then nothing is guaranteed.
+
+  class Events
+    module Abstract
+      def on_start req, res
+      end
+
+      def on_commit req, res
+      end
+
+      def on_send req, res
+      end
+
+      def on_finish req, res
+      end
+
+      def on_error req, res, e
+      end
+    end
+
+    class EventedBodyProxy < Rack::BodyProxy # :nodoc:
+      attr_reader :request, :response
+
+      def initialize body, request, response, handlers, &block
+        super(body, &block)
+        @request  = request
+        @response = response
+        @handlers = handlers
+      end
+
+      def each
+        @handlers.reverse_each { |handler| handler.on_send request, response }
+        super
+      end
+    end
+
+    class BufferedResponse < Rack::Response::Raw # :nodoc:
+      attr_reader :body
+
+      def initialize status, headers, body
+        super(status, headers)
+        @body = body
+      end
+
+      def to_a; [status, headers, body]; end
+    end
+
+    def initialize app, handlers
+      @app      = app
+      @handlers = handlers
+    end
+
+    def call env
+      request = make_request env
+      on_start request, nil
+
+      begin
+        status, headers, body = @app.call request.env
+        response = make_response status, headers, body
+        on_commit request, response
+      rescue StandardError => e
+        on_error request, response, e
+        on_finish request, response
+        raise
+      end
+
+      body = EventedBodyProxy.new(body, request, response, @handlers) do
+        on_finish request, response
+      end
+      [response.status, response.headers, body]
+    end
+
+    private
+
+    def on_error request, response, e
+      @handlers.reverse_each { |handler| handler.on_error request, response, e }
+    end
+
+    def on_commit request, response
+      @handlers.reverse_each { |handler| handler.on_commit request, response }
+    end
+
+    def on_start request, response
+      @handlers.each { |handler| handler.on_start request, nil }
+    end
+
+    def on_finish request, response
+      @handlers.reverse_each { |handler| handler.on_finish request, response }
+    end
+
+    def make_request env
+      Rack::Request.new env
+    end
+
+    def make_response status, headers, body
+      BufferedResponse.new status, headers, body
+    end
+  end
+end
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 5b755f56..09eb0afb 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,
         {
@@ -149,7 +158,7 @@ module Rack
 
     def filesize path
       # If response_body is present, use its size.
-      return Rack::Utils.bytesize(response_body) if response_body
+      return response_body.bytesize if response_body
 
       #   We check via File::size? whether this file provides size info
       #   via stat (e.g. /proc files often don't), otherwise we have to
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/scgi.rb b/lib/rack/handler/scgi.rb
index e056a01d..beda9c3e 100644
--- a/lib/rack/handler/scgi.rb
+++ b/lib/rack/handler/scgi.rb
@@ -41,7 +41,8 @@ module Rack
         env[QUERY_STRING] ||= ""
         env[SCRIPT_NAME] = ""
 
-        rack_input = StringIO.new(input_body, encoding: Encoding::BINARY)
+        rack_input = StringIO.new(input_body)
+        rack_input.set_encoding(Encoding::BINARY)
 
         env.update(
           RACK_VERSION      => Rack::VERSION,
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/lint.rb b/lib/rack/lint.rb
index 54d37822..683ba684 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -659,7 +659,7 @@ module Rack
     def check_content_type(status, headers)
       headers.each { |key, value|
         ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-        ## 204, 205 or 304.
+        ## 204 or 304.
         if key.downcase == "content-type"
           assert("Content-Type header found in #{status} response, not allowed") {
             not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
@@ -674,7 +674,7 @@ module Rack
       headers.each { |key, value|
         if key.downcase == 'content-length'
           ## There must not be a <tt>Content-Length</tt> header when the
-          ## +Status+ is 1xx, 204, 205 or 304.
+          ## +Status+ is 1xx, 204 or 304.
           assert("Content-Length header found in #{status} response, not allowed") {
             not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? status.to_i
           }
diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb
index 923dca59..b5a41e8e 100644
--- a/lib/rack/lock.rb
+++ b/lib/rack/lock.rb
@@ -11,12 +11,21 @@ module Rack
 
     def call(env)
       @mutex.lock
+      @env = env
+      @old_rack_multithread = env[RACK_MULTITHREAD]
       begin
-        response = @app.call(env.merge(RACK_MULTITHREAD => false))
-        returned = response << BodyProxy.new(response.pop) { @mutex.unlock }
+        response = @app.call(env.merge!(RACK_MULTITHREAD => false))
+        returned = response << BodyProxy.new(response.pop) { unlock }
       ensure
-        @mutex.unlock unless returned
+        unlock unless returned
       end
     end
+
+    private
+
+    def unlock
+      @mutex.unlock
+      @env[RACK_MULTITHREAD] = @old_rack_multithread
+    end
   end
 end
diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb
index f5637771..06df21f7 100644
--- a/lib/rack/method_override.rb
+++ b/lib/rack/method_override.rb
@@ -38,6 +38,9 @@ module Rack
     def method_override_param(req)
       req.POST[METHOD_OVERRIDE_PARAM_KEY]
     rescue Utils::InvalidParameterError, Utils::ParameterTypeError
+      req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
+    rescue EOFError
+      req.get_header(RACK_ERRORS).puts "Bad request content body"
     end
   end
 end
diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb
index a2fc2401..914bf3b5 100644
--- a/lib/rack/mock.rb
+++ b/lib/rack/mock.rb
@@ -91,13 +91,13 @@ module Rack
 
       env = DEFAULT_ENV.dup
 
-      env[REQUEST_METHOD]  = opts[:method] ? opts[:method].to_s.upcase : GET
-      env[SERVER_NAME]     = uri.host || "example.org"
-      env[SERVER_PORT]     = uri.port ? uri.port.to_s : "80"
-      env[QUERY_STRING]    = uri.query.to_s
-      env[PATH_INFO]       = (!uri.path || uri.path.empty?) ? "/" : uri.path
-      env[RACK_URL_SCHEME] = uri.scheme || "http"
-      env[HTTPS]           = env[RACK_URL_SCHEME] == "https" ? "on" : "off"
+      env[REQUEST_METHOD]  = (opts[:method] ? opts[:method].to_s.upcase : GET).b
+      env[SERVER_NAME]     = (uri.host || "example.org").b
+      env[SERVER_PORT]     = (uri.port ? uri.port.to_s : "80").b
+      env[QUERY_STRING]    = (uri.query.to_s).b
+      env[PATH_INFO]       = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
+      env[RACK_URL_SCHEME] = (uri.scheme || "http").b
+      env[HTTPS]           = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
 
       env[SCRIPT_NAME] = opts[:script_name] || ""
 
@@ -128,7 +128,7 @@ module Rack
         end
       end
 
-      empty_str = ''.force_encoding(Encoding::ASCII_8BIT)
+      empty_str = String.new
       opts[:input] ||= empty_str
       if String === opts[:input]
         rack_input = StringIO.new(opts[:input])
@@ -139,7 +139,7 @@ module Rack
       rack_input.set_encoding(Encoding::BINARY)
       env[RACK_INPUT] = rack_input
 
-      env["CONTENT_LENGTH"] ||= env[RACK_INPUT].length.to_s
+      env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
 
       opts.each { |field, value|
         env[field] = value  if String === field
@@ -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
@@ -191,7 +190,7 @@ module Rack
     end
 
     def empty?
-      [201, 204, 205, 304].include? status
+      [201, 204, 304].include? status
     end
   end
 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..c02e26f6 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -5,10 +5,10 @@ module Rack
     class MultipartPartLimitError < Errno::EMFILE; end
 
     class Parser
-      BUFSIZE = 16384
+      BUFSIZE = 1_048_576
       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
@@ -137,7 +135,7 @@ module Rack
             klass = TempfilePart
             @open_files += 1
           else
-            body = ''.force_encoding(Encoding::ASCII_8BIT)
+            body = String.new
             klass = BufferPart
           end
 
@@ -167,15 +165,15 @@ module Rack
       attr_reader :state
 
       def initialize(boundary, tempfile, bufsize, query_parser)
-        @buf            = "".force_encoding(Encoding::ASCII_8BIT)
+        @buf            = String.new
 
         @query_parser   = query_parser
         @params         = query_parser.make_params
         @boundary       = "--#{boundary}"
-        @boundary_size  = @boundary.bytesize + EOL.size
         @bufsize        = bufsize
 
         @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
+        @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
         @full_boundary = @boundary
         @end_boundary = @boundary + '--'
         @state = :FAST_FORWARD
@@ -265,15 +263,17 @@ module Rack
       end
 
       def handle_mime_body
-        if @buf =~ rx
+        if i = @buf.index(rx)
           # Save the rest.
-          if i = @buf.index(rx)
-            @collector.on_mime_body @mime_index, @buf.slice!(0, i)
-            @buf.slice!(0, 2) # Remove \r\n after the content
-          end
+          @collector.on_mime_body @mime_index, @buf.slice!(0, i)
+          @buf.slice!(0, 2) # Remove \r\n after the content
           @state = :CONSUME_TOKEN
           @mime_index += 1
         else
+          # Save the read body part.
+          if @rx_max_size < @buf.size
+            @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
+          end
           :want_read
         end
       end
@@ -347,6 +347,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 e97f302f..28712ef6 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
@@ -57,6 +40,12 @@ module Rack
         super()
       end
 
+      # Predicate method to test to see if `name` has been set as request
+      # specific data
+      def has_header?(name)
+        @env.key? name
+      end
+
       # Get a request specific value for `name`.
       def get_header(name)
         @env[name]
@@ -68,9 +57,9 @@ module Rack
         @env.fetch(name, &block)
       end
 
-      # Delete a request specific value for `name`.
-      def delete_header(name)
-        @env.delete name
+      # Loops through each key / value pair in the request specific data.
+      def each_header(&block)
+        @env.each(&block)
       end
 
       # Set a request specific value for `name` to `v`
@@ -78,15 +67,28 @@ module Rack
         @env[name] = v
       end
 
-      # Predicate method to test to see if `name` has been set as request
-      # specific data
-      def has_header?(name)
-        @env.key? name
+      # Add a header that may have multiple values.
+      #
+      # Example:
+      #   request.add_header 'Accept', 'image/png'
+      #   request.add_header 'Accept', '*/*'
+      #
+      #   assert_equal 'image/png,*/*', request.get_header('Accept')
+      #
+      # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+      def add_header key, v
+        if v.nil?
+          get_header key
+        elsif has_header? key
+          set_header key, "#{get_header key},#{v}"
+        else
+          set_header key, v
+        end
       end
 
-      # Loops through each key / value pair in the request specific data.
-      def each_header(&block)
-        @env.each(&block)
+      # Delete a request specific value for `name`.
+      def delete_header(name)
+        @env.delete name
       end
 
       def initialize_copy(other)
@@ -96,7 +98,7 @@ module Rack
 
     module Helpers
       # The set of form-data media-types. Requests that do not indicate
-      # one of the media types presents in this list will not be eligible
+      # one of the media types present in this list will not be eligible
       # for form-data / param parsing.
       FORM_DATA_MEDIA_TYPES = [
         'application/x-www-form-urlencoded',
@@ -104,7 +106,7 @@ module Rack
       ]
 
       # The set of media-types. Requests that do not indicate
-      # one of the media types presents in this list will not be eligible
+      # one of the media types present in this list will not be eligible
       # for param parsing like soap attachments or generic multiparts
       PARSEABLE_DATA_MEDIA_TYPES = [
         'multipart/related',
@@ -141,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
 
@@ -155,10 +157,10 @@ module Rack
       def delete?;  request_method == DELETE  end
 
       # Checks the HTTP request method (or verb) to see if it was of type GET
-      def get?;     request_method == GET       end
+      def get?;     request_method == GET     end
 
       # Checks the HTTP request method (or verb) to see if it was of type HEAD
-      def head?;    request_method == HEAD      end
+      def head?;    request_method == HEAD    end
 
       # Checks the HTTP request method (or verb) to see if it was of type OPTIONS
       def options?; request_method == OPTIONS end
@@ -195,6 +197,10 @@ module Rack
         end
       end
 
+      def authority
+        get_header(SERVER_NAME) + ':' + get_header(SERVER_PORT)
+      end
+
       def cookies
         hash = fetch_header(RACK_REQUEST_COOKIE_HASH) do |k|
           set_header(k, {})
@@ -414,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/response.rb b/lib/rack/response.rb
index 78c9f473..a9f0c2a3 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -60,7 +60,7 @@ module Rack
     def finish(&block)
       @block = block
 
-      if [204, 205, 304].include?(status.to_i)
+      if [204, 304].include?(status.to_i)
         delete_header CONTENT_TYPE
         delete_header CONTENT_LENGTH
         close
@@ -99,7 +99,7 @@ module Rack
       @block == nil && @body.empty?
     end
 
-    def have_header?(key);  headers.key? key;   end
+    def has_header?(key);   headers.key? key;   end
     def get_header(key);    headers[key];       end
     def set_header(key, v); headers[key] = v;   end
     def delete_header(key); headers.delete key; end
@@ -132,7 +132,26 @@ module Rack
       def redirect?;            [301, 302, 303, 307, 308].include? status; end
 
       def include?(header)
-        have_header? header
+        has_header? header
+      end
+
+      # Add a header that may have multiple values.
+      #
+      # Example:
+      #   response.add_header 'Vary', 'Accept-Encoding'
+      #   response.add_header 'Vary', 'Cookie'
+      #
+      #   assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
+      #
+      # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+      def add_header key, v
+        if v.nil?
+          get_header key
+        elsif has_header? key
+          set_header key, "#{get_header key},#{v}"
+        else
+          set_header key, v
+        end
       end
 
       def content_type
@@ -176,6 +195,22 @@ module Rack
       def set_cookie_header= v
         set_header SET_COOKIE, v
       end
+
+      def cache_control
+        get_header CACHE_CONTROL
+      end
+
+      def cache_control= v
+        set_header CACHE_CONTROL, v
+      end
+
+      def etag
+        get_header ETAG
+      end
+
+      def etag= v
+        set_header ETAG, v
+      end
     end
 
     include Helpers
@@ -183,14 +218,15 @@ module Rack
     class Raw
       include Helpers
 
-      attr_reader :status, :headers
+      attr_reader :headers
+      attr_accessor :status
 
       def initialize status, headers
         @status = status
         @headers = headers
       end
 
-      def have_header?(key);  headers.key? key;   end
+      def has_header?(key);   headers.key? key;   end
       def get_header(key);    headers[key];       end
       def set_header(key, v); headers[key] = v;   end
       def delete_header(key); headers.delete key; end
diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb
index 7752bee8..bb15bdb1 100644
--- a/lib/rack/runtime.rb
+++ b/lib/rack/runtime.rb
@@ -1,3 +1,5 @@
+require 'rack/utils'
+
 module Rack
   # Sets an "X-Runtime" response header, indicating the response
   # time of the request, in seconds
@@ -16,9 +18,9 @@ module Rack
     end
 
     def call(env)
-      start_time = clock_time
+      start_time = Utils.clock_time
       status, headers, body = @app.call(env)
-      request_time = clock_time - start_time
+      request_time = Utils.clock_time - start_time
 
       unless headers.has_key?(@header_name)
         headers[@header_name] = FORMAT_STRING % request_time
@@ -26,17 +28,5 @@ module Rack
 
       [status, headers, body]
     end
-
-    private
-
-    if defined?(Process::CLOCK_MONOTONIC)
-      def clock_time
-        Process.clock_gettime(Process::CLOCK_MONOTONIC)
-      end
-    else
-      def clock_time
-        Time.now.to_f
-      end
-    end
   end
 end
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index bdb7cf2f..34c1a84e 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -150,8 +150,12 @@ module Rack
       if mapping = @mappings.find { |internal,_| internal =~ path }
         path.sub(*mapping)
       elsif mapping = env['HTTP_X_ACCEL_MAPPING']
-        internal, external = mapping.split('=', 2).map(&:strip)
-        path.sub(/^#{internal}/i, external)
+        mapping.split(',').map(&:strip).each do |m|
+          internal, external = m.split('=', 2).map(&:strip)
+          new_path = path.sub(/^#{internal}/i, external)
+          return new_path unless path == new_path
+        end
+        path
       end
     end
   end
diff --git a/lib/rack/server.rb b/lib/rack/server.rb
index 690f1096..ce965144 100644
--- a/lib/rack/server.rb
+++ b/lib/rack/server.rb
@@ -1,4 +1,5 @@
 require 'optparse'
+require 'fileutils'
 
 
 module Rack
@@ -46,7 +47,7 @@ module Rack
 
           opts.separator ""
           opts.separator "Rack options:"
-          opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s|
+          opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick)") { |s|
             options[:server] = s
           }
 
@@ -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 fe11d902..1bb8d5d0 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
@@ -42,7 +44,7 @@ module Rack
         end
 
         def options
-          @req.get_header RACK_SESSION_OPTIONS
+          @req.session_options
         end
 
         def each(&block)
@@ -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
 
@@ -165,7 +177,7 @@ module Rack
       # * :key determines the name of the cookie, by default it is
       #   'rack.session'
       # * :path, :domain, :expire_after, :secure, and :httponly set the related
-      #   cookie options as by Rack::Response#add_cookie
+      #   cookie options as by Rack::Response#set_cookie
       # * :skip will not a set a cookie in the response nor update the session state
       # * :defer will not set a cookie in the response but still update the session
       #   state if it is used with a backend
@@ -198,7 +210,7 @@ module Rack
           :sidbits =>       128,
           :cookie_only =>   true,
           :secure_random => ::SecureRandom
-        }
+        }.freeze
 
         attr_reader :key, :default_options, :sid_secure
 
@@ -351,6 +363,7 @@ module Rack
             set_cookie(req, res, cookie.merge!(options))
           end
         end
+        public :commit_session
 
         # Sets the cookie back to the client with session id. We skip the cookie
         # setting if the value didn't change (sid is the same) or expires was given.
@@ -395,7 +408,7 @@ module Rack
 
       class ID < Persisted
         def self.inherited(klass)
-          k = klass.ancestors.find { |kl| kl.superclass == ID }
+          k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
           unless k.instance_variable_defined?(:"@_rack_warned")
             warn "#{klass} is inheriting from #{ID}.  Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
             k.instance_variable_set(:"@_rack_warned", true)
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index bd047163..71bb96f4 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -105,6 +105,8 @@ module Rack
 
       def initialize(app, options={})
         @secrets = options.values_at(:secret, :old_secret).compact
+        @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
+
         warn <<-MSG unless secure?(options)
         SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
         This poses a security threat. It is strongly recommended that you
@@ -180,7 +182,7 @@ module Rack
       end
 
       def generate_hmac(data, secret)
-        OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, secret, data)
+        OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
       end
 
       def secure?(options)
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 8eab6828..98ceee8a 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -1,4 +1,5 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: true
 require 'uri'
 require 'fileutils'
 require 'set'
@@ -77,6 +78,17 @@ module Rack
       self.default_query_parser = self.default_query_parser.new_space_limit(v)
     end
 
+    if defined?(Process::CLOCK_MONOTONIC)
+      def clock_time
+        Process.clock_gettime(Process::CLOCK_MONOTONIC)
+      end
+    else
+      def clock_time
+        Time.now.to_f
+      end
+    end
+    module_function :clock_time
+
     def parse_query(qs, d = nil, &unescaper)
       Rack::Utils.default_query_parser.parse_query(qs, d, &unescaper)
     end
@@ -210,39 +222,26 @@ module Rack
         domain  = "; domain=#{value[:domain]}"   if value[:domain]
         path    = "; path=#{value[:path]}"       if value[:path]
         max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
-        # There is an RFC mess in the area of date formatting for Cookies. Not
-        # only are there contradicting RFCs and examples within RFC text, but
-        # there are also numerous conflicting names of fields and partially
-        # cross-applicable specifications.
-        #
-        # These are best described in RFC 2616 3.3.1. This RFC text also
-        # specifies that RFC 822 as updated by RFC 1123 is preferred. That is a
-        # fixed length format with space-date delimited fields.
-        #
-        # See also RFC 1123 section 5.2.14.
-        #
-        # RFC 6265 also specifies "sane-cookie-date" as RFC 1123 date, defined
-        # in RFC 2616 3.3.1. RFC 6265 also gives examples that clearly denote
-        # the space delimited format. These formats are compliant with RFC 2822.
-        #
-        # For reference, all involved RFCs are:
-        # RFC 822
-        # RFC 1123
-        # RFC 2109
-        # RFC 2616
-        # RFC 2822
-        # RFC 2965
-        # RFC 6265
-        expires = "; expires=" +
-          rfc2822(value[:expires].clone.gmtime) if value[:expires]
+        expires = "; expires=#{value[:expires].httpdate}" 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, ''
@@ -251,6 +250,8 @@ module Rack
         [header, cookie].join("\n")
       when Array
         (header + [cookie]).join("\n")
+      else
+        raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
       end
     end
     module_function :add_cookie_to_header
@@ -486,9 +487,9 @@ module Rack
 
     # Every standard HTTP code mapped to the appropriate message.
     # Generated with:
-    # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
-    #   ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
-    #             puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
+    #   curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
+    #     ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
+    #               puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
     HTTP_STATUS_CODES = {
       100 => 'Continue',
       101 => 'Switching Protocols',
@@ -537,6 +538,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',
@@ -551,7 +553,7 @@ module Rack
     }
 
     # Responses with HTTP status codes that should not have an entity body
-    STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 205 << 304)
+    STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
 
     SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
       [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
@@ -580,9 +582,16 @@ module Rack
 
       clean.unshift '/' if parts.empty? || parts.first.empty?
 
-      ::File.join(*clean)
+      ::File.join clean
     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..ec2b79f6 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -12,24 +12,22 @@ the simplest way possible, it unifies and distills the API for web
 servers, web frameworks, and software in between (the so-called
 middleware) into a single method call.
 
-Also see http://rack.github.io/.
+Also see https://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.homepage        = 'http://rack.github.io/'
+  s.homepage        = 'https://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 c664583d..aa9c0e0a 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,23 +1,34 @@
 require 'minitest/autorun'
 
 module Rack
-  class TestCase
-    # Keep this first.
-    PID = fork {
-      ENV['RACK_ENV'] = 'deployment'
-      ENV['RUBYLIB'] = [
-        ::File.expand_path('../../lib', __FILE__),
-        ENV['RUBYLIB'],
-      ].compact.join(':')
+  class TestCase < Minitest::Test
+    # Check for Lighttpd and launch it for tests if available.
+    `which lighttpd`
 
-      Dir.chdir(::File.expand_path("../cgi", __FILE__)) do
-        exec "lighttpd -D -f lighttpd.conf"
-      end
-    }
+    if $?.success?
+      begin
+        # Keep this first.
+        LIGHTTPD_PID = fork {
+          ENV['RACK_ENV'] = 'deployment'
+          ENV['RUBYLIB'] = [
+            ::File.expand_path('../../lib', __FILE__),
+            ENV['RUBYLIB'],
+          ].compact.join(':')
 
-    Minitest.after_run do
-      Process.kill 15, PID
-      Process.wait(PID)
+          Dir.chdir(::File.expand_path("../cgi", __FILE__)) do
+            exec "lighttpd -D -f lighttpd.conf"
+          end
+        }
+      rescue NotImplementedError
+        warn "Your Ruby doesn't support Kernel#fork. Skipping Rack::Handler::CGI and ::FastCGI tests."
+      else
+        Minitest.after_run do
+          Process.kill 15, LIGHTTPD_PID
+          Process.wait LIGHTTPD_PID
+        end
+      end
+    else
+      warn "Lighttpd isn't installed. Skipping Rack::Handler::CGI and FastCGI tests. Install lighttpd to run them."
     end
   end
 end
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_body_proxy.rb b/test/spec_body_proxy.rb
index 9df6f1d7..4db447a0 100644
--- a/test/spec_body_proxy.rb
+++ b/test/spec_body_proxy.rb
@@ -1,7 +1,6 @@
 require 'minitest/autorun'
 require 'rack/body_proxy'
 require 'stringio'
-require 'ostruct'
 
 describe Rack::BodyProxy do
   it 'call each on the wrapped body' do
@@ -58,7 +57,7 @@ describe Rack::BodyProxy do
   end
 
   it 'not respond to :to_ary' do
-    body = OpenStruct.new(:to_ary => true)
+    body = Object.new.tap { |o| def o.to_ary() end }
     body.respond_to?(:to_ary).must_equal true
 
     proxy = Rack::BodyProxy.new(body) { }
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index cb0bbbd4..326f6b6c 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -174,6 +174,27 @@ describe Rack::Builder do
     Rack::MockRequest.new(app).get("/").must_be :server_error?
   end
 
+  it "supports #freeze_app for freezing app and middleware" do
+    app = builder do
+      freeze_app
+      use Rack::ShowExceptions
+      use(Class.new do
+        def initialize(app) @app = app end
+        def call(env) @a = 1 if env['PATH_INFO'] == '/a'; @app.call(env) end
+      end)
+      o = Object.new
+      def o.call(env)
+        @a = 1 if env['PATH_INFO'] == '/b';
+        [200, {}, []]
+      end
+      run o
+    end
+
+    Rack::MockRequest.new(app).get("/a").must_be :server_error?
+    Rack::MockRequest.new(app).get("/b").must_be :server_error?
+    Rack::MockRequest.new(app).get("/c").status.must_equal 200
+  end
+
   it 'complains about a missing run' do
     proc do
       Rack::Lint.new Rack::Builder.app { use Rack::ShowExceptions }
diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb
index b89ed9af..77020c2f 100644
--- a/test/spec_cgi.rb
+++ b/test/spec_cgi.rb
@@ -1,5 +1,7 @@
 require 'helper'
-begin
+
+if defined? LIGHTTPD_PID
+
 require File.expand_path('../testrequest', __FILE__)
 require 'rack/handler/cgi'
 
@@ -79,8 +81,4 @@ describe Rack::Handler::CGI do
   end
 end
 
-rescue RuntimeError
-  $stderr.puts "Skipping Rack::Handler::CGI tests (lighttpd is required). Install lighttpd and try again."
-rescue NotImplementedError
-  $stderr.puts "Your Ruby implemenation or platform does not support fork. Skipping Rack::Handler::CGI tests."
-end
+end # if defined? LIGHTTPD_PID
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index 7bbcfd92..dc6e8c9d 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -92,7 +92,7 @@ describe Rack::Chunked do
     body.join.must_equal 'Hello World!'
   end
 
-  [100, 204, 205, 304].each do |status_code|
+  [100, 204, 304].each do |status_code|
     it "not modify response when status code is #{status_code}" do
       app = lambda { |env| [status_code, {}, []] }
       status, headers, _ = chunked(app).call(@env)
diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb
index fd69c375..58f37ad5 100644
--- a/test/spec_conditional_get.rb
+++ b/test/spec_conditional_get.rb
@@ -33,7 +33,7 @@ describe Rack::ConditionalGet do
 
   it "set a 304 status and truncate body when If-None-Match hits" do
     app = conditional_get(lambda { |env|
-      [200, {'Etag'=>'1234'}, ['TEST']] })
+      [200, {'ETag'=>'1234'}, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_NONE_MATCH' => '1234')
@@ -57,7 +57,7 @@ describe Rack::ConditionalGet do
   it "set a 304 status and truncate body when both If-None-Match and If-Modified-Since hits" do
     timestamp = Time.now.httpdate
     app = conditional_get(lambda { |env|
-      [200, {'Last-Modified'=>timestamp, 'Etag'=>'1234'}, ['TEST']] })
+      [200, {'Last-Modified'=>timestamp, 'ETag'=>'1234'}, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '1234')
diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb
index 261e4505..89752bbe 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -36,13 +36,13 @@ describe Rack::ContentLength do
   it "not set Content-Length on 304 responses" do
     app = lambda { |env| [304, {}, []] }
     response = content_length(app).call(request)
-    response[1]['Content-Length'].must_equal nil
+    response[1]['Content-Length'].must_be_nil
   end
 
   it "not set Content-Length when Transfer-Encoding is chunked" do
     app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked'}, []] }
     response = content_length(app).call(request)
-    response[1]['Content-Length'].must_equal nil
+    response[1]['Content-Length'].must_be_nil
   end
 
   # Using "Connection: close" for this is fairly contended. It might be useful
@@ -51,7 +51,7 @@ describe Rack::ContentLength do
   # should "not force a Content-Length when Connection:close" do
   #   app = lambda { |env| [200, {'Connection' => 'close'}, []] }
   #   response = content_length(app).call({})
-  #   response[1]['Content-Length'].must_equal nil
+  #   response[1]['Content-Length'].must_be_nil
   # end
 
   it "close bodies that need to be closed" do
@@ -64,7 +64,7 @@ describe Rack::ContentLength do
 
     app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
     response = content_length(app).call(request)
-    body.closed.must_equal nil
+    body.closed.must_be_nil
     response[2].close
     body.closed.must_equal true
   end
diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb
index 281879f3..daf75355 100644
--- a/test/spec_content_type.rb
+++ b/test/spec_content_type.rb
@@ -41,6 +41,6 @@ describe Rack::ContentType do
   it "not set Content-Type on 304 responses" do
     app = lambda { |env| [304, {}, []] }
     response = content_type(app, "text/html").call(request)
-    response[1]['Content-Type'].must_equal nil
+    response[1]['Content-Type'].must_be_nil
   end
 end
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index ba7ec5d3..a5e91285 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -44,6 +44,8 @@ describe Rack::Deflater do
       [accept_encoding, accept_encoding.dup]
     end
 
+    start = Time.now.to_i
+
     # build response
     status, headers, body = build_response(
       options['app_status'] || expected_status,
@@ -67,6 +69,13 @@ describe Rack::Deflater do
       when 'gzip'
         io = StringIO.new(body_text)
         gz = Zlib::GzipReader.new(io)
+        mtime = gz.mtime.to_i
+        if last_mod = headers['Last-Modified']
+          Time.httpdate(last_mod).to_i.must_equal mtime
+        else
+          mtime.must_be(:<=, Time.now.to_i)
+          mtime.must_be(:>=, start.to_i)
+        end
         tmp = gz.read
         gz.close
         tmp
@@ -81,13 +90,22 @@ describe Rack::Deflater do
     yield(status, headers, body) if block_given?
   end
 
+  # automatic gzip detection (streamable)
+  def auto_inflater
+    Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
+  end
+
+  def deflate_or_gzip
+    {'deflate, gzip' => 'gzip'}
+  end
+
   it 'be able to deflate bodies that respond to each' do
     app_body = Object.new
     class << app_body; def each; yield('foo'); yield('bar'); end; end
 
-    verify(200, 'foobar', 'deflate', { 'app_body' => app_body }) do |status, headers, body|
+    verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'deflate',
+        'Content-Encoding' => 'gzip',
         'Vary' => 'Accept-Encoding',
         'Content-Type' => 'text/plain'
       })
@@ -98,15 +116,15 @@ describe Rack::Deflater do
     app_body = Object.new
     class << app_body; def each; yield('foo'); yield('bar'); end; end
 
-    verify(200, app_body, 'deflate', { 'skip_body_verify' => true }) do |status, headers, body|
+    verify(200, app_body, deflate_or_gzip, { 'skip_body_verify' => true }) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'deflate',
+        'Content-Encoding' => 'gzip',
         'Vary' => 'Accept-Encoding',
         'Content-Type' => 'text/plain'
       })
 
       buf = []
-      inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+      inflater = auto_inflater
       body.each { |part| buf << inflater.inflate(part) }
       buf << inflater.finish
 
@@ -118,32 +136,33 @@ describe Rack::Deflater do
     app_body = Object.new
     class << app_body; def each; yield('foo'); yield('bar'); end; end
     opts = { 'skip_body_verify' => true }
-    verify(200, app_body, 'deflate', opts) do |status, headers, body|
+    verify(200, app_body, 'gzip', opts) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'deflate',
+        'Content-Encoding' => 'gzip',
         'Vary' => 'Accept-Encoding',
         'Content-Type' => 'text/plain'
       })
 
       buf = []
-      inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+      inflater = auto_inflater
       FakeDisconnect = Class.new(RuntimeError)
       assert_raises(FakeDisconnect, "not Zlib::DataError not raised") do
         body.each do |part|
-          buf << inflater.inflate(part)
+          tmp = inflater.inflate(part)
+          buf << tmp if tmp.bytesize > 0
           raise FakeDisconnect
         end
       end
-      assert_raises(Zlib::BufError) { inflater.finish }
+      inflater.finish
       buf.must_equal(%w(foo))
     end
   end
 
   # TODO: This is really just a special case of the above...
   it 'be able to deflate String bodies' do
-    verify(200, 'Hello world!', 'deflate') do |status, headers, body|
+    verify(200, 'Hello world!', deflate_or_gzip) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'deflate',
+        'Content-Encoding' => 'gzip',
         'Vary' => 'Accept-Encoding',
         'Content-Type' => 'text/plain'
       })
@@ -280,7 +299,7 @@ describe Rack::Deflater do
         'Content-Encoding' => 'identity'
       }
     }
-    verify(200, 'Hello World!', 'deflate', options)
+    verify(200, 'Hello World!', deflate_or_gzip, options)
   end
 
   it "deflate if content-type matches :include" do
@@ -334,7 +353,7 @@ describe Rack::Deflater do
         :if => lambda { |env, status, headers, body| true }
       }
     }
-    verify(200, 'Hello World!', 'deflate', options)
+    verify(200, 'Hello World!', deflate_or_gzip, options)
   end
 
   it "not deflate if :if lambda evaluates to false" do
@@ -362,4 +381,38 @@ describe Rack::Deflater do
 
     verify(200, response, 'gzip', options)
   end
+
+  it 'will honor sync: false to avoid unnecessary flushing' do
+    app_body = Object.new
+    class << app_body
+      def each
+        (0..20).each { |i| yield "hello\n".freeze }
+      end
+    end
+
+    options = {
+      'deflater_options' => { :sync => false },
+      'app_body' => app_body,
+      'skip_body_verify' => true,
+    }
+    verify(200, app_body, deflate_or_gzip, options) do |status, headers, body|
+      headers.must_equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+
+      buf = ''
+      raw_bytes = 0
+      inflater = auto_inflater
+      body.each do |part|
+        raw_bytes += part.bytesize
+        buf << inflater.inflate(part)
+      end
+      buf << inflater.finish
+      expect = "hello\n" * 21
+      buf.must_equal expect
+      raw_bytes.must_be(:<, expect.bytesize)
+    end
+  end
 end
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..74795759 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
@@ -58,7 +58,7 @@ describe Rack::ETag do
   it "not set Cache-Control if directive isn't present" do
     app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
     response = etag(app, nil, nil).call(request)
-    response[1]['Cache-Control'].must_equal nil
+    response[1]['Cache-Control'].must_be_nil
   end
 
   it "not change ETag if it is already 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_fastcgi.rb b/test/spec_fastcgi.rb
index ef7c3c3f..5a48327b 100644
--- a/test/spec_fastcgi.rb
+++ b/test/spec_fastcgi.rb
@@ -1,5 +1,7 @@
 require 'helper'
-begin
+
+if defined? LIGHTTPD_PID
+
 require File.expand_path('../testrequest', __FILE__)
 require 'rack/handler/fastcgi'
 
@@ -11,10 +13,6 @@ describe Rack::Handler::FastCGI do
     @port = 9203
   end
 
-  if `which lighttpd` && !$?.success?
-    raise "lighttpd not found"
-  end
-
   it "respond" do
     sleep 1
     GET("/test")
@@ -84,8 +82,4 @@ describe Rack::Handler::FastCGI do
   end
 end
 
-rescue RuntimeError
-  $stderr.puts "Skipping Rack::Handler::FastCGI tests (lighttpd is required). Install lighttpd and try again."
-rescue LoadError
-  $stderr.puts "Skipping Rack::Handler::FastCGI tests (FCGI is required). `gem install fcgi` and try again."
-end
+end # if defined? LIGHTTPD_PID
diff --git a/test/spec_file.rb b/test/spec_file.rb
index 2d0919a9..48c0ab90 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))
 
@@ -179,8 +184,8 @@ describe Rack::File do
     status, heads, _ = file(DOCROOT).call(env)
 
     status.must_equal 200
-    heads['Cache-Control'].must_equal nil
-    heads['Access-Control-Allow-Origin'].must_equal nil
+    heads['Cache-Control'].must_be_nil
+    heads['Access-Control-Allow-Origin'].must_be_nil
   end
 
   it "only support GET, HEAD, and OPTIONS requests" do
@@ -234,7 +239,26 @@ describe Rack::File do
     req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil)))
     res = req.get "/cgi/test"
     res.must_be :successful?
-    res['Content-Type'].must_equal nil
+    res['Content-Type'].must_be_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
+
+  class MyFile < Rack::File
+    def response_body
+      "hello world"
+    end
+  end
+
+  it "behaves gracefully if response_body is present" do
+    file = Rack::Lint.new MyFile.new(DOCROOT)
+    res  = Rack::MockRequest.new(file).get("/cgi/test")
+
+    res.must_be :ok?
   end
 
 end
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index 6d1c2c45..d99c1aa3 100644
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -269,7 +269,7 @@ describe Rack::Lint do
     # }.must_raise(Rack::Lint::LintError).
     #   message.must_match(/No Content-Type/)
 
-    [100, 101, 204, 205, 304].each do |status|
+    [100, 101, 204, 304].each do |status|
       lambda {
         Rack::Lint.new(lambda { |env|
                          [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
@@ -280,7 +280,7 @@ describe Rack::Lint do
   end
 
   it "notice content-length errors" do
-    [100, 101, 204, 205, 304].each do |status|
+    [100, 101, 204, 304].each do |status|
       lambda {
         Rack::Lint.new(lambda { |env|
                          [status, {"Content-length" => "0"}, []]
diff --git a/test/spec_lock.rb b/test/spec_lock.rb
index aa3efa54..c6f7c05e 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -147,7 +147,8 @@ describe Rack::Lock do
     }, false)
     env = Rack::MockRequest.env_for("/")
     env['rack.multithread'].must_equal true
-    app.call(env)
+    _, _, body = app.call(env)
+    body.close
     env['rack.multithread'].must_equal true
   end
 
@@ -191,4 +192,13 @@ describe Rack::Lock do
     lambda { app.call(env) }.must_raise Exception
     lock.synchronized.must_equal false
   end
+
+  it "not replace the environment" do
+    env  = Rack::MockRequest.env_for("/")
+    app  = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, [inner_env.object_id.to_s]] })
+
+    _, _, body = app.call(env)
+
+    body.to_enum.to_a.must_equal [env.object_id.to_s]
+  end
 end
diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb
index ef054364..1d9f0fc3 100644
--- a/test/spec_media_type.rb
+++ b/test/spec_media_type.rb
@@ -8,7 +8,7 @@ describe Rack::MediaType do
     before { @content_type = nil }
 
     it '#type is nil' do
-      Rack::MediaType.type(@content_type).must_equal nil
+      Rack::MediaType.type(@content_type).must_be_nil
     end
 
     it '#params is empty' do
diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb
index 14ace0b1..bb72af9f 100644
--- a/test/spec_method_override.rb
+++ b/test/spec_method_override.rb
@@ -66,14 +66,27 @@ EOF
                       "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
                       "CONTENT_LENGTH" => input.size.to_s,
                       :method => "POST", :input => input)
-    begin
-      app.call env
-    rescue EOFError
-    end
+    app.call env
 
     env["REQUEST_METHOD"].must_equal "POST"
   end
 
+  it "writes error to RACK_ERRORS when given invalid multipart form data" do
+    input = <<EOF
+--AaB03x\r
+content-disposition: form-data; name="huge"; filename="huge"\r
+EOF
+    env = Rack::MockRequest.env_for("/",
+                      "CONTENT_TYPE" => "multipart/form-data, boundary=AaB03x",
+                      "CONTENT_LENGTH" => input.size.to_s,
+                      Rack::RACK_ERRORS => StringIO.new,
+                      :method => "POST", :input => input)
+    Rack::MethodOverride.new(proc { [200, {"Content-Type" => "text/plain"}, []] }).call env
+
+    env[Rack::RACK_ERRORS].rewind
+    env[Rack::RACK_ERRORS].read.must_match /Bad request content body/
+  end
+
   it "not modify REQUEST_METHOD for POST requests when the params are unparseable" do
     env = Rack::MockRequest.env_for("/", :method => "POST", :input => "(%bad-params%)")
     app.call env
diff --git a/test/spec_mime.rb b/test/spec_mime.rb
index cd40b4b5..569233b4 100644
--- a/test/spec_mime.rb
+++ b/test/spec_mime.rb
@@ -19,7 +19,7 @@ describe Rack::Mime do
   end
 
   it "should support null fallbacks" do
-    Rack::Mime.mime_type('.nothing', nil).must_equal nil
+    Rack::Mime.mime_type('.nothing', nil).must_be_nil
   end
 
   it "should match exact mimes" do
diff --git a/test/spec_mock.rb b/test/spec_mock.rb
index f27f7d6e..c7992321 100644
--- a/test/spec_mock.rb
+++ b/test/spec_mock.rb
@@ -84,6 +84,15 @@ describe Rack::MockRequest do
   it "set content length" do
     env = Rack::MockRequest.env_for("/", :input => "foo")
     env["CONTENT_LENGTH"].must_equal "3"
+
+    env = Rack::MockRequest.env_for("/", :input => StringIO.new("foo"))
+    env["CONTENT_LENGTH"].must_equal "3"
+
+    env = Rack::MockRequest.env_for("/", :input => Tempfile.new("name").tap { |t| t << "foo" })
+    env["CONTENT_LENGTH"].must_equal "3"
+
+    env = Rack::MockRequest.env_for("/", :input => IO.pipe.first)
+    env["CONTENT_LENGTH"].must_be_nil
   end
 
   it "allow posting" do
@@ -211,6 +220,23 @@ describe Rack::MockRequest do
     Rack::MockRequest.new(capp).get('/', :lint => true)
     called.must_equal true
   end
+
+  it "defaults encoding to ASCII 8BIT" do
+    req = Rack::MockRequest.env_for("/foo")
+
+    keys = [
+        Rack::REQUEST_METHOD,
+        Rack::SERVER_NAME,
+        Rack::SERVER_PORT,
+        Rack::QUERY_STRING,
+        Rack::PATH_INFO,
+        Rack::HTTPS,
+        Rack::RACK_URL_SCHEME
+    ]
+    keys.each do |k|
+      assert_equal Encoding::ASCII_8BIT, req[k].encoding
+    end
+  end
 end
 
 describe Rack::MockResponse do
@@ -273,3 +299,70 @@ describe Rack::MockResponse do
     }.must_raise Rack::MockRequest::FatalWarning
   end
 end
+
+describe Rack::MockResponse, 'headers' do
+  before do
+    @res = Rack::MockRequest.new(app).get('')
+    @res.set_header 'FOO', '1'
+  end
+
+  it 'has_header?' do
+    lambda { @res.has_header? nil }.must_raise NoMethodError
+
+    @res.has_header?('FOO').must_equal true
+    @res.has_header?('Foo').must_equal true
+  end
+
+  it 'get_header' do
+    lambda { @res.get_header nil }.must_raise NoMethodError
+
+    @res.get_header('FOO').must_equal '1'
+    @res.get_header('Foo').must_equal '1'
+  end
+
+  it 'set_header' do
+    lambda { @res.set_header nil, '1' }.must_raise NoMethodError
+
+    @res.set_header('FOO', '2').must_equal '2'
+    @res.get_header('FOO').must_equal '2'
+
+    @res.set_header('Foo', '3').must_equal '3'
+    @res.get_header('Foo').must_equal '3'
+    @res.get_header('FOO').must_equal '3'
+
+    @res.set_header('FOO', nil).must_be_nil
+    @res.get_header('FOO').must_be_nil
+    @res.has_header?('FOO').must_equal true
+  end
+
+  it 'add_header' do
+    lambda { @res.add_header nil, '1' }.must_raise NoMethodError
+
+    # Sets header on first addition
+    @res.add_header('FOO', '1').must_equal '1,1'
+    @res.get_header('FOO').must_equal '1,1'
+
+    # Ignores nil additions
+    @res.add_header('FOO', nil).must_equal '1,1'
+    @res.get_header('FOO').must_equal '1,1'
+
+    # Converts additions to strings
+    @res.add_header('FOO', 2).must_equal '1,1,2'
+    @res.get_header('FOO').must_equal '1,1,2'
+
+    # Respects underlying case-sensitivity
+    @res.add_header('Foo', 'yep').must_equal '1,1,2,yep'
+    @res.get_header('Foo').must_equal '1,1,2,yep'
+    @res.get_header('FOO').must_equal '1,1,2,yep'
+  end
+
+  it 'delete_header' do
+    lambda { @res.delete_header nil }.must_raise NoMethodError
+
+    @res.delete_header('FOO').must_equal '1'
+    @res.has_header?('FOO').must_equal false
+
+    @res.has_header?('Foo').must_equal false
+    @res.delete_header('Foo').must_be_nil
+  end
+end
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 9e8a6140..40bab4cd 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -27,7 +27,7 @@ describe Rack::Multipart do
   it "return nil if content type is not multipart" do
     env = Rack::MockRequest.env_for("/",
             "CONTENT_TYPE" => 'application/x-www-form-urlencoded')
-    Rack::Multipart.parse_multipart(env).must_equal nil
+    Rack::Multipart.parse_multipart(env).must_be_nil
   end
 
   it "parse multipart content when content type present but filename is not" do
@@ -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
@@ -99,11 +107,6 @@ describe Rack::Multipart do
     def rd.rewind; end
     wr.sync = true
 
-    # mock out length to make this pipe look like a Tempfile
-    def rd.length
-      1024 * 1024 * 8
-    end
-
     # write to a pipe in a background thread, this will write a lot
     # unless Rack (properly) shuts down the read end
     thr = Thread.new do
@@ -128,7 +131,7 @@ describe Rack::Multipart do
 
     fixture = {
       "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
-      "CONTENT_LENGTH" => rd.length.to_s,
+      "CONTENT_LENGTH" => (1024 * 1024 * 8).to_s,
       :input => rd,
     }
 
@@ -297,11 +300,17 @@ 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)
     params["submit-name"].must_equal "Larry"
-    params["files"].must_equal nil
+    params["files"].must_be_nil
     params.keys.wont_include "files"
   end
 
@@ -549,7 +558,7 @@ Content-Type: image/jpeg\r
 
   it "return nil if no UploadedFiles were used" do
     data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}])
-    data.must_equal nil
+    data.must_be_nil
   end
 
   it "raise ArgumentError if params is not a Hash" do
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 44fcfd45..bdad68fa 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -12,11 +12,31 @@ class RackRequestTest < Minitest::Spec
     refute_same req.env, req.dup.env
   end
 
+  it 'can check if something has been set' do
+    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+    refute req.has_header?("FOO")
+  end
+
   it "can get a key from the env" do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     assert_equal "example.com", req.get_header("SERVER_NAME")
   end
 
+  it 'can calculate the authority' do
+    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+    assert_equal "example.com:8080", req.authority
+  end
+
+  it 'can calculate the authority without a port' do
+    req = make_request(Rack::MockRequest.env_for("http://example.com/"))
+    assert_equal "example.com:80", req.authority
+  end
+
+  it 'can calculate the authority without a port on ssl' do
+    req = make_request(Rack::MockRequest.env_for("https://example.com/"))
+    assert_equal "example.com:443", req.authority
+  end
+
   it 'yields to the block if no value has been set' do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     yielded = false
@@ -29,17 +49,6 @@ class RackRequestTest < Minitest::Spec
     assert_equal "bar", req.get_header("FOO")
   end
 
-  it 'can set values in the env' do
-    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
-    req.set_header("FOO", "BAR")
-    assert_equal "BAR", req.get_header("FOO")
-  end
-
-  it 'can check if something has been set' do
-    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
-    refute req.has_header?("FOO")
-  end
-
   it 'can iterate over values' do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     req.set_header 'foo', 'bar'
@@ -50,6 +59,25 @@ class RackRequestTest < Minitest::Spec
     assert_equal 'bar', hash['foo']
   end
 
+  it 'can set values in the env' do
+    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+    req.set_header("FOO", "BAR")
+    assert_equal "BAR", req.get_header("FOO")
+  end
+
+  it 'can add to multivalued headers in the env' do
+    req = make_request(Rack::MockRequest.env_for('http://example.com:8080/'))
+
+    assert_equal '1', req.add_header('FOO', '1')
+    assert_equal '1', req.get_header('FOO')
+
+    assert_equal '1,2', req.add_header('FOO', '2')
+    assert_equal '1,2', req.get_header('FOO')
+
+    assert_equal '1,2', req.add_header('FOO', nil)
+    assert_equal '1,2', req.get_header('FOO')
+  end
+
   it 'can delete env values' do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     req.set_header 'foo', 'bar'
@@ -448,7 +476,7 @@ class RackRequestTest < Minitest::Spec
 
     req = make_request \
       Rack::MockRequest.env_for("/")
-    req.referer.must_equal nil
+    req.referer.must_be_nil
   end
 
   it "extract user agent correctly" do
@@ -458,25 +486,25 @@ class RackRequestTest < Minitest::Spec
 
     req = make_request \
       Rack::MockRequest.env_for("/")
-    req.user_agent.must_equal nil
+    req.user_agent.must_be_nil
   end
 
   it "treat missing content type as nil" do
     req = make_request \
       Rack::MockRequest.env_for("/")
-    req.content_type.must_equal nil
+    req.content_type.must_be_nil
   end
 
   it "treat empty content type as nil" do
     req = make_request \
       Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "")
-    req.content_type.must_equal nil
+    req.content_type.must_be_nil
   end
 
   it "return nil media type for empty content type" do
     req = make_request \
       Rack::MockRequest.env_for("/", "CONTENT_TYPE" => "")
-    req.media_type.must_equal nil
+    req.media_type.must_be_nil
   end
 
   it "cache, but invalidates the cache" do
@@ -1268,13 +1296,18 @@ EOF
     req.trusted_proxy?('unix').must_equal 0
     req.trusted_proxy?('unix:/tmp/sock').must_equal 0
 
-    req.trusted_proxy?("unix.example.org").must_equal nil
-    req.trusted_proxy?("example.org\n127.0.0.1").must_equal nil
-    req.trusted_proxy?("127.0.0.1\nexample.org").must_equal nil
-    req.trusted_proxy?("11.0.0.1").must_equal nil
-    req.trusted_proxy?("172.15.0.1").must_equal nil
-    req.trusted_proxy?("172.32.0.1").must_equal nil
-    req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal nil
+    req.trusted_proxy?("unix.example.org").must_be_nil
+    req.trusted_proxy?("example.org\n127.0.0.1").must_be_nil
+    req.trusted_proxy?("127.0.0.1\nexample.org").must_be_nil
+    req.trusted_proxy?("11.0.0.1").must_be_nil
+    req.trusted_proxy?("172.15.0.1").must_be_nil
+    req.trusted_proxy?("172.32.0.1").must_be_nil
+    req.trusted_proxy?("2001:470:1f0b:18f8::1").must_be_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
@@ -1339,8 +1372,8 @@ EOF
       include Rack::Request::Helpers
       extend Forwardable
 
-      def_delegators :@req, :get_header, :fetch_header, :delete_header,
-        :set_header, :has_header?, :each_header
+      def_delegators :@req, :has_header?, :get_header, :fetch_header,
+        :each_header, :set_header, :add_header, :delete_header
 
       def_delegators :@req, :[], :[]=, :values_at
 
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 73634b9e..4fd7d2b3 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -4,6 +4,22 @@ require 'rack/response'
 require 'stringio'
 
 describe Rack::Response do
+  it 'has cache-control methods' do
+    response = Rack::Response.new
+    cc = 'foo'
+    response.cache_control = cc
+    assert_equal cc, response.cache_control
+    assert_equal cc, response.to_a[2]['Cache-Control']
+  end
+
+  it 'has an etag method' do
+    response = Rack::Response.new
+    etag = 'foo'
+    response.etag = etag
+    assert_equal etag, response.etag
+    assert_equal etag, response.to_a[2]['ETag']
+  end
+
   it "have sensible default values" do
     response = Rack::Response.new
     status, header, body = response.finish
@@ -39,7 +55,7 @@ describe Rack::Response do
 
   it "can set and read headers" do
     response = Rack::Response.new
-    response["Content-Type"].must_equal nil
+    response["Content-Type"].must_be_nil
     response["Content-Type"] = "text/plain"
     response["Content-Type"].must_equal "text/plain"
   end
@@ -99,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"
@@ -106,7 +186,7 @@ describe Rack::Response do
     response.delete_cookie "foo"
     response["Set-Cookie"].must_equal [
       "foo2=bar2",
-      "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
+      "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
     ].join("\n")
   end
 
@@ -116,10 +196,10 @@ describe Rack::Response do
     response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
     response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
     response.delete_cookie "foo", :domain => ".example.com"
-    response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+    response["Set-Cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
     response.delete_cookie "foo", :domain => "sample.example.com"
-    response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000",
-                                         "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+    response["Set-Cookie"].must_equal ["foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
+                                         "foo=; domain=sample.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
   end
 
   it "can delete cookies with the same name with different paths" do
@@ -131,7 +211,7 @@ describe Rack::Response do
 
     response.delete_cookie "foo", :path => "/path"
     response["Set-Cookie"].must_equal ["foo=bar; path=/",
-                                         "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"].join("\n")
+                                         "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
   end
 
   it "can do redirects" do
@@ -193,8 +273,8 @@ describe Rack::Response do
     _, header, body = r.finish
     str = ""; body.each { |part| str << part }
     str.must_be :empty?
-    header["Content-Type"].must_equal nil
-    header['Content-Length'].must_equal nil
+    header["Content-Type"].must_be_nil
+    header['Content-Length'].must_be_nil
 
     lambda {
       Rack::Response.new(Object.new)
@@ -330,7 +410,7 @@ describe Rack::Response do
     res.body.must_be :closed?
   end
 
-  it "calls close on #body when 204, 205, or 304" do
+  it "calls close on #body when 204 or 304" do
     res = Rack::Response.new
     res.body = StringIO.new
     res.finish
@@ -344,7 +424,7 @@ describe Rack::Response do
     res.body = StringIO.new
     res.status = 205
     _, _, b = res.finish
-    res.body.must_be :closed?
+    res.body.wont_be :closed?
     b.wont_equal res.body
 
     res.body = StringIO.new
@@ -360,3 +440,71 @@ describe Rack::Response do
     lambda { res.finish.last.to_ary }.must_raise NoMethodError
   end
 end
+
+describe Rack::Response, 'headers' do
+  before do
+    @response = Rack::Response.new([], 200, { 'Foo' => '1' })
+  end
+
+  it 'has_header?' do
+    lambda { @response.has_header? nil }.must_raise NoMethodError
+
+    @response.has_header?('Foo').must_equal true
+    @response.has_header?('foo').must_equal true
+  end
+
+  it 'get_header' do
+    lambda { @response.get_header nil }.must_raise NoMethodError
+
+    @response.get_header('Foo').must_equal '1'
+    @response.get_header('foo').must_equal '1'
+  end
+
+  it 'set_header' do
+    lambda { @response.set_header nil, '1' }.must_raise NoMethodError
+
+    @response.set_header('Foo', '2').must_equal '2'
+    @response.has_header?('Foo').must_equal true
+    @response.get_header('Foo').must_equal('2')
+
+    @response.set_header('Foo', nil).must_be_nil
+    @response.has_header?('Foo').must_equal true
+    @response.get_header('Foo').must_be_nil
+  end
+
+  it 'add_header' do
+    lambda { @response.add_header nil, '1' }.must_raise NoMethodError
+
+    # Add a value to an existing header
+    @response.add_header('Foo', '2').must_equal '1,2'
+    @response.get_header('Foo').must_equal '1,2'
+
+    # Add nil to an existing header
+    @response.add_header('Foo', nil).must_equal '1,2'
+    @response.get_header('Foo').must_equal '1,2'
+
+    # Add nil to a nonexistent header
+    @response.add_header('Bar', nil).must_be_nil
+    @response.has_header?('Bar').must_equal false
+    @response.get_header('Bar').must_be_nil
+
+    # Add a value to a nonexistent header
+    @response.add_header('Bar', '1').must_equal '1'
+    @response.has_header?('Bar').must_equal true
+    @response.get_header('Bar').must_equal '1'
+  end
+
+  it 'delete_header' do
+    lambda { @response.delete_header nil }.must_raise NoMethodError
+
+    @response.delete_header('Foo').must_equal '1'
+    (!!@response.has_header?('Foo')).must_equal false
+
+    @response.delete_header('Foo').must_be_nil
+    @response.has_header?('Foo').must_equal false
+
+    @response.set_header('Foo', 1)
+    @response.delete_header('foo').must_equal 1
+    @response.has_header?('Foo').must_equal false
+  end
+end
diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb
index 18689857..1eb9413c 100644
--- a/test/spec_sendfile.rb
+++ b/test/spec_sendfile.rb
@@ -122,4 +122,39 @@ describe Rack::Sendfile do
       FileUtils.remove_entry_secure dir2
     end
   end
+
+  it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings via header" do
+    begin
+      dir1 = Dir.mktmpdir
+      dir2 = Dir.mktmpdir
+
+      first_body = open_file(File.join(dir1, 'rack_sendfile'))
+      first_body.puts 'hello world'
+
+      second_body = open_file(File.join(dir2, 'rack_sendfile'))
+      second_body.puts 'goodbye world'
+
+      headers = {
+        'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+        'HTTP_X_ACCEL_MAPPING' => "#{dir1}/=/foo/bar/, #{dir2}/=/wibble/"
+      }
+
+      request(headers, first_body) do |response|
+        response.must_be :ok?
+        response.body.must_be :empty?
+        response.headers['Content-Length'].must_equal '0'
+        response.headers['X-Accel-Redirect'].must_equal '/foo/bar/rack_sendfile'
+      end
+
+      request(headers, second_body) do |response|
+        response.must_be :ok?
+        response.body.must_be :empty?
+        response.headers['Content-Length'].must_equal '0'
+        response.headers['X-Accel-Redirect'].must_equal '/wibble/rack_sendfile'
+      end
+    ensure
+      FileUtils.remove_entry_secure dir1
+      FileUtils.remove_entry_secure dir2
+    end
+  end
 end
diff --git a/test/spec_server.rb b/test/spec_server.rb
index a3690bce..4864a87a 100644
--- a/test/spec_server.rb
+++ b/test/spec_server.rb
@@ -77,7 +77,7 @@ describe Rack::Server do
       o, ENV["REQUEST_METHOD"] = ENV["REQUEST_METHOD"], 'foo'
       server = Rack::Server.new(:app => 'foo')
       server.server.name =~ /CGI/
-      Rack::Server.logging_middleware.call(server).must_equal nil
+      Rack::Server.logging_middleware.call(server).must_be_nil
     ensure
       ENV['REQUEST_METHOD'] = o
     end
@@ -85,7 +85,7 @@ describe Rack::Server do
 
   it "be quiet if said so" do
     server = Rack::Server.new(:app => "FOO", :quiet => true)
-    Rack::Server.logging_middleware.call(server).must_equal nil
+    Rack::Server.logging_middleware.call(server).must_be_nil
   end
 
   it "use a full path to the pidfile" do
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_session_cookie.rb b/test/spec_session_cookie.rb
index 2b382b50..9201a729 100644
--- a/test/spec_session_cookie.rb
+++ b/test/spec_session_cookie.rb
@@ -98,18 +98,18 @@ describe Rack::Session::Cookie do
 
       it 'rescues failures on decode' do
         coder = Rack::Session::Cookie::Base64::Marshal.new
-        coder.decode('lulz').must_equal nil
+        coder.decode('lulz').must_be_nil
       end
     end
 
     describe 'JSON' do
-      it 'marshals and base64 encodes' do
+      it 'JSON and base64 encodes' do
         coder = Rack::Session::Cookie::Base64::JSON.new
         obj   = %w[fuuuuu]
         coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m')
       end
 
-      it 'marshals and base64 decodes' do
+      it 'JSON and base64 decodes' do
         coder = Rack::Session::Cookie::Base64::JSON.new
         str   = [::JSON.dump(%w[fuuuuu])].pack('m')
         coder.decode(str).must_equal ::JSON.parse(str.unpack('m').first)
@@ -117,7 +117,7 @@ describe Rack::Session::Cookie do
 
       it 'rescues failures on decode' do
         coder = Rack::Session::Cookie::Base64::JSON.new
-        coder.decode('lulz').must_equal nil
+        coder.decode('lulz').must_be_nil
       end
     end
 
@@ -139,7 +139,7 @@ describe Rack::Session::Cookie do
 
       it 'rescues failures on decode' do
         coder = Rack::Session::Cookie::Base64::ZipJSON.new
-        coder.decode('lulz').must_equal nil
+        coder.decode('lulz').must_be_nil
       end
     end
   end
@@ -311,6 +311,22 @@ describe Rack::Session::Cookie do
     response.body.must_equal '{"counter"=>2}'
   end
 
+  it "supports custom digest class" do
+    app = [incrementor, { :secret => "test", hmac: OpenSSL::Digest::SHA256 }]
+
+    response = response_for(:app => app)
+    response = response_for(:app => app, :cookie => response)
+    response.body.must_equal '{"counter"=>2}'
+
+    response = response_for(:app => app, :cookie => response)
+    response.body.must_equal '{"counter"=>3}'
+
+    app = [incrementor, { :secret => "other" }]
+
+    response = response_for(:app => app, :cookie => response)
+    response.body.must_equal '{"counter"=>1}'
+  end
+
   it "can handle Rack::Lint middleware" do
     response = response_for(:app => incrementor)
 
diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb
index 08ef5f70..93a03d12 100644
--- a/test/spec_session_memcache.rb
+++ b/test/spec_session_memcache.rb
@@ -35,9 +35,9 @@ begin
     Rack::Session::Memcache.new(incrementor)
 
     it "faults on no connection" do
-      lambda{
+      lambda {
         Rack::Session::Memcache.new(incrementor, :memcache_server => 'nosuchserver')
-      }.should.raise
+      }.must_raise(RuntimeError).message.must_equal 'No memcache servers'
     end
 
     it "connects to existing server" do
@@ -143,7 +143,7 @@ begin
       res1.body.must_equal '{"counter"=>1}'
 
       res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
-      res2["Set-Cookie"].must_equal nil
+      res2["Set-Cookie"].must_be_nil
       res2.body.must_equal '{"counter"=>2}'
 
       res3 = req.get("/", "HTTP_COOKIE" => cookie)
@@ -183,7 +183,7 @@ begin
       creq = Rack::MockRequest.new(count)
 
       res0 = dreq.get("/")
-      res0["Set-Cookie"].must_equal nil
+      res0["Set-Cookie"].must_be_nil
       res0.body.must_equal '{"counter"=>1}'
 
       res0 = creq.get("/")
@@ -201,7 +201,7 @@ begin
       creq = Rack::MockRequest.new(count)
 
       res0 = sreq.get("/")
-      res0["Set-Cookie"].must_equal nil
+      res0["Set-Cookie"].must_be_nil
       res0.body.must_equal '{"counter"=>1}'
 
       res0 = creq.get("/")
diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb
index 5eaadfff..2d061691 100644
--- a/test/spec_session_pool.rb
+++ b/test/spec_session_pool.rb
@@ -138,7 +138,7 @@ describe Rack::Session::Pool do
     dreq = Rack::MockRequest.new(defer)
 
     res1 = dreq.get("/")
-    res1["Set-Cookie"].must_equal nil
+    res1["Set-Cookie"].must_be_nil
     res1.body.must_equal '{"counter"=>1}'
     pool.pool.size.must_equal 1
   end
diff --git a/test/spec_static.rb b/test/spec_static.rb
index f0a47171..634f8acf 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -97,7 +97,7 @@ describe Rack::Static do
   it "serves regular files if client accepts gzip encoding and gzip files are not present" do
     res = @gzip_request.get("/cgi/rackup_stub.rb", 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip')
     res.must_be :ok?
-    res.headers['Content-Encoding'].must_equal nil
+    res.headers['Content-Encoding'].must_be_nil
     res.headers['Content-Type'].must_equal 'text/x-script.ruby'
     res.body.must_match(/ruby/)
   end
@@ -105,7 +105,7 @@ describe Rack::Static do
   it "serves regular files if client does not accept gzip encoding" do
     res = @gzip_request.get("/cgi/test")
     res.must_be :ok?
-    res.headers['Content-Encoding'].must_equal nil
+    res.headers['Content-Encoding'].must_be_nil
     res.headers['Content-Type'].must_equal 'text/plain'
     res.body.must_match(/ruby/)
   end
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 76198fb8..143ad30a 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -64,7 +64,7 @@ describe Rack::Utils do
   end
 
   it "not hang on escaping long strings that end in % (http://redmine.ruby-lang.org/issues/5149)" do
-    timeout(1) do
+    Timeout.timeout(1) do
       lambda {
         URI.decode_www_form_component "A string that causes catastrophic backtracking as it gets longer %"
       }.must_raise ArgumentError
@@ -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
@@ -345,23 +368,6 @@ describe Rack::Utils do
     Rack::Utils.build_query(key => nil).must_equal Rack::Utils.escape(key)
   end
 
-  it "parse cookies" do
-    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m")
-    Rack::Utils.parse_cookies(env).must_equal({"zoo" => "m"})
-
-    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%")
-    Rack::Utils.parse_cookies(env).must_equal({"foo" => "%"})
-
-    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;foo=car")
-    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"})
-
-    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m")
-    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar", "quux" => "h&m"})
-
-    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar").freeze
-    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"})
-  end
-
   it "parse q-values" do
     # XXX handle accept-extension
     Rack::Utils.q_values("foo;q=0.5,bar,baz;q=0.9").must_equal [
@@ -388,7 +394,7 @@ describe Rack::Utils do
     Rack::Utils.best_q_match("text/plain,text/html", %w[text/html text/plain]).must_equal "text/html"
 
     # When there are no matches, return nil:
-    Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_equal nil
+    Rack::Utils.best_q_match("application/json", %w[text/html text/plain]).must_be_nil
   end
 
   it "escape html entities [&><'\"/]" do
@@ -421,9 +427,9 @@ describe Rack::Utils do
       Rack::Utils.select_best_encoding(a, b)
     end
 
-    helper.call(%w(), [["x", 1]]).must_equal nil
-    helper.call(%w(identity), [["identity", 0.0]]).must_equal nil
-    helper.call(%w(identity), [["*", 0.0]]).must_equal nil
+    helper.call(%w(), [["x", 1]]).must_be_nil
+    helper.call(%w(identity), [["identity", 0.0]]).must_be_nil
+    helper.call(%w(identity), [["*", 0.0]]).must_be_nil
 
     helper.call(%w(identity), [["compress", 1.0], ["gzip", 1.0]]).must_equal "identity"
 
@@ -483,17 +489,64 @@ describe Rack::Utils do
   end
 end
 
+describe Rack::Utils, "cookies" do
+  it "parses cookies" do
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m")
+    Rack::Utils.parse_cookies(env).must_equal({"zoo" => "m"})
+
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=%")
+    Rack::Utils.parse_cookies(env).must_equal({"foo" => "%"})
+
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;foo=car")
+    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"})
+
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar;quux=h&m")
+    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar", "quux" => "h&m"})
+
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "foo=bar").freeze
+    Rack::Utils.parse_cookies(env).must_equal({"foo" => "bar"})
+  end
+
+  it "adds new cookies to nil header" do
+    Rack::Utils.add_cookie_to_header(nil, 'name', 'value').must_equal 'name=value'
+  end
+
+  it "adds new cookies to blank header" do
+    header = ''
+    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal 'name=value'
+    header.must_equal ''
+  end
+
+  it "adds new cookies to string header" do
+    header = 'existing-cookie'
+    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value"
+    header.must_equal 'existing-cookie'
+  end
+
+  it "adds new cookies to array header" do
+    header = %w[ existing-cookie ]
+    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value"
+    header.must_equal %w[ existing-cookie ]
+  end
+
+  it "adds new cookies to an unrecognized header" do
+    lambda {
+      Rack::Utils.add_cookie_to_header(Object.new, 'name', 'value')
+    }.must_raise ArgumentError
+  end
+end
+
 describe Rack::Utils, "byte_range" do
   it "ignore missing or syntactically invalid byte ranges" do
-    Rack::Utils.byte_ranges({},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "foobar"},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "furlongs=123-456"},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes="},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-"},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123,456"},500).must_equal nil
+    Rack::Utils.byte_ranges({},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "foobar"},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "furlongs=123-456"},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes="},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-"},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123,456"},500).must_be_nil
     # A range of non-positive length is syntactically invalid and ignored:
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-123"},500).must_equal nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_equal nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-123"},500).must_be_nil
+    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_be_nil
   end
 
   it "parse simple byte ranges" do
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
index 8e0360d2..e3050f6f 100644
--- a/test/spec_webrick.rb
+++ b/test/spec_webrick.rb
@@ -1,6 +1,6 @@
 require 'minitest/autorun'
 require 'rack/mock'
-require 'concurrent/atomic/count_down_latch'
+require 'concurrent/atomic/event'
 require File.expand_path('../testrequest', __FILE__)
 
 Thread.abort_on_exception = true
@@ -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
@@ -106,8 +119,7 @@ describe Rack::Handler::WEBrick do
   end
 
   it "provide a .run" do
-    block_ran = false
-    latch = Concurrent::CountDownLatch.new 1
+    latch = Concurrent::Event.new
 
     t = Thread.new do
       Rack::Handler::WEBrick.run(lambda {},
@@ -116,10 +128,9 @@ describe Rack::Handler::WEBrick do
                                    :Port => 9210,
                                    :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
                                    :AccessLog => []}) { |server|
-        block_ran = true
         assert_kind_of WEBrick::HTTPServer, server
         @s = server
-        latch.count_down
+        latch.set
       }
     end
 
@@ -158,7 +169,7 @@ describe Rack::Handler::WEBrick do
     Rack::Lint.new(lambda{ |req|
       [
         200,
-        {"rack.hijack" => io_lambda},
+        [ [ "rack.hijack", io_lambda ] ],
         [""]
       ]
     })
@@ -182,12 +193,13 @@ describe Rack::Handler::WEBrick do
     Net::HTTP.start(@host, @port){ |http|
       res = http.get("/chunked")
       res["Transfer-Encoding"].must_equal "chunked"
-      res["Content-Length"].must_equal nil
+      res["Content-Length"].must_be_nil
       res.body.must_equal "chunked"
     }
   end
 
   after do
+  @status_thread.join
   @server.shutdown
   @thread.join
   end