summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md124
-rw-r--r--Gemfile2
-rw-r--r--README.rdoc4
-rw-r--r--lib/rack/auth/basic.rb3
-rw-r--r--lib/rack/auth/digest/md5.rb8
-rw-r--r--lib/rack/auth/digest/nonce.rb7
-rw-r--r--lib/rack/builder.rb6
-rw-r--r--lib/rack/cascade.rb5
-rw-r--r--lib/rack/common_logger.rb4
-rw-r--r--lib/rack/core_ext/regexp.rb14
-rw-r--r--lib/rack/deflater.rb11
-rw-r--r--lib/rack/directory.rb5
-rw-r--r--lib/rack/file.rb176
-rw-r--r--lib/rack/files.rb178
-rw-r--r--lib/rack/handler.rb7
-rw-r--r--lib/rack/media_type.rb13
-rw-r--r--lib/rack/multipart/parser.rb8
-rw-r--r--lib/rack/query_parser.rb56
-rw-r--r--lib/rack/reloader.rb6
-rw-r--r--lib/rack/request.rb61
-rw-r--r--lib/rack/response.rb9
-rw-r--r--lib/rack/sendfile.rb7
-rw-r--r--lib/rack/server.rb6
-rw-r--r--lib/rack/session/abstract/id.rb26
-rw-r--r--lib/rack/session/cookie.rb9
-rw-r--r--lib/rack/session/memcache.rb5
-rw-r--r--lib/rack/simple_body_proxy.rb13
-rw-r--r--lib/rack/static.rb19
-rw-r--r--lib/rack/utils.rb55
-rw-r--r--rack.gemspec9
-rw-r--r--test/builder/anything.rb7
-rw-r--r--test/helper.rb2
-rw-r--r--test/spec_auth_basic.rb2
-rw-r--r--test/spec_auth_digest.rb2
-rw-r--r--test/spec_body_proxy.rb2
-rw-r--r--test/spec_builder.rb9
-rw-r--r--test/spec_cascade.rb6
-rw-r--r--test/spec_chunked.rb2
-rw-r--r--test/spec_common_logger.rb2
-rw-r--r--test/spec_conditional_get.rb2
-rw-r--r--test/spec_config.rb2
-rw-r--r--test/spec_content_length.rb2
-rw-r--r--test/spec_content_type.rb2
-rw-r--r--test/spec_deflater.rb17
-rw-r--r--test/spec_directory.rb2
-rw-r--r--test/spec_etag.rb2
-rw-r--r--test/spec_files.rb (renamed from test/spec_file.rb)62
-rw-r--r--test/spec_handler.rb2
-rw-r--r--test/spec_head.rb2
-rw-r--r--test/spec_lint.rb2
-rw-r--r--test/spec_lobster.rb2
-rw-r--r--test/spec_lock.rb2
-rw-r--r--test/spec_logger.rb2
-rw-r--r--test/spec_media_type.rb2
-rw-r--r--test/spec_method_override.rb2
-rw-r--r--test/spec_mime.rb2
-rw-r--r--test/spec_mock.rb2
-rw-r--r--test/spec_multipart.rb2
-rw-r--r--test/spec_null_logger.rb2
-rw-r--r--test/spec_recursive.rb2
-rw-r--r--test/spec_request.rb42
-rw-r--r--test/spec_response.rb30
-rw-r--r--test/spec_rewindable_input.rb2
-rw-r--r--test/spec_runtime.rb2
-rw-r--r--test/spec_sendfile.rb21
-rw-r--r--test/spec_server.rb4
-rw-r--r--test/spec_session_abstract_id.rb2
-rw-r--r--test/spec_session_abstract_session_hash.rb8
-rw-r--r--test/spec_session_cookie.rb2
-rw-r--r--test/spec_session_memcache.rb2
-rw-r--r--test/spec_session_pool.rb2
-rw-r--r--test/spec_show_exceptions.rb2
-rw-r--r--test/spec_show_status.rb2
-rw-r--r--test/spec_static.rb2
-rw-r--r--test/spec_tempfile_reaper.rb2
-rw-r--r--test/spec_thin.rb2
-rw-r--r--test/spec_urlmap.rb2
-rw-r--r--test/spec_utils.rb6
-rw-r--r--test/spec_version.rb2
-rw-r--r--test/spec_webrick.rb2
80 files changed, 704 insertions, 442 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1286d14a..1825ddc5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,27 +1,125 @@
 # Changelog
-All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/)
 
-## [Unreleased]
+All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
+
+## Unreleased
+
+_Note: There are many unreleased changes in Rack (`master` is around 300 commits ahead of `2-0-stable`), and below is not an exhaustive list. If you would like to help out and document some of the unreleased changes, PRs are welcome._
+
 ### Added
-- CHANGELOG.md using keep a changelog formatting by @twitnithegirl
 
 ### Changed
-- `Rack::Utils.status_code` now raises an error when the status symbol is invalid instead of `500`.
-- `Rack::Request::SCHEME_WHITELIST` has been renamed to `Rack::Request::ALLOWED_SCHEMES`
-- `Rack::Multipart::Parser.get_filename` now accepts file that contains `+` in its name, avoiding the replacement of `+` to space character since filenames with `+` are valid.
+
+- Use `Time#httpdate` format for Expires, as proposed by RFC 7231. ([@nanaya](https://github.com/nanaya))
+- Make `Utils.status_code` raise an error when the status symbol is invalid instead of `500`.
+- Rename `Request::SCHEME_WHITELIST` to `Request::ALLOWED_SCHEMES`.
+- Make `Multipart::Parser.get_filename` accept files with `+` in their name.
+- Add Falcon to the default handler fallbacks. ([@ioquatix](https://github.com/ioquatix))
+- Update codebase to avoid string mutations in preparation for `frozen_string_literals`. ([@pat](https://github.com/pat))
+- Change `MockRequest#env_for` to rely on the input optionally responding to `#size` instead of `#length`. ([@janko](https://github.com/janko))
+- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern)).
 
 ### Removed
-- HISTORY.md by @twitnithegirl
-- NEWS.md by @twitnithegirl
+
+### Documentation
+
+- Update broken example in `Session::Abstract::ID` documentation. ([tonytonyjan](https://github.com/tonytonyjan))
+- Add Padrino to the list of frameworks implmenting Rack. ([@wikimatze](https://github.com/wikimatze))
+- Remove Mongrel from the suggested server options in the help output. ([@tricknotes](https://github.com/tricknotes))
+- Replace `HISTORY.md` and `NEWS.md` with `CHANGELOG.md`. ([@twitnithegirl](https://github.com/twitnithegirl))
+- Backfill `CHANGELOG.md` from 2.0.1 to 2.0.7 releases. ([@drenmi](https://github.com/Drenmi))
+
+## [2.0.7] - 2019-04-02
+
+### Fixed
+
+- Remove calls to `#eof?` on Rack input in `Multipart::Parser`, as this breaks the specification. ([@matthewd](https://github.com/matthewd))
+- Preserve forwarded IP addresses for trusted proxy chains. ([@SamSaffron](https://github.com/SamSaffron))
+
+## [2.0.6] - 2018-11-05
+
+### Fixed
+
+- [[CVE-2018-16470](https://nvd.nist.gov/vuln/detail/CVE-2018-16470)] Reduce buffer size of `Multipart::Parser` to avoid pathological parsing. ([@tenderlove](https://github.com/tenderlove))
+- Fix a call to a non-existing method `#accepts_html` in the `ShowExceptions` middleware. ([@tomelm](https://github.com/tomelm))
+- [[CVE-2018-16471](https://nvd.nist.gov/vuln/detail/CVE-2018-16471)] Whitelist HTTP and HTTPS schemes in `Request#scheme` to prevent a possible XSS attack. ([@PatrickTulskie](https://github.com/PatrickTulskie))
+
+## [2.0.5] - 2018-04-23
+
+### Fixed
+
+- Record errors originating from invalid UTF8 in `MethodOverride` middleware instead of breaking. ([@mclark](https://github.com/mclark))
+
+## [2.0.4] - 2018-01-31
+
+### Changed
+
+- Ensure the `Lock` middleware passes the original `env` object. ([@lugray](https://github.com/lugray))
+- Improve performance of `Multipart::Parser` when uploading large files. ([@tompng](https://github.com/tompng))
+- Increase buffer size in `Multipart::Parser` for better performance. ([@jkowens](https://github.com/jkowens))
+- Reduce memory usage of `Multipart::Parser` when uploading large files. ([@tompng](https://github.com/tompng))
+- Replace ConcurrentRuby dependency with native `Queue`. ([@devmchakan](https://github.com/devmchakan))
+
+### Fixed
+
+- Require the correct digest algorithm in the `ETag` middleware. ([@matthewd](https://github.com/matthewd))
+
+### Documentation
+
+- Update homepage links to use SSL. ([@hugoabonizio](https://github.com/hugoabonizio))
+
+## [2.0.3] - 2017-05-15
+
+### Changed
+
+- Ensure `env` values are ASCII 8-bit encoded. ([@eileencodes](https://github.com/eileencodes))
+
+### Fixed
+
+- Prevent exceptions when a class with mixins inherits from `Session::Abstract::ID`. ([@jnraine](https://github.com/jnraine))
+
+## [2.0.2] - 2017-05-08
+
+### Added
+
+- Allow `Session::Abstract::SessionHash#fetch` to accept a block with a default value. ([@yannvanhalewyn](https://github.com/yannvanhalewyn))
+- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans](https://github.com/jeremyevans))
+
+### Changed
+
+- Freeze default session options to avoid accidental mutation. ([@kirs](https://github.com/kirs))
+- Detect partial hijack without hash headers. ([@devmchakan](https://github.com/devmchakan))
+- Update tests to use MiniTest 6 matchers. ([@tonytonyjan](https://github.com/tonytonyjan))
+- Allow 205 Reset Content responses to set a Content-Length, as RFC 7231 proposes setting this to 0. ([@devmchakan](https://github.com/devmchakan))
+
+### Fixed
+
+- Handle `NULL` bytes in multipart filenames. ([@casperisfine](https://github.com/casperisfine))
+- Remove warnings due to miscapitalized global. ([@ioquatix](https://github.com/ioquatix))
+- Prevent exceptions caused by a race condition on multi-threaded servers. ([@sophiedeziel](https://github.com/sophiedeziel))
+- Add RDoc as an explicit depencency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan))
+- Record errors originating from `Multipart::Parser` in the `MethodOverride` middleware instead of letting them bubble up. ([@carlzulauf](https://github.com/carlzulauf))
+- Remove remaining use of removed `Utils#bytesize` method from the `File` middleware. ([@brauliomartinezlm](https://github.com/brauliomartinezlm))
+
+### Removed
+
+- Remove `deflate` encoding support to reduce caching overhead. ([@devmchakan](https://github.com/devmchakan))
+
+### Documentation
+
+- Update broken example in `Deflater` documentation. ([@mwpastore](https://github.com/mwpastore))
+
+## [2.0.1] - 2016-06-30
+
+### Changed
+
+- Remove JSON as an explicit dependency. ([@mperham](https://github.com/mperham))
 
 
-#
-#
 # History/News Archive
 Items below this line are from the previously maintained HISTORY.md and NEWS.md files.
-#
 
-## [2.0.0]
+## [2.0.0.rc1] 2016-05-06
 - Rack::Session::Abstract::ID is deprecated. Please change to use Rack::Session::Abstract::Persisted
 
 ## [2.0.0.alpha] 2015-12-04
@@ -119,7 +217,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
   - Rack::Auth::AbstractRequest#scheme now yields strings, not symbols
   - Rack::Utils cookie functions now format expires in RFC 2822 format
   - Rack::File now has a default mime type
-  - rackup -b 'run Rack::File.new(".")', option provides command line configs
+  - rackup -b 'run Rack::Files.new(".")', option provides command line configs
   - Rack::Deflater will no longer double encode bodies
   - Rack::Mime#match? provides convenience for Accept header matching
   - Rack::Utils#q_values provides splitting for Accept headers
diff --git a/Gemfile b/Gemfile
index de5bae5f..b9d9d748 100644
--- a/Gemfile
+++ b/Gemfile
@@ -12,7 +12,7 @@ c_platforms = Bundler::Dsl::VALID_PLATFORMS.dup.delete_if do |platform|
   platform =~ /jruby/
 end
 
-gem "rubocop", require: false
+gem "rubocop", "0.68.1", require: false
 
 # Alternative solution that might work, but it has bad interactions with
 # Gemfile.lock if that gets committed/reused:
diff --git a/README.rdoc b/README.rdoc
index 4b66e779..89bb475a 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -32,12 +32,12 @@ These web servers include \Rack handlers in their distributions:
 * Ebb
 * Fuzed
 * Glassfish v3
+* NGINX Unit
 * Phusion Passenger (which is mod_rack for Apache and for nginx)
 * Puma
 * Reel
 * unixrack
 * uWSGI
-* yahns
 
 Any valid \Rack app will run the same on all these handlers, without
 changing anything.
@@ -76,7 +76,7 @@ applications needs using middleware, for example:
 * Rack::CommonLogger, for creating Apache-style logfiles.
 * Rack::ShowException, for catching unhandled exceptions and
   presenting them in a nice and helpful way with clickable backtrace.
-* Rack::File, for serving static files.
+* Rack::Files, for serving static files.
 * ...many others!
 
 All these components use the same interface, which is described in
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index dfe2ce96..d334939c 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -2,6 +2,7 @@
 
 require 'rack/auth/abstract/handler'
 require 'rack/auth/abstract/request'
+require 'base64'
 
 module Rack
   module Auth
@@ -47,7 +48,7 @@ module Rack
         end
 
         def credentials
-          @credentials ||= params.unpack("m*").first.split(/:/, 2)
+          @credentials ||= Base64.decode64(params).split(':', 2)
         end
 
         def username
diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb
index ec6d8748..62bff984 100644
--- a/lib/rack/auth/digest/md5.rb
+++ b/lib/rack/auth/digest/md5.rb
@@ -108,21 +108,21 @@ module Rack
         alias :H :md5
 
         def KD(secret, data)
-          H([secret, data] * ':')
+          H "#{secret}:#{data}"
         end
 
         def A1(auth, password)
-          [ auth.username, auth.realm, password ] * ':'
+          "#{auth.username}:#{auth.realm}:#{password}"
         end
 
         def A2(auth)
-          [ auth.method, auth.uri ] * ':'
+          "#{auth.method}:#{auth.uri}"
         end
 
         def digest(auth, password)
           password_hash = passwords_hashed? ? password : H(A1(auth, password))
 
-          KD(password_hash, [ auth.nonce, auth.nc, auth.cnonce, QOP, H(A2(auth)) ] * ':')
+          KD password_hash, "#{auth.nonce}:#{auth.nc}:#{auth.cnonce}:#{QOP}:#{H A2(auth)}"
         end
 
       end
diff --git a/lib/rack/auth/digest/nonce.rb b/lib/rack/auth/digest/nonce.rb
index 6c1f28a3..3216d973 100644
--- a/lib/rack/auth/digest/nonce.rb
+++ b/lib/rack/auth/digest/nonce.rb
@@ -1,6 +1,7 @@
 # frozen_string_literal: true
 
 require 'digest/md5'
+require 'base64'
 
 module Rack
   module Auth
@@ -20,7 +21,7 @@ module Rack
         end
 
         def self.parse(string)
-          new(*string.unpack("m*").first.split(' ', 2))
+          new(*Base64.decode64(string).split(' ', 2))
         end
 
         def initialize(timestamp = Time.now, given_digest = nil)
@@ -28,11 +29,11 @@ module Rack
         end
 
         def to_s
-          [([ @timestamp, digest ] * ' ')].pack("m*").strip
+          Base64.encode64("#{@timestamp} #{digest}").strip
         end
 
         def digest
-          ::Digest::MD5.hexdigest([ @timestamp, self.class.private_key ] * ':')
+          ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}")
         end
 
         def valid?
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 92af0e58..dcd40c76 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -31,10 +31,12 @@ module Rack
   # You can use +map+ to construct a Rack::URLMap in a convenient way.
 
   class Builder
+
+    # https://stackoverflow.com/questions/2223882/whats-the-difference-between-utf-8-and-utf-8-without-bom
     UTF_8_BOM = '\xef\xbb\xbf'
 
     def self.parse_file(config, opts = Server::Options.new)
-      if config =~ /\.ru$/
+      if config.end_with?('.ru')
         return self.load_file(config, opts)
       else
         require config
@@ -95,7 +97,7 @@ module Rack
     def use(middleware, *args, &block)
       if @map
         mapping, @map = @map, nil
-        @use << proc { |app| generate_map app, mapping }
+        @use << proc { |app| generate_map(app, mapping) }
       end
       @use << proc { |app| middleware.new(app, *args, &block) }
     end
diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb
index 76bc9a1a..1ed7ffa9 100644
--- a/lib/rack/cascade.rb
+++ b/lib/rack/cascade.rb
@@ -11,7 +11,7 @@ module Rack
     attr_reader :apps
 
     def initialize(apps, catch = [404, 405])
-      @apps = []; @has_app = {}
+      @apps = []
       apps.each { |app| add app }
 
       @catch = {}
@@ -41,12 +41,11 @@ module Rack
     end
 
     def add(app)
-      @has_app[app] = true
       @apps << app
     end
 
     def include?(app)
-      @has_app.include? app
+      @apps.include?(app)
     end
 
     alias_method :<<, :add
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index 71e35394..692b2070 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -66,8 +66,8 @@ module Rack
     end
 
     def extract_content_length(headers)
-      value = headers[CONTENT_LENGTH] or return '-'
-      value.to_s == '0' ? '-' : value
+      value = headers[CONTENT_LENGTH]
+      !value || value.to_s == '0' ? '-' : value
     end
   end
 end
diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb
new file mode 100644
index 00000000..a32fcdf6
--- /dev/null
+++ b/lib/rack/core_ext/regexp.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Regexp has `match?` since Ruby 2.4
+# so to support Ruby < 2.4 we need to define this method
+
+module Rack
+  module RegexpExtensions
+    refine Regexp do
+      def match?(string, pos = 0)
+        !!match(string, pos)
+      end
+    end unless //.respond_to?(:match?)
+  end
+end
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index 67598ef2..939832ce 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -4,6 +4,8 @@ require "zlib"
 require "time"  # for Time.httpdate
 require 'rack/utils'
 
+require_relative 'core_ext/regexp'
+
 module Rack
   # This middleware enables compression of http responses.
   #
@@ -17,6 +19,8 @@ module Rack
   # directive of 'no-transform' is present, or when the response status
   # code is one that doesn't allow an entity body.
   class Deflater
+    using ::Rack::RegexpExtensions
+
     ##
     # Creates Rack::Deflater middleware.
     #
@@ -84,8 +88,9 @@ module Rack
         gzip = ::Zlib::GzipWriter.new(self)
         gzip.mtime = @mtime if @mtime
         @body.each { |part|
-          gzip.write(part)
-          gzip.flush if @sync
+          len = gzip.write(part)
+          # Flushing empty parts would raise Zlib::BufError.
+          gzip.flush if @sync && len > 0
         }
       ensure
         gzip.close
@@ -108,7 +113,7 @@ module Rack
       # Skip compressing empty entity body responses and responses with
       # no-transform set.
       if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
-          headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
+          /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
          (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
         return false
       end
diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb
index f0acc40d..b08f5949 100644
--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -3,6 +3,7 @@
 require 'time'
 require 'rack/utils'
 require 'rack/mime'
+require 'rack/files'
 
 module Rack
   # Rack::Directory serves entries below the +root+ given, according to the
@@ -10,7 +11,7 @@ module Rack
   # will be presented in an html based index. If a file is found, the env will
   # be passed to the specified +app+.
   #
-  # If +app+ is not specified, a Rack::File of the same +root+ will be used.
+  # If +app+ is not specified, a Rack::Files of the same +root+ will be used.
 
   class Directory
     DIR_FILE = "<tr><td class='name'><a href='%s'>%s</a></td><td class='size'>%s</td><td class='type'>%s</td><td class='mtime'>%s</td></tr>"
@@ -60,7 +61,7 @@ table { width:100%%; }
 
     def initialize(root, app = nil)
       @root = ::File.expand_path(root)
-      @app = app || Rack::File.new(@root)
+      @app = app || Rack::Files.new(@root)
       @head = Rack::Head.new(lambda { |env| get env })
     end
 
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 425c1d38..85d5be72 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -1,178 +1,8 @@
 # frozen_string_literal: true
 
-require 'time'
-require 'rack/utils'
-require 'rack/mime'
-require 'rack/request'
-require 'rack/head'
+require 'rack/files'
 
 module Rack
-  # Rack::File serves files below the +root+ directory given, according to the
-  # path info of the Rack request.
-  # e.g. when Rack::File.new("/etc") is used, you can access 'passwd' file
-  # as http://localhost:9292/passwd
-  #
-  # Handlers can detect if bodies are a Rack::File, and use mechanisms
-  # like sendfile on the +path+.
-
-  class File
-    ALLOWED_VERBS = %w[GET HEAD OPTIONS]
-    ALLOW_HEADER = ALLOWED_VERBS.join(', ')
-
-    attr_reader :root
-
-    def initialize(root, headers = {}, default_mime = 'text/plain')
-      @root = ::File.expand_path 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
-      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
-        ::File.file?(path) && ::File.readable?(path)
-      rescue SystemCallError
-        false
-      end
-
-      if available
-        serving(request, path)
-      else
-        fail(404, "File not found: #{path_info}")
-      end
-    end
-
-    def serving(request, path)
-      if request.options?
-        return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
-      end
-      last_modified = ::File.mtime(path).httpdate
-      return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
-
-      headers = { "Last-Modified" => last_modified }
-      mime_type = mime_type path, @default_mime
-      headers[CONTENT_TYPE] = mime_type if mime_type
-
-      # Set custom headers
-      @headers.each { |field, content| headers[field] = content } if @headers
-
-      response = [ 200, headers ]
-
-      size = filesize path
-
-      range = nil
-      ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
-      if ranges.nil? || ranges.length > 1
-        # No ranges, or multiple ranges (which we don't support):
-        # TODO: Support multiple byte-ranges
-        response[0] = 200
-        range = 0..size - 1
-      elsif ranges.empty?
-        # Unsatisfiable. Return error, and file size:
-        response = fail(416, "Byte range unsatisfiable")
-        response[1]["Content-Range"] = "bytes */#{size}"
-        return response
-      else
-        # Partial content:
-        range = ranges[0]
-        response[0] = 206
-        response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
-        size = range.end - range.begin + 1
-      end
-
-      response[2] = [response_body] unless response_body.nil?
-
-      response[1][CONTENT_LENGTH] = size.to_s
-      response[2] = make_body request, path, range
-      response
-    end
-
-    class Iterator
-      attr_reader :path, :range
-      alias :to_path :path
-
-      def initialize path, range
-        @path  = path
-        @range = range
-      end
-
-      def each
-        ::File.open(path, "rb") do |file|
-          file.seek(range.begin)
-          remaining_len = range.end - range.begin + 1
-          while remaining_len > 0
-            part = file.read([8192, remaining_len].min)
-            break unless part
-            remaining_len -= part.length
-
-            yield part
-          end
-        end
-      end
-
-      def close; end
-    end
-
-    private
-
-    def make_body request, path, range
-      if request.head?
-        []
-      else
-        Iterator.new path, range
-      end
-    end
-
-    def fail(status, body, headers = {})
-      body += "\n"
-
-      [
-        status,
-        {
-          CONTENT_TYPE   => "text/plain",
-          CONTENT_LENGTH => body.size.to_s,
-          "X-Cascade" => "pass"
-        }.merge!(headers),
-        [body]
-      ]
-    end
-
-    # The MIME type for the contents of the file located at @path
-    def mime_type path, default_mime
-      Mime.mime_type(::File.extname(path), default_mime)
-    end
-
-    def filesize path
-      # If response_body is present, use its size.
-      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
-      #   figure it out by reading the whole file into memory.
-      ::File.size?(path) || ::File.read(path).bytesize
-    end
-
-    # By default, the response body for file requests is nil.
-    # In this case, the response body will be generated later
-    # from the file at @path
-    def response_body
-      nil
-    end
-  end
+  warn "Rack::File is deprecated, please use Rack::Files instead."
+  File = Files
 end
diff --git a/lib/rack/files.rb b/lib/rack/files.rb
new file mode 100644
index 00000000..61658e5c
--- /dev/null
+++ b/lib/rack/files.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+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
+  # path info of the Rack request.
+  # e.g. when Rack::Files.new("/etc") is used, you can access 'passwd' file
+  # as http://localhost:9292/passwd
+  #
+  # Handlers can detect if bodies are a Rack::File, and use mechanisms
+  # like sendfile on the +path+.
+
+  class Files
+    ALLOWED_VERBS = %w[GET HEAD OPTIONS]
+    ALLOW_HEADER = ALLOWED_VERBS.join(', ')
+
+    attr_reader :root
+
+    def initialize(root, headers = {}, default_mime = 'text/plain')
+      @root = ::File.expand_path 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
+      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
+        ::File.file?(path) && ::File.readable?(path)
+      rescue SystemCallError
+        false
+      end
+
+      if available
+        serving(request, path)
+      else
+        fail(404, "File not found: #{path_info}")
+      end
+    end
+
+    def serving(request, path)
+      if request.options?
+        return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
+      end
+      last_modified = ::File.mtime(path).httpdate
+      return [304, {}, []] if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
+
+      headers = { "Last-Modified" => last_modified }
+      mime_type = mime_type path, @default_mime
+      headers[CONTENT_TYPE] = mime_type if mime_type
+
+      # Set custom headers
+      @headers.each { |field, content| headers[field] = content } if @headers
+
+      response = [ 200, headers ]
+
+      size = filesize path
+
+      range = nil
+      ranges = Rack::Utils.get_byte_ranges(request.get_header('HTTP_RANGE'), size)
+      if ranges.nil? || ranges.length > 1
+        # No ranges, or multiple ranges (which we don't support):
+        # TODO: Support multiple byte-ranges
+        response[0] = 200
+        range = 0..size - 1
+      elsif ranges.empty?
+        # Unsatisfiable. Return error, and file size:
+        response = fail(416, "Byte range unsatisfiable")
+        response[1]["Content-Range"] = "bytes */#{size}"
+        return response
+      else
+        # Partial content:
+        range = ranges[0]
+        response[0] = 206
+        response[1]["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
+        size = range.end - range.begin + 1
+      end
+
+      response[2] = [response_body] unless response_body.nil?
+
+      response[1][CONTENT_LENGTH] = size.to_s
+      response[2] = make_body request, path, range
+      response
+    end
+
+    class Iterator
+      attr_reader :path, :range
+      alias :to_path :path
+
+      def initialize path, range
+        @path  = path
+        @range = range
+      end
+
+      def each
+        ::File.open(path, "rb") do |file|
+          file.seek(range.begin)
+          remaining_len = range.end - range.begin + 1
+          while remaining_len > 0
+            part = file.read([8192, remaining_len].min)
+            break unless part
+            remaining_len -= part.length
+
+            yield part
+          end
+        end
+      end
+
+      def close; end
+    end
+
+    private
+
+    def make_body request, path, range
+      if request.head?
+        []
+      else
+        Iterator.new path, range
+      end
+    end
+
+    def fail(status, body, headers = {})
+      body += "\n"
+
+      [
+        status,
+        {
+          CONTENT_TYPE   => "text/plain",
+          CONTENT_LENGTH => body.size.to_s,
+          "X-Cascade" => "pass"
+        }.merge!(headers),
+        [body]
+      ]
+    end
+
+    # The MIME type for the contents of the file located at @path
+    def mime_type path, default_mime
+      Mime.mime_type(::File.extname(path), default_mime)
+    end
+
+    def filesize path
+      # If response_body is present, use its size.
+      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
+      #   figure it out by reading the whole file into memory.
+      ::File.size?(path) || ::File.read(path).bytesize
+    end
+
+    # By default, the response body for file requests is nil.
+    # In this case, the response body will be generated later
+    # from the file at @path
+    def response_body
+      nil
+    end
+  end
+end
diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb
index bc0a3bf8..df17b238 100644
--- a/lib/rack/handler.rb
+++ b/lib/rack/handler.rb
@@ -19,7 +19,7 @@ module Rack
       end
 
       if klass = @handlers[server]
-        klass.split("::").inject(Object) { |o, x| o.const_get(x) }
+        const_get(klass)
       else
         const_get(server, false)
       end
@@ -45,6 +45,9 @@ module Rack
       raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
     end
 
+    SERVER_NAMES = %w(puma thin falcon webrick).freeze
+    private_constant :SERVER_NAMES
+
     def self.default
       # Guess.
       if ENV.include?("PHP_FCGI_CHILDREN")
@@ -54,7 +57,7 @@ module Rack
       elsif ENV.include?("RACK_HANDLER")
         self.get(ENV["RACK_HANDLER"])
       else
-        pick ['puma', 'thin', 'falcon', 'webrick']
+        pick SERVER_NAMES
       end
     end
 
diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb
index 9eec0c8f..41937c99 100644
--- a/lib/rack/media_type.rb
+++ b/lib/rack/media_type.rb
@@ -15,7 +15,7 @@ module Rack
       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
       def type(content_type)
         return nil unless content_type
-        content_type.split(SPLIT_PATTERN, 2).first.downcase
+        content_type.split(SPLIT_PATTERN, 2).first.tap &:downcase!
       end
 
       # The media type parameters provided in CONTENT_TYPE as a Hash, or
@@ -25,15 +25,18 @@ module Rack
       #   { 'charset' => 'utf-8' }
       def params(content_type)
         return {} if content_type.nil?
-        Hash[*content_type.split(SPLIT_PATTERN)[1..-1].
-          collect { |s| s.split('=', 2) }.
-          map { |k, v| [k.downcase, strip_doublequotes(v)] }.flatten]
+
+        content_type.split(SPLIT_PATTERN)[1..-1].each_with_object({}) do |s, hsh|
+          k, v = s.split('=', 2)
+
+          hsh[k.tap(&:downcase!)] = strip_doublequotes(v)
+        end
       end
 
       private
 
         def strip_doublequotes(str)
-          (str[0] == ?" && str[-1] == ?") ? str[1..-2] : str
+          (str.start_with?('"') && str.end_with?('"')) ? str[1..-2] : str
         end
     end
   end
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 1bce2809..7c38d5f3 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -2,12 +2,15 @@
 
 require 'rack/utils'
 require 'strscan'
+require 'rack/core_ext/regexp'
 
 module Rack
   module Multipart
     class MultipartPartLimitError < Errno::EMFILE; end
 
     class Parser
+      using ::Rack::RegexpExtensions
+
       BUFSIZE = 1_048_576
       TEXT_PLAIN = "text/plain"
       TEMPFILE_FACTORY = lambda { |filename, content_type|
@@ -277,6 +280,7 @@ module Rack
             delta = @sbuf.rest_size - @rx_max_size
             @collector.on_mime_body @mime_index, @sbuf.peek(delta)
             @sbuf.pos += delta
+            @sbuf.string = @sbuf.rest
           end
           :want_read
         end
@@ -311,7 +315,7 @@ module Rack
 
         return unless filename
 
-        if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
+        if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
           filename = Utils.unescape_path(filename)
         end
 
@@ -348,7 +352,7 @@ module Rack
               k, v = param.split('=', 2)
               k.strip!
               v.strip!
-              v = v[1..-2] if v[0] == '"' && v[-1] == '"'
+              v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
               encoding = Encoding.find v if k == CHARSET
             end
           end
diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb
index 6f69e0ed..2a4eb244 100644
--- a/lib/rack/query_parser.rb
+++ b/lib/rack/query_parser.rb
@@ -1,7 +1,11 @@
 # frozen_string_literal: true
 
+require_relative 'core_ext/regexp'
+
 module Rack
   class QueryParser
+    using ::Rack::RegexpExtensions
+
     DEFAULT_SEP = /[&;] */n
     COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
 
@@ -51,7 +55,7 @@ module Rack
         end
       end
 
-      return params.to_params_hash
+      return params.to_h
     end
 
     # parse_nested_query expands a query string into structural types. Supported
@@ -69,7 +73,7 @@ module Rack
         normalize_params(params, k, v, param_depth_limit)
       end
 
-      return params.to_params_hash
+      return params.to_h
     rescue ArgumentError => e
       raise InvalidParameterError, e.message
     end
@@ -137,7 +141,7 @@ module Rack
     end
 
     def params_hash_has_key?(hash, key)
-      return false if key =~ /\[\]/
+      return false if /\[\]/.match?(key)
 
       key.split(/[\[\]]+/).inject(hash) do |h, part|
         next h if part == ''
@@ -173,22 +177,42 @@ module Rack
         @params.key?(key)
       end
 
-      def to_params_hash
-        hash = @params
-        hash.keys.each do |key|
-          value = hash[key]
-          if value.kind_of?(self.class)
-            if value.object_id == self.object_id
-              hash[key] = hash
-            else
-              hash[key] = value.to_params_hash
-            end
-          elsif value.kind_of?(Array)
-            value.map! {|x| x.kind_of?(self.class) ? x.to_params_hash : x}
+      # Recursively unwraps nested `Params` objects and constructs an object
+      # of the same shape, but using the objects' internal representations
+      # (Ruby hashes) in place of the objects. The result is a hash consisting
+      # purely of Ruby primitives.
+      #
+      #   Mutation warning!
+      #
+      #   1. This method mutates the internal representation of the `Params`
+      #      objects in order to save object allocations.
+      #
+      #   2. The value you get back is a reference to the internal hash
+      #      representation, not a copy.
+      #
+      #   3. Because the `Params` object's internal representation is mutable
+      #      through the `#[]=` method, it is not thread safe. The result of
+      #      getting the hash representation while another thread is adding a
+      #      key to it is non-deterministic.
+      #
+      def to_h
+        @params.each do |key, value|
+          case value
+          when self
+            # Handle circular references gracefully.
+            @params[key] = @params
+          when Params
+            @params[key] = value.to_h
+          when Array
+            value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
+          else
+            # Ignore anything that is not a `Params` object or
+            # a collection that can contain one.
           end
         end
-        hash
+        @params
       end
+      alias_method :to_params_hash, :to_h
     end
   end
 end
diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb
index c97d8635..e23ed1fb 100644
--- a/lib/rack/reloader.rb
+++ b/lib/rack/reloader.rb
@@ -6,6 +6,8 @@
 
 require 'pathname'
 
+require_relative 'core_ext/regexp'
+
 module Rack
 
   # High performant source reloader
@@ -22,6 +24,8 @@ module Rack
   # It is performing a check/reload cycle at the start of every request, but
   # also respects a cool down time, during which nothing will be done.
   class Reloader
+    using ::Rack::RegexpExtensions
+
     def initialize(app, cooldown = 10, backend = Stat)
       @app = app
       @cooldown = cooldown
@@ -71,7 +75,7 @@ module Rack
         paths = ['./', *$LOAD_PATH].uniq
 
         files.map{|file|
-          next if file =~ /\.(so|bundle)$/ # cannot reload compiled files
+          next if /\.(so|bundle)$/.match?(file) # cannot reload compiled files
 
           found, stat = figure_path(file, paths)
           next unless found && stat && mtime = stat.mtime
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index ce5bc0cd..54ea86c4 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -3,6 +3,8 @@
 require 'rack/utils'
 require 'rack/media_type'
 
+require_relative 'core_ext/regexp'
+
 module Rack
   # Rack::Request provides a convenient interface to a Rack
   # environment.  It is stateless, the environment +env+ passed to the
@@ -13,11 +15,13 @@ module Rack
   #   req.params["data"]
 
   class Request
+    using ::Rack::RegexpExtensions
+
     class << self
       attr_accessor :ip_filter
     end
 
-    self.ip_filter = lambda { |ip| 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 }
+    self.ip_filter = lambda { |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.match?(ip) }
     ALLOWED_SCHEMES = %w(https http).freeze
     SCHEME_WHITELIST = ALLOWED_SCHEMES
     if Object.respond_to?(:deprecate_constant)
@@ -219,7 +223,7 @@ module Rack
         string = get_header HTTP_COOKIE
 
         return hash if string == get_header(RACK_REQUEST_COOKIE_STRING)
-        hash.replace Utils.parse_cookies_header get_header HTTP_COOKIE
+        hash.replace Utils.parse_cookies_header string
         set_header(RACK_REQUEST_COOKIE_STRING, string)
         hash
       end
@@ -243,18 +247,23 @@ module Rack
 
       def host
         # Remove port number.
-        host_with_port.to_s.sub(/:\d+\z/, '')
+        h = host_with_port
+        if colon_index = h.index(":")
+          h[0, colon_index]
+        else
+          h
+        end
       end
 
       def port
-        if port = host_with_port.split(/:/)[1]
+        if port = extract_port(host_with_port)
           port.to_i
         elsif port = get_header(HTTP_X_FORWARDED_PORT)
           port.to_i
         elsif has_header?(HTTP_X_FORWARDED_HOST)
           DEFAULT_PORTS[scheme]
         elsif has_header?(HTTP_X_FORWARDED_PROTO)
-          DEFAULT_PORTS[get_header(HTTP_X_FORWARDED_PROTO).split(',')[0]]
+          DEFAULT_PORTS[extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO))]
         else
           get_header(SERVER_PORT).to_i
         end
@@ -349,7 +358,7 @@ module Rack
 
             # Fix for Safari Ajax postings that always append \0
             # form_vars.sub!(/\0\z/, '') # performance replacement:
-            form_vars.slice!(-1) if form_vars[-1] == ?\0
+            form_vars.slice!(-1) if form_vars.end_with?("\0")
 
             set_header RACK_REQUEST_FORM_VARS, form_vars
             set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
@@ -398,7 +407,8 @@ module Rack
       #
       # <tt>env['rack.input']</tt> is not touched.
       def delete_param(k)
-        [ self.POST.delete(k), self.GET.delete(k) ].compact.first
+        post_value, get_value = self.POST.delete(k), self.GET.delete(k)
+        post_value || get_value
       end
 
       def base_url
@@ -491,11 +501,18 @@ module Rack
       def strip_port(ip_address)
         # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
         # returns: "2001:db8:cafe::17"
-        return ip_address.gsub(/(^\[|\]:\d+$)/, '') if ip_address.include?('[')
+        sep_start = ip_address.index('[')
+        sep_end = ip_address.index(']')
+        if (sep_start && sep_end)
+          return ip_address[sep_start + 1, sep_end - 1]
+        end
 
         # IPv4 format with optional port: "192.0.2.43:47011"
         # returns: "192.0.2.43"
-        return ip_address.gsub(/:\d+$/, '') if ip_address.count(':') == 1
+        sep = ip_address.index(':')
+        if (sep && ip_address.count(':') == 1)
+          return ip_address[0, sep]
+        end
 
         ip_address
       end
@@ -505,16 +522,28 @@ module Rack
       end
 
       def forwarded_scheme
-        scheme_headers = [
-          get_header(HTTP_X_FORWARDED_SCHEME),
-          get_header(HTTP_X_FORWARDED_PROTO).to_s.split(',')[0]
-        ]
+        allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
+        allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
+      end
+
+      def allowed_scheme(header)
+        header if ALLOWED_SCHEMES.include?(header)
+      end
 
-        scheme_headers.each do |header|
-          return header if ALLOWED_SCHEMES.include?(header)
+      def extract_proto_header(header)
+        if header
+          if (comma_index = header.index(','))
+            header[0, comma_index]
+          else
+            header
+          end
         end
+      end
 
-        nil
+      def extract_port(uri)
+        if (colon_index = uri.index(':'))
+          uri[colon_index + 1, uri.length]
+        end
       end
     end
 
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index db856f8a..58f9e5d6 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -3,7 +3,6 @@
 require 'rack/request'
 require 'rack/utils'
 require 'rack/body_proxy'
-require 'rack/simple_body_proxy'
 require 'rack/media_type'
 require 'time'
 
@@ -34,7 +33,7 @@ module Rack
 
     def initialize(body = [], status = 200, header = {})
       @status = status.to_i
-      @header = Utils::HeaderHash.new.merge(header)
+      @header = Utils::HeaderHash.new(header)
 
       @writer  = lambda { |x| @body << x }
       @block   = nil
@@ -73,11 +72,7 @@ module Rack
         close
         [status.to_i, header, []]
       else
-        if @block.nil?
-          [status.to_i, header, SimpleBodyProxy.new(@body)]
-        else
-          [status.to_i, header, BodyProxy.new(self){}]
-        end
+        [status.to_i, header, BodyProxy.new(self){}]
       end
     end
     alias to_a finish           # For *response
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index 6113f858..3774b260 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'rack/file'
+require 'rack/files'
 require 'rack/body_proxy'
 
 module Rack
@@ -16,7 +16,7 @@ module Rack
   #
   # In order to take advantage of this middleware, the response body must
   # respond to +to_path+ and the request must include an X-Sendfile-Type
-  # header. Rack::File and other components implement +to_path+ so there's
+  # header. Rack::Files and other components implement +to_path+ so there's
   # rarely anything you need to do in your application. The X-Sendfile-Type
   # header is typically set in your web servers configuration. The following
   # sections attempt to document
@@ -117,7 +117,8 @@ module Rack
           path = ::File.expand_path(body.to_path)
           if url = map_accel_path(env, path)
             headers[CONTENT_LENGTH] = '0'
-            headers[type] = url
+            # '?' must be percent-encoded because it is not query string but a part of path
+            headers[type] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
             obody = body
             body = Rack::BodyProxy.new([]) do
               obody.close if obody.respond_to?(:close)
diff --git a/lib/rack/server.rb b/lib/rack/server.rb
index 2a3095b3..f0bc1500 100644
--- a/lib/rack/server.rb
+++ b/lib/rack/server.rb
@@ -3,10 +3,12 @@
 require 'optparse'
 require 'fileutils'
 
+require_relative 'core_ext/regexp'
 
 module Rack
 
   class Server
+    using ::Rack::RegexpExtensions
 
     class Options
       def parse!(args)
@@ -134,7 +136,7 @@ module Rack
 
             has_options = false
             server.valid_options.each do |name, description|
-              next if name.to_s.match(/^(Host|Port)[^a-zA-Z]/) # ignore handler's host and port options, we do our own.
+              next if /^(Host|Port)[^a-zA-Z]/.match?(name.to_s) # ignore handler's host and port options, we do our own.
               info << "  -O %-21s %s" % [name, description]
               has_options = true
             end
@@ -252,7 +254,7 @@ module Rack
     class << self
       def logging_middleware
         lambda { |server|
-          server.server.name =~ /CGI/ || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr]
+          /CGI/.match?(server.server.name) || server.options[:quiet] ? nil : [Rack::CommonLogger, $stderr]
         }
       end
 
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
index c9f9f458..b15ee3b8 100644
--- a/lib/rack/session/abstract/id.rb
+++ b/lib/rack/session/abstract/id.rb
@@ -17,6 +17,26 @@ module Rack
       # SessionHash is responsible to lazily load the session from store.
 
       class SessionHash
+        using Module.new {
+          refine Hash do
+            def transform_keys(&block)
+              hash = {}
+              each do |key, value|
+                hash[block.call(key)] = value
+              end
+              hash
+            end
+          end
+        } unless {}.respond_to?(:transform_keys)
+
+        def transform_keys(&block)
+          hash = dup
+          each do |key, value|
+            hash[block.call(key)] = value
+          end
+          hash
+        end
+
         include Enumerable
         attr_writer :id
 
@@ -162,11 +182,7 @@ module Rack
         end
 
         def stringify_keys(other)
-          hash = {}
-          other.each do |key, value|
-            hash[key.to_s] = value
-          end
-          hash
+          other.transform_keys(&:to_s)
         end
       end
 
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index 618b1a0f..70ddadd6 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -6,6 +6,7 @@ require 'rack/request'
 require 'rack/response'
 require 'rack/session/abstract/id'
 require 'json'
+require 'base64'
 
 module Rack
 
@@ -51,11 +52,11 @@ module Rack
       # Encode session cookies as Base64
       class Base64
         def encode(str)
-          [str].pack('m')
+          ::Base64.encode64(str)
         end
 
         def decode(str)
-          str.unpack('m').first
+          ::Base64.decode64(str)
         end
 
         # Encode session cookies as Marshaled Base64 data
@@ -139,9 +140,7 @@ module Rack
           session_data = request.cookies[@key]
 
           if @secrets.size > 0 && session_data
-            digest, session_data = session_data.reverse.split("--", 2)
-            digest.reverse! if digest
-            session_data.reverse! if session_data
+            session_data, _, digest = session_data.rpartition('--')
             session_data = nil unless digest_match?(session_data, digest)
           end
 
diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb
index a05ea0fb..dd587633 100644
--- a/lib/rack/session/memcache.rb
+++ b/lib/rack/session/memcache.rb
@@ -4,6 +4,7 @@
 
 require 'rack/session/abstract/id'
 require 'memcache'
+require 'rack/core_ext/regexp'
 
 module Rack
   module Session
@@ -22,6 +23,8 @@ module Rack
     # a full description of behaviour, please see memcache's documentation.
 
     class Memcache < Abstract::ID
+      using ::Rack::RegexpExtensions
+
       attr_reader :mutex, :pool
 
       DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
@@ -52,7 +55,7 @@ module Rack
         with_lock(env) do
           unless sid and session = @pool.get(sid)
             sid, session = generate_sid, {}
-            unless /^STORED/ =~ @pool.add(sid, session)
+            unless /^STORED/.match?(@pool.add(sid, session))
               raise "Session collision on '#{sid.inspect}'"
             end
           end
diff --git a/lib/rack/simple_body_proxy.rb b/lib/rack/simple_body_proxy.rb
deleted file mode 100644
index fe007c4c..00000000
--- a/lib/rack/simple_body_proxy.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  class SimpleBodyProxy
-    def initialize(body)
-      @body = body
-    end
-
-    def each(&blk)
-      @body.each(&blk)
-    end
-  end
-end
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 766ba0bf..9a0017db 100644
--- a/lib/rack/static.rb
+++ b/lib/rack/static.rb
@@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
-require "rack/file"
+require "rack/files"
 require "rack/utils"
 
+require_relative 'core_ext/regexp'
+
 module Rack
 
   # The Rack::Static middleware intercepts requests for static files
   # (javascript files, images, stylesheets, etc) based on the url prefixes or
-  # route mappings passed in the options, and serves them using a Rack::File
+  # route mappings passed in the options, and serves them using a Rack::Files
   # object. This allows a Rack stack to serve both static and dynamic content.
   #
   # Examples:
@@ -84,6 +86,7 @@ module Rack
   #         ]
   #
   class Static
+    using ::Rack::RegexpExtensions
 
     def initialize(app, options = {})
       @app = app
@@ -97,11 +100,11 @@ module Rack
       # Allow for legacy :cache_control option while prioritizing global header_rules setting
       @header_rules.unshift([:all, { CACHE_CONTROL => options[:cache_control] }]) if options[:cache_control]
 
-      @file_server = Rack::File.new(root)
+      @file_server = Rack::Files.new(root)
     end
 
     def add_index_root?(path)
-      @index && route_file(path) && path =~ /\/$/
+      @index && route_file(path) && path.end_with?('/')
     end
 
     def overwrite_file_path(path)
@@ -122,7 +125,7 @@ module Rack
       if can_serve(path)
         if overwrite_file_path(path)
           env[PATH_INFO] = (add_index_root?(path) ? path + @index : @urls[path])
-        elsif @gzip && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
+        elsif @gzip && env['HTTP_ACCEPT_ENCODING'] && /\bgzip\b/.match?(env['HTTP_ACCEPT_ENCODING'])
           path = env[PATH_INFO]
           env[PATH_INFO] += '.gz'
           response = @file_server.call(env)
@@ -159,14 +162,14 @@ module Rack
         when :all
           true
         when :fonts
-          path =~ /\.(?:ttf|otf|eot|woff2|woff|svg)\z/
+          /\.(?:ttf|otf|eot|woff2|woff|svg)\z/.match?(path)
         when String
           path = ::Rack::Utils.unescape(path)
           path.start_with?(rule) || path.start_with?('/' + rule)
         when Array
-          path =~ /\.(#{rule.join('|')})\z/
+          /\.(#{rule.join('|')})\z/.match?(path)
         when Regexp
-          path =~ rule
+          rule.match?(path)
         else
           false
         end
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 6772089e..38d37aae 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -8,11 +8,15 @@ require 'tempfile'
 require 'rack/query_parser'
 require 'time'
 
+require_relative 'core_ext/regexp'
+
 module Rack
   # Rack::Utils contains a grab-bag of useful methods for writing web
   # applications adopted from all kinds of Ruby libraries.
 
   module Utils
+    using ::Rack::RegexpExtensions
+
     ParameterTypeError = QueryParser::ParameterTypeError
     InvalidParameterError = QueryParser::InvalidParameterError
     DEFAULT_SEP = QueryParser::DEFAULT_SEP
@@ -120,7 +124,7 @@ module Rack
       when Hash
         value.map { |k, v|
           build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-        }.reject(&:empty?).join('&')
+        }.delete_if(&:empty?).join('&')
       when nil
         prefix
       else
@@ -134,7 +138,7 @@ module Rack
       q_value_header.to_s.split(/\s*,\s*/).map do |part|
         value, parameters = part.split(/\s*;\s*/, 2)
         quality = 1.0
-        if md = /\Aq=([\d.]+)/.match(parameters)
+        if parameters && (md = /\Aq=([\d.]+)/.match(parameters))
           quality = md[1].to_f
         end
         [value, quality]
@@ -177,27 +181,26 @@ module Rack
       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
 
       expanded_accept_encoding =
-        accept_encoding.map { |m, q|
+        accept_encoding.each_with_object([]) do |(m, q), list|
           if m == "*"
-            (available_encodings - accept_encoding.map { |m2, _| m2 }).map { |m2| [m2, q] }
+            (available_encodings - accept_encoding.map(&:first))
+              .each { |m2| list << [m2, q] }
           else
-            [[m, q]]
+            list << [m, q]
           end
-        }.inject([]) { |mem, list|
-          mem + list
-        }
+        end
 
-      encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map { |m, _| m }
+      encoding_candidates = expanded_accept_encoding.sort_by { |_, q| -q }.map!(&:first)
 
       unless encoding_candidates.include?("identity")
         encoding_candidates.push("identity")
       end
 
-      expanded_accept_encoding.each { |m, q|
+      expanded_accept_encoding.each do |m, q|
         encoding_candidates.delete(m) if q == 0.0
-      }
+      end
 
-      return (encoding_candidates & available_encodings)[0]
+      (encoding_candidates & available_encodings)[0]
     end
     module_function :select_best_encoding
 
@@ -230,6 +233,8 @@ module Rack
           case value[:same_site]
           when false, nil
             nil
+          when :none, 'None', :None
+            '; SameSite=None'
           when :lax, 'Lax', :Lax
             '; SameSite=Lax'
           when true, :strict, 'Strict', :Strict
@@ -273,15 +278,15 @@ module Rack
         cookies = header
       end
 
-      cookies.reject! { |cookie|
-        if value[:domain]
-          cookie =~ /\A#{escape(key)}=.*domain=#{value[:domain]}/
-        elsif value[:path]
-          cookie =~ /\A#{escape(key)}=.*path=#{value[:path]}/
-        else
-          cookie =~ /\A#{escape(key)}=/
-        end
-      }
+      regexp = if value[:domain]
+                 /\A#{escape(key)}=.*domain=#{value[:domain]}/
+               elsif value[:path]
+                 /\A#{escape(key)}=.*path=#{value[:path]}/
+               else
+                 /\A#{escape(key)}=/
+               end
+
+      cookies.reject! { |cookie| regexp.match? cookie }
 
       cookies.join("\n")
     end
@@ -410,11 +415,9 @@ module Rack
 
     # A case-insensitive Hash that preserves the original case of a
     # header when set.
-    class HeaderHash < Hash
-      def self.new(hash = {})
-        HeaderHash === hash ? hash : super(hash)
-      end
-
+    #
+    # @api private
+    class HeaderHash < Hash # :nodoc:
       def initialize(hash = {})
         super()
         @names = {}
diff --git a/rack.gemspec b/rack.gemspec
index ba822629..f7b13b17 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -28,8 +28,17 @@ EOF
   s.email           = 'leah@vuxu.org'
   s.homepage        = 'https://rack.github.io/'
   s.required_ruby_version = '>= 2.2.2'
+  s.metadata              = {
+    "bug_tracker_uri"   => "https://github.com/rack/rack/issues",
+    "changelog_uri"     => "https://github.com/rack/rack/blob/master/CHANGELOG.md",
+    "documentation_uri" => "https://rubydoc.info/github/rack/rack",
+    "homepage_uri"      => "https://rack.github.io",
+    "mailing_list_uri"  => "https://groups.google.com/forum/#!forum/rack-devel",
+    "source_code_uri"   => "https://github.com/rack/rack"
+  }
 
   s.add_development_dependency 'minitest', "~> 5.0"
   s.add_development_dependency 'minitest-sprint'
+  s.add_development_dependency 'minitest-global_expectations'
   s.add_development_dependency 'rake'
 end
diff --git a/test/builder/anything.rb b/test/builder/anything.rb
deleted file mode 100644
index d8a65871..00000000
--- a/test/builder/anything.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class Anything
-  def self.call(env)
-    [200, { 'Content-Type' => 'text/plain' }, ['OK']]
-  end
-end
diff --git a/test/helper.rb b/test/helper.rb
index 9a26e6ac..dff89558 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 
 module Rack
   class TestCase < Minitest::Test
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb
index 994a79a7..3e479ace 100644
--- a/test/spec_auth_basic.rb
+++ b/test/spec_auth_basic.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/auth/basic'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb
index d60417eb..cc205aa9 100644
--- a/test/spec_auth_digest.rb
+++ b/test/spec_auth_digest.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/auth/digest/md5'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb
index 73b194d0..6be79f8b 100644
--- a/test/spec_body_proxy.rb
+++ b/test/spec_body_proxy.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/body_proxy'
 require 'stringio'
 
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index b1701735..853fb7b1 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/builder'
 require 'rack/lint'
 require 'rack/mock'
@@ -240,13 +240,6 @@ describe Rack::Builder do
       env.must_equal({})
     end
 
-    it "requires anything not ending in .ru" do
-      $: << File.dirname(__FILE__)
-      app, * = Rack::Builder.parse_file 'builder/anything'
-      Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
-      $:.pop
-    end
-
     it 'requires an_underscore_app not ending in .ru' do
       $: << File.dirname(__FILE__)
       app, * = Rack::Builder.parse_file 'builder/an_underscore_app'
diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb
index 8061a254..abb7b57f 100644
--- a/test/spec_cascade.rb
+++ b/test/spec_cascade.rb
@@ -1,9 +1,9 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/cascade'
-require 'rack/file'
+require 'rack/files'
 require 'rack/lint'
 require 'rack/urlmap'
 require 'rack/mock'
@@ -14,7 +14,7 @@ describe Rack::Cascade do
   end
 
   docroot = File.expand_path(File.dirname(__FILE__))
-  app1 = Rack::File.new(docroot)
+  app1 = Rack::Files.new(docroot)
 
   app2 = Rack::URLMap.new("/crash" => lambda { |env| raise "boom" })
 
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index 26c5c37f..daa36cad 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/chunked'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb
index 0aa2a048..330a6480 100644
--- a/test/spec_common_logger.rb
+++ b/test/spec_common_logger.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/common_logger'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb
index a6a33df1..8402f04e 100644
--- a/test/spec_conditional_get.rb
+++ b/test/spec_conditional_get.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'time'
 require 'rack/conditional_get'
 require 'rack/mock'
diff --git a/test/spec_config.rb b/test/spec_config.rb
index d5f7ceca..d97107b6 100644
--- a/test/spec_config.rb
+++ b/test/spec_config.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/builder'
 require 'rack/config'
 require 'rack/content_length'
diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb
index 8856e7d3..2e7a8581 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/content_length'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb
index a46f95ea..53f1d172 100644
--- a/test/spec_content_type.rb
+++ b/test/spec_content_type.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/content_type'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index b0640a04..75244dcc 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'time'  # for Time#httpdate
 require 'rack/deflater'
@@ -114,6 +114,19 @@ describe Rack::Deflater do
     end
   end
 
+  it 'be able to deflate bodies that respond to each and contain empty chunks' do
+    app_body = Object.new
+    class << app_body; def each; yield('foo'); yield(''); yield('bar'); end; end
+
+    verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body|
+      headers.must_equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+    end
+  end
+
   it 'flush deflated chunks to the client as they become ready' do
     app_body = Object.new
     class << app_body; def each; yield('foo'); yield('bar'); end; end
@@ -388,7 +401,7 @@ describe Rack::Deflater do
     app_body = Object.new
     class << app_body
       def each
-        (0..20).each { |i| yield "hello\n".freeze }
+        (0..20).each { |i| yield "hello\n" }
       end
     end
 
diff --git a/test/spec_directory.rb b/test/spec_directory.rb
index 1187471c..8635ec90 100644
--- a/test/spec_directory.rb
+++ b/test/spec_directory.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/directory'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index 5e13d538..750ceaac 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/etag'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_file.rb b/test/spec_files.rb
index 55b6eaad..ad57972d 100644
--- a/test/spec_file.rb
+++ b/test/spec_files.rb
@@ -1,21 +1,21 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
-require 'rack/file'
+require 'minitest/global_expectations/autorun'
+require 'rack/files'
 require 'rack/lint'
 require 'rack/mock'
 
-describe Rack::File do
+describe Rack::Files do
   DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
 
-  def file(*args)
-    Rack::Lint.new Rack::File.new(*args)
+  def files(*args)
+    Rack::Lint.new Rack::Files.new(*args)
   end
 
   it 'serves files with + in the file name' do
     Dir.mktmpdir do |dir|
       File.write File.join(dir, "you+me.txt"), "hello world"
-      app = file(dir)
+      app = files(dir)
       env = Rack::MockRequest.env_for("/you+me.txt")
       status, _, body = app.call env
 
@@ -28,14 +28,14 @@ describe Rack::File do
   end
 
   it "serve files" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test")
 
     res.must_be :ok?
     assert_match(res, /ruby/)
   end
 
   it "set Last-Modified header" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test")
 
     path = File.join(DOCROOT, "/cgi/test")
 
@@ -45,7 +45,7 @@ describe Rack::File do
 
   it "return 304 if file isn't modified since last serve" do
     path = File.join(DOCROOT, "/cgi/test")
-    res = Rack::MockRequest.new(file(DOCROOT)).
+    res = Rack::MockRequest.new(files(DOCROOT)).
       get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate)
 
     res.status.must_equal 304
@@ -54,14 +54,14 @@ describe Rack::File do
 
   it "return the file if it's modified since last serve" do
     path = File.join(DOCROOT, "/cgi/test")
-    res = Rack::MockRequest.new(file(DOCROOT)).
+    res = Rack::MockRequest.new(files(DOCROOT)).
       get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => (File.mtime(path) - 100).httpdate)
 
     res.must_be :ok?
   end
 
   it "serve files with URL encoded filenames" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test"
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%74%65%73%74") # "/cgi/test"
 
     res.must_be :ok?
     # res.must_match(/ruby/)    # nope
@@ -71,12 +71,12 @@ describe Rack::File do
   end
 
   it "serve uri with URL encoded null byte (%00) in filenames" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/test%00")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test%00")
     res.must_be :bad_request?
   end
 
   it "allow safe directory traversal" do
-    req = Rack::MockRequest.new(file(DOCROOT))
+    req = Rack::MockRequest.new(files(DOCROOT))
 
     res = req.get('/cgi/../cgi/test')
     res.must_be :successful?
@@ -89,7 +89,7 @@ describe Rack::File do
   end
 
   it "not allow unsafe directory traversal" do
-    req = Rack::MockRequest.new(file(DOCROOT))
+    req = Rack::MockRequest.new(files(DOCROOT))
 
     res = req.get("/../README.rdoc")
     res.must_be :client_error?
@@ -104,7 +104,7 @@ describe Rack::File do
   end
 
   it "allow files with .. in their name" do
-    req = Rack::MockRequest.new(file(DOCROOT))
+    req = Rack::MockRequest.new(files(DOCROOT))
     res = req.get("/cgi/..test")
     res.must_be :not_found?
 
@@ -116,33 +116,33 @@ describe Rack::File do
   end
 
   it "not allow unsafe directory traversal with encoded periods" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/%2E%2E/README")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/%2E%2E/README")
 
     res.must_be :client_error?
     res.must_be :not_found?
   end
 
   it "allow safe directory traversal with encoded periods" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/%2E%2E/cgi/test")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/%2E%2E/cgi/test")
 
     res.must_be :successful?
   end
 
   it "404 if it can't find the file" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi/blubb")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/blubb")
 
     res.must_be :not_found?
   end
 
   it "detect SystemCallErrors" do
-    res = Rack::MockRequest.new(file(DOCROOT)).get("/cgi")
+    res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi")
 
     res.must_be :not_found?
   end
 
   it "return bodies that respond to #to_path" do
     env = Rack::MockRequest.env_for("/cgi/test")
-    status, _, body = Rack::File.new(DOCROOT).call(env)
+    status, _, body = Rack::Files.new(DOCROOT).call(env)
 
     path = File.join(DOCROOT, "/cgi/test")
 
@@ -154,7 +154,7 @@ describe Rack::File do
   it "return correct byte range in body" do
     env = Rack::MockRequest.env_for("/cgi/test")
     env["HTTP_RANGE"] = "bytes=22-33"
-    res = Rack::MockResponse.new(*file(DOCROOT).call(env))
+    res = Rack::MockResponse.new(*files(DOCROOT).call(env))
 
     res.status.must_equal 206
     res["Content-Length"].must_equal "12"
@@ -165,7 +165,7 @@ describe Rack::File do
   it "return error for unsatisfiable byte range" do
     env = Rack::MockRequest.env_for("/cgi/test")
     env["HTTP_RANGE"] = "bytes=1234-5678"
-    res = Rack::MockResponse.new(*file(DOCROOT).call(env))
+    res = Rack::MockResponse.new(*files(DOCROOT).call(env))
 
     res.status.must_equal 416
     res["Content-Range"].must_equal "bytes */208"
@@ -173,7 +173,7 @@ describe Rack::File do
 
   it "support custom http headers" do
     env = Rack::MockRequest.env_for("/cgi/test")
-    status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38',
+    status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38',
      'Access-Control-Allow-Origin' => '*').call(env)
 
     status.must_equal 200
@@ -183,7 +183,7 @@ describe Rack::File do
 
   it "support not add custom http headers if none are supplied" do
     env = Rack::MockRequest.env_for("/cgi/test")
-    status, heads, _ = file(DOCROOT).call(env)
+    status, heads, _ = files(DOCROOT).call(env)
 
     status.must_equal 200
     heads['Cache-Control'].must_be_nil
@@ -191,7 +191,7 @@ describe Rack::File do
   end
 
   it "only support GET, HEAD, and OPTIONS requests" do
-    req = Rack::MockRequest.new(file(DOCROOT))
+    req = Rack::MockRequest.new(files(DOCROOT))
 
     forbidden = %w[post put patch delete]
     forbidden.each do |method|
@@ -209,7 +209,7 @@ describe Rack::File do
   end
 
   it "set Allow correctly for OPTIONS requests" do
-    req = Rack::MockRequest.new(file(DOCROOT))
+    req = Rack::MockRequest.new(files(DOCROOT))
     res = req.options('/cgi/test')
     res.must_be :successful?
     res.headers['Allow'].wont_equal nil
@@ -217,35 +217,35 @@ describe Rack::File do
   end
 
   it "set Content-Length correctly for HEAD requests" do
-    req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
+    req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT)))
     res = req.head "/cgi/test"
     res.must_be :successful?
     res['Content-Length'].must_equal "208"
   end
 
   it "default to a mime type of text/plain" do
-    req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT)))
+    req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT)))
     res = req.get "/cgi/test"
     res.must_be :successful?
     res['Content-Type'].must_equal "text/plain"
   end
 
   it "allow the default mime type to be set" do
-    req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, 'application/octet-stream')))
+    req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, 'application/octet-stream')))
     res = req.get "/cgi/test"
     res.must_be :successful?
     res['Content-Type'].must_equal "application/octet-stream"
   end
 
   it "not set Content-Type if the mime type is not set" do
-    req = Rack::MockRequest.new(Rack::Lint.new(Rack::File.new(DOCROOT, nil, nil)))
+    req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT, nil, nil)))
     res = req.get "/cgi/test"
     res.must_be :successful?
     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 = Rack::MockRequest.new(files(DOCROOT)).head("/cgi/missing")
     res.must_be :not_found?
     res.body.must_be :empty?
   end
diff --git a/test/spec_handler.rb b/test/spec_handler.rb
index c38cf4e9..5746dc22 100644
--- a/test/spec_handler.rb
+++ b/test/spec_handler.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/handler'
 
 class Rack::Handler::Lobster; end
diff --git a/test/spec_head.rb b/test/spec_head.rb
index 1cf8b391..f6f41a5d 100644
--- a/test/spec_head.rb
+++ b/test/spec_head.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/head'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index 07f7fe2b..192f260f 100644
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'tempfile'
 require 'rack/lint'
diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb
index d4fcff47..9f3b9a89 100644
--- a/test/spec_lobster.rb
+++ b/test/spec_lobster.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/lobster'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_lock.rb b/test/spec_lock.rb
index 25d71fc6..cd9e1230 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/lock'
 require 'rack/mock'
diff --git a/test/spec_logger.rb b/test/spec_logger.rb
index d6876c5f..f453b14d 100644
--- a/test/spec_logger.rb
+++ b/test/spec_logger.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/lint'
 require 'rack/logger'
diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb
index 580d24ce..7d52b4d4 100644
--- a/test/spec_media_type.rb
+++ b/test/spec_media_type.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/media_type'
 
 describe Rack::MediaType do
diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb
index 00990f9b..6b01f7c9 100644
--- a/test/spec_method_override.rb
+++ b/test/spec_method_override.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/method_override'
 require 'rack/mock'
diff --git a/test/spec_mime.rb b/test/spec_mime.rb
index b9258eb6..8d1ca256 100644
--- a/test/spec_mime.rb
+++ b/test/spec_mime.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/mime'
 
 describe Rack::Mime do
diff --git a/test/spec_mock.rb b/test/spec_mock.rb
index 4c5e1b7c..d7246d3f 100644
--- a/test/spec_mock.rb
+++ b/test/spec_mock.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'yaml'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 67888dc0..b029048e 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/multipart'
 require 'rack/multipart/parser'
diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb
index f15d47a9..1037c9fa 100644
--- a/test/spec_null_logger.rb
+++ b/test/spec_null_logger.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/mock'
 require 'rack/null_logger'
diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb
index be1e97e6..e77d966d 100644
--- a/test/spec_recursive.rb
+++ b/test/spec_recursive.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/recursive'
 require 'rack/mock'
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 5c7a9639..583a367e 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'cgi'
 require 'rack/request'
@@ -1321,26 +1321,26 @@ EOF
 
   it "regards local addresses as proxies" do
     req = make_request(Rack::MockRequest.env_for("/"))
-    req.trusted_proxy?('127.0.0.1').must_equal 0
-    req.trusted_proxy?('10.0.0.1').must_equal 0
-    req.trusted_proxy?('172.16.0.1').must_equal 0
-    req.trusted_proxy?('172.20.0.1').must_equal 0
-    req.trusted_proxy?('172.30.0.1').must_equal 0
-    req.trusted_proxy?('172.31.0.1').must_equal 0
-    req.trusted_proxy?('192.168.0.1').must_equal 0
-    req.trusted_proxy?('::1').must_equal 0
-    req.trusted_proxy?('fd00::').must_equal 0
-    req.trusted_proxy?('localhost').must_equal 0
-    req.trusted_proxy?('unix').must_equal 0
-    req.trusted_proxy?('unix:/tmp/sock').must_equal 0
-
-    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
+    req.trusted_proxy?('127.0.0.1').must_equal true
+    req.trusted_proxy?('10.0.0.1').must_equal true
+    req.trusted_proxy?('172.16.0.1').must_equal true
+    req.trusted_proxy?('172.20.0.1').must_equal true
+    req.trusted_proxy?('172.30.0.1').must_equal true
+    req.trusted_proxy?('172.31.0.1').must_equal true
+    req.trusted_proxy?('192.168.0.1').must_equal true
+    req.trusted_proxy?('::1').must_equal true
+    req.trusted_proxy?('fd00::').must_equal true
+    req.trusted_proxy?('localhost').must_equal true
+    req.trusted_proxy?('unix').must_equal true
+    req.trusted_proxy?('unix:/tmp/sock').must_equal true
+
+    req.trusted_proxy?("unix.example.org").must_equal false
+    req.trusted_proxy?("example.org\n127.0.0.1").must_equal false
+    req.trusted_proxy?("127.0.0.1\nexample.org").must_equal false
+    req.trusted_proxy?("11.0.0.1").must_equal false
+    req.trusted_proxy?("172.15.0.1").must_equal false
+    req.trusted_proxy?("172.32.0.1").must_equal false
+    req.trusted_proxy?("2001:470:1f0b:18f8::1").must_equal false
   end
 
   it "sets the default session to an empty hash" do
diff --git a/test/spec_response.rb b/test/spec_response.rb
index f4248dd9..3cd56664 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/response'
 require 'stringio'
@@ -62,6 +62,16 @@ describe Rack::Response do
     response["Content-Type"].must_equal "text/plain"
   end
 
+  it "doesn't mutate given headers" do
+    [{}, Rack::Utils::HeaderHash.new].each do |header|
+      response = Rack::Response.new([], 200, header)
+      response.header["Content-Type"] = "text/plain"
+      response.header["Content-Type"].must_equal "text/plain"
+
+      header.wont_include("Content-Type")
+    end
+  end
+
   it "can override the initial Content-Type with a different case" do
     response = Rack::Response.new("", 200, "content-type" => "text/plain")
     response["Content-Type"].must_equal "text/plain"
@@ -117,6 +127,24 @@ describe Rack::Response do
     response["Set-Cookie"].must_equal "foo=bar"
   end
 
+  it "can set SameSite cookies with symbol value :none" do
+    response = Rack::Response.new
+    response.set_cookie "foo", { value: "bar", same_site: :none }
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=None"
+  end
+
+  it "can set SameSite cookies with symbol value :None" do
+    response = Rack::Response.new
+    response.set_cookie "foo", { value: "bar", same_site: :None }
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=None"
+  end
+
+  it "can set SameSite cookies with string value 'None'" do
+    response = Rack::Response.new
+    response.set_cookie "foo", { value: "bar", same_site: "None" }
+    response["Set-Cookie"].must_equal "foo=bar; SameSite=None"
+  end
+
   it "can set SameSite cookies with symbol value :lax" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: :lax }
diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb
index 932e9de3..6bb5f5cf 100644
--- a/test/spec_rewindable_input.rb
+++ b/test/spec_rewindable_input.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/rewindable_input'
 
diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb
index 5a7e84bd..10e561de 100644
--- a/test/spec_runtime.rb
+++ b/test/spec_runtime.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/mock'
 require 'rack/runtime'
diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb
index cae458e4..cbed8db3 100644
--- a/test/spec_sendfile.rb
+++ b/test/spec_sendfile.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'fileutils'
 require 'rack/lint'
 require 'rack/sendfile'
@@ -8,10 +8,10 @@ require 'rack/mock'
 require 'tmpdir'
 
 describe Rack::Sendfile do
-  def sendfile_body
-    FileUtils.touch File.join(Dir.tmpdir,  "rack_sendfile")
+  def sendfile_body(filename = "rack_sendfile")
+    FileUtils.touch File.join(Dir.tmpdir,  filename)
     res = ['Hello World']
-    def res.to_path ; File.join(Dir.tmpdir,  "rack_sendfile") ; end
+    res.define_singleton_method(:to_path) { File.join(Dir.tmpdir,  filename) }
     res
   end
 
@@ -74,6 +74,19 @@ describe Rack::Sendfile do
     end
   end
 
+  it "sets X-Accel-Redirect response header to percent-encoded path" do
+    headers = {
+      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+      'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar%/"
+    }
+    request headers, sendfile_body('file_with_%_?_symbol') 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%25/file_with_%25_%3F_symbol'
+    end
+  end
+
   it 'writes to rack.error when no X-Accel-Mapping is specified' do
     request 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' do |response|
       response.must_be :ok?
diff --git a/test/spec_server.rb b/test/spec_server.rb
index 7a60a61e..b09caf03 100644
--- a/test/spec_server.rb
+++ b/test/spec_server.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/server'
 require 'tempfile'
@@ -161,6 +161,8 @@ describe Rack::Server do
   end
 
   it "check pid file presence and not owned process" do
+    owns_pid_1 = (Process.kill(0, 1) rescue nil) == 1
+    skip "cannot test if pid 1 owner matches current process (eg. docker/lxc)" if owns_pid_1
     pidfile = Tempfile.open('pidfile') { |f| f.write(1); break f }.path
     server = Rack::Server.new(pid: pidfile)
     server.send(:pidfile_process_status).must_equal :not_owned
diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb
index 00140c16..3591a3de 100644
--- a/test/spec_session_abstract_id.rb
+++ b/test/spec_session_abstract_id.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 ### WARNING: there be hax in this file.
 
 require 'rack/session/abstract/id'
diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb
index 37030a8c..5d0d10ce 100644
--- a/test/spec_session_abstract_session_hash.rb
+++ b/test/spec_session_abstract_session_hash.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/session/abstract/id'
 
 describe Rack::Session::Abstract::SessionHash do
@@ -44,4 +44,10 @@ describe Rack::Session::Abstract::SessionHash do
       lambda { hash.fetch(:unknown) }.must_raise KeyError
     end
   end
+
+  describe "#stringify_keys" do
+    it "returns hash or session hash with keys stringified" do
+      assert_equal({ "foo" => :bar, "baz" => :qux }, hash.send(:stringify_keys, hash).to_h)
+    end
+  end
 end
diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb
index 8ecfde53..9b4442dd 100644
--- a/test/spec_session_cookie.rb
+++ b/test/spec_session_cookie.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/session/cookie'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb
index da90b340..a015cee6 100644
--- a/test/spec_session_memcache.rb
+++ b/test/spec_session_memcache.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 begin
   require 'rack/session/memcache'
   require 'rack/lint'
diff --git a/test/spec_session_pool.rb b/test/spec_session_pool.rb
index 6eecce36..fda5f56e 100644
--- a/test/spec_session_pool.rb
+++ b/test/spec_session_pool.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'thread'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb
index 9cad32ce..a4ade121 100644
--- a/test/spec_show_exceptions.rb
+++ b/test/spec_show_exceptions.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/show_exceptions'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb
index a4b58e2a..ca23134e 100644
--- a/test/spec_show_status.rb
+++ b/test/spec_show_status.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/show_status'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_static.rb b/test/spec_static.rb
index 7c510bf6..d33e8edc 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/static'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb
index f6b79641..0e7de841 100644
--- a/test/spec_tempfile_reaper.rb
+++ b/test/spec_tempfile_reaper.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/tempfile_reaper'
 require 'rack/lint'
 require 'rack/mock'
diff --git a/test/spec_thin.rb b/test/spec_thin.rb
index cc4967b2..0729c3f3 100644
--- a/test/spec_thin.rb
+++ b/test/spec_thin.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 begin
 require 'rack/handler/thin'
 require File.expand_path('../testrequest', __FILE__)
diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb
index f5d7e115..9ce38298 100644
--- a/test/spec_urlmap.rb
+++ b/test/spec_urlmap.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/urlmap'
 require 'rack/mock'
 
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 427bb455..6210fd73 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/utils'
 require 'rack/mock'
 require 'timeout'
@@ -671,10 +671,10 @@ describe Rack::Utils::HeaderHash do
     h.delete("Hello").must_be_nil
   end
 
-  it "avoid unnecessary object creation if possible" do
+  it "dups given HeaderHash" do
     a = Rack::Utils::HeaderHash.new("foo" => "bar")
     b = Rack::Utils::HeaderHash.new(a)
-    b.object_id.must_equal a.object_id
+    b.object_id.wont_equal a.object_id
     b.must_equal a
   end
 
diff --git a/test/spec_version.rb b/test/spec_version.rb
index 04604ebf..d4191aa4 100644
--- a/test/spec_version.rb
+++ b/test/spec_version.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 
 describe Rack do
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
index cabb2e4a..0d0aa8f7 100644
--- a/test/spec_webrick.rb
+++ b/test/spec_webrick.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack/mock'
 require 'thread'
 require File.expand_path('../testrequest', __FILE__)