diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a89fa6d1737698609c76fc17741a70de68ec88e1
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,107 @@
+workflows:
+  version: 2
+
+  test:
+    jobs:
+      - test-jruby
+      - test-ruby-2.2
+      - test-ruby-2.3
+      - test-ruby-2.4
+      - test-ruby-2.5
+      - test-ruby-2.6
+      - test-ruby-2.7
+
+version: 2
+
+default-steps: &default-steps
+  - checkout
+  - run: sudo apt-get install lighttpd libfcgi-dev libmemcached-dev
+
+  # Restore bundle cache
+  - type: cache-restore
+    key: rack-{{ checksum "rack.gemspec" }}-{{ checksum "Gemfile" }}
+
+  # Bundle install dependencies
+  - run: bundle install --path vendor/bundle
+
+  # Store bundle cache
+  - type: cache-save
+    key: rack-{{ checksum "rack.gemspec" }}-{{ checksum "Gemfile" }}
+    paths:
+      - vendor/bundle
+
+  - run: bundle exec rubocop
+
+  - run: bundle exec rake ci
+
+jobs:
+  test-ruby-2.2:
+    docker:
+      - image: circleci/ruby:2.2
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+
+  test-ruby-2.3:
+    docker:
+      - image: circleci/ruby:2.3
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+
+  test-ruby-2.4:
+    docker:
+      - image: circleci/ruby:2.4
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+
+  test-ruby-2.5:
+    docker:
+      - image: circleci/ruby:2.5
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+
+  test-ruby-2.6:
+    docker:
+      - image: circleci/ruby:2.6
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+  
+  test-ruby-2.7:
+    docker:
+      - image: circleci/ruby:2.7
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps    
+
+  test-jruby:
+    docker:
+      - image: circleci/jruby
+        # Spawn a process owned by root
+        # This works around an issue explained here:
+        # https://github.com/circleci/circleci-images/pull/132
+        command: sudo /bin/sh
+      - image: memcached:1.4
+    steps: *default-steps
+
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..7a7ad3a55d7fca35fce01e6b8280538f2e113710
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+RDOX
+ChangeLog
+*.gem
+lighttpd.errors
+*.rbc
+stage
+*.tar.gz
+Gemfile.lock
+.rbx
+doc
+/.bundle
+/.yardoc
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000000000000000000000000000000000000..22ed992086ac71f876125e8b1bc2a3c6f3b3ed26
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,48 @@
+AllCops:
+  TargetRubyVersion: 2.2
+  DisabledByDefault: true
+  Exclude:
+    - '**/vendor/**/*'
+
+Style/FrozenStringLiteralComment:
+  Enabled: true
+  EnforcedStyle: always
+  Exclude:
+    - 'test/builder/bom.ru'
+
+# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }.
+Style/HashSyntax:
+  Enabled: true
+
+Layout/EmptyLineAfterMagicComment:
+  Enabled: true
+
+Layout/LeadingCommentSpace:
+  Enabled: true
+  Exclude:
+    - 'test/builder/options.ru'
+
+Layout/SpaceAfterColon:
+  Enabled: true
+
+Layout/SpaceAfterComma:
+  Enabled: true
+
+Layout/SpaceAroundEqualsInParameterDefault:
+  Enabled: true
+
+Layout/SpaceAroundKeyword:
+  Enabled: true
+
+Layout/SpaceAroundOperators:
+  Enabled: true
+
+Layout/SpaceBeforeComma:
+  Enabled: true
+
+Layout/SpaceBeforeFirstArg:
+  Enabled: true
+
+# Use `{ a: 1 }` not `{a:1}`.
+Layout/SpaceInsideHashLiteralBraces:
+  Enabled: true
diff --git a/.yardopts b/.yardopts
new file mode 100644
index 0000000000000000000000000000000000000000..f4d6aebae4cc1d7b85159bfcdf95421c287f55bc
--- /dev/null
+++ b/.yardopts
@@ -0,0 +1,2 @@
+-
+SPEC
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..4f0be3424ea7b43a7efd459771e0241eae154a84
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,77 @@
+## [2.1.4] - 2020-06-15
+
+- [CVE-2020-8184] When parsing cookies, only decode the value
+
+## [2.1.3] - 2020-05-12
+
+- [CVE-2020-8161] Use Dir.entries instead of Dir[glob] to prevent user-specified glob metacharacters
+-
+## [2.1.2] - 2020-01-27
+
+- Fix multipart parser for some files to prevent denial of service ([@aiomaster](https://github.com/aiomaster))
+- Fix `Rack::Builder#use` with keyword arguments ([@kamipo](https://github.com/kamipo))
+- Skip deflating in Rack::Deflater if Content-Length is 0 ([@jeremyevans](https://github.com/jeremyevans))
+- Remove `SessionHash#transform_keys`, no longer needed ([@pavel](https://github.com/pavel))
+- Add to_hash to wrap Hash and Session classes ([@oleh-demyanyuk](https://github.com/oleh-demyanyuk))
+- Handle case where session id key is requested but missing ([@jeremyevans](https://github.com/jeremyevans))
+
+## [2.1.1] - 2020-01-12
+
+- Remove `Rack::Chunked` from `Rack::Server` default middleware. ([#1475](https://github.com/rack/rack/pull/1475), [@ioquatix](https://github.com/ioquatix))
+
+## [2.1.0] - 2020-01-10
+
+### Added
+
+- Add support for `SameSite=None` cookie value. ([@hennikul](https://github.com/hennikul))
+- Add trailer headers. ([@eileencodes](https://github.com/eileencodes))
+- Add MIME Types for video streaming. ([@styd](https://github.com/styd))
+- Add MIME Type for WASM. ([@buildrtech](https://github.com/buildrtech))
+- Add `Early Hints(103)` to status codes. ([@egtra](https://github.com/egtra))
+- Add `Too Early(425)` to status codes. ([@y-yagi]((https://github.com/y-yagi)))
+- Add `Bandwidth Limit Exceeded(509)` to status codes. ([@CJKinni](https://github.com/CJKinni))
+- Add method for custom `ip_filter`. ([@svcastaneda](https://github.com/svcastaneda))
+- Add boot-time profiling capabilities to `rackup`. ([@tenderlove](https://github.com/tenderlove))
+- Add multi mapping support for `X-Accel-Mappings` header. ([@yoshuki](https://github.com/yoshuki))
+- Add `sync: false` option to `Rack::Deflater`. (Eric Wong)
+- Add `Builder#freeze_app` to freeze application and all middleware instances. ([@jeremyevans](https://github.com/jeremyevans))
+- Add API to extract cookies from `Rack::MockResponse`. ([@petercline](https://github.com/petercline))
+
+### Changed
+
+- Don't propagate nil values from middleware. ([@ioquatix](https://github.com/ioquatix))
+- Lazily initialize the response body and only buffer it if required. ([@ioquatix](https://github.com/ioquatix))
+- Fix deflater zlib buffer errors on empty body part. ([@felixbuenemann](https://github.com/felixbuenemann))
+- Set `X-Accel-Redirect` to percent-encoded path. ([@diskkid](https://github.com/diskkid))
+- Remove unnecessary buffer growing when parsing multipart. ([@tainoe](https://github.com/tainoe))
+- Expand the root path in `Rack::Static` upon initialization. ([@rosenfeld](https://github.com/rosenfeld))
+- Make `ShowExceptions` work with binary data. ([@axyjo](https://github.com/axyjo))
+- Use buffer string when parsing multipart requests. ([@janko-m](https://github.com/janko-m))
+- Support optional UTF-8 Byte Order Mark (BOM) in config.ru. ([@mikegee](https://github.com/mikegee))
+- Handle `X-Forwarded-For` with optional port. ([@dpritchett](https://github.com/dpritchett))
+- 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`. ([@adambutler](https://github.com/adambutler))
+- Rename `Request::SCHEME_WHITELIST` to `Request::ALLOWED_SCHEMES`.
+- Make `Multipart::Parser.get_filename` accept files with `+` in their name. ([@lucaskanashiro](https://github.com/lucaskanashiro))
+- 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)).
+- Prefer Base64 “strict encoding” for Base64 cookies. ([@ioquatix](https://github.com/ioquatix))
+
+### Removed
+
+- Remove `to_ary` from Response ([@tenderlove](https://github.com/tenderlove))
+- Deprecate `Rack::Session::Memcache` in favor of `Rack::Session::Dalli` from dalli gem ([@fatkodima](https://github.com/fatkodima))
+
+### Fixed
+
+- Eliminate warnings for Ruby 2.7. ([@osamtimizer](https://github.com/osamtimizer]))
+
+### 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))
+- CHANGELOG updates. ([@drenmi](https://github.com/Drenmi), [@p8](https://github.com/p8))
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..62a3494e88155ae9d7f6e617e6d3bed148fa8683
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gemspec
+
+# What we need to do here is just *exclude* JRuby, but bundler has no way to do
+# this, because of some argument that I know I had with Yehuda and Carl years
+# ago, but I've since forgotten. Anyway, we actually need it here, and it's not
+# avaialable, so prepare yourself for a yak shave when this breaks.
+c_platforms = Bundler::Dsl::VALID_PLATFORMS.dup.delete_if do |platform|
+  platform =~ /jruby/
+end
+
+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:
+# c_platforms = [:mri] if Gem.platforms.last.os == "java"
+
+group :extra do
+  gem 'fcgi', platforms: c_platforms
+  gem 'dalli'
+  gem 'thin', platforms: c_platforms
+end
+
+group :doc do
+  gem 'rdoc'
+end
diff --git a/HISTORY.md b/HISTORY.md
deleted file mode 100644
index 406d1758ca840c060476ad2c502e501ba72869ac..0000000000000000000000000000000000000000
--- a/HISTORY.md
+++ /dev/null
@@ -1,505 +0,0 @@
-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
-	`Rack::Session::Abstract::Persisted`.
-	`Rack::Session::Abstract::Persisted` uses a request object rather than
-	the `env` hash.
-
-Fri Sep  4 17:32:12 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Pull `ENV` access inside the request object in to a module.  This
-	will help with legacy Request objects that are ENV based but don't
-	want to inherit from Rack::Request
-
-Fri Sep  4 16:09:11 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Move most methods on the `Rack::Request` to a module
-	`Rack::Request::Helpers` and use public API to get values from the
-	request object.  This enables users to mix `Rack::Request::Helpers` in
-	to their own objects so they can implement
-	`(get|set|fetch|each)_header` as they see fit (for example a proxy
-	object).
-
-Fri Sep  4 14:15:32 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Files and directories with + in the name are served correctly.
-	Rather than unescaping paths like a form, we unescape with a URI
-	parser using `Rack::Utils.unescape_path`. Fixes #265
-
-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
-	HTTP_RANGE string passed to it without depending on the `env` hash.
-	`byte_ranges` is deprecated in favor of this method.
-
-Sat Aug 22 17:49:49 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Change Session internals to use Request objects for looking up
-	session information. This allows us to only allocate one request
-	object when dealing with session objects (rather than doing it every
-	time we need to manipulate cookies, etc).
-
-Fri Aug 21 16:30:51 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Add `Rack::Request#initialize_copy` so that the env is duped when
-	the request gets duped.
-
-Thu Aug 20 16:20:58 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Added methods for manipulating request specific data.  This includes
-	data set as CGI parameters, and just any arbitrary data the user wants
-	to associate with a particular request.  New methods:
-
-	  * 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>
-
-	*  lib/rack/utils.rb: add a method for constructing "delete" cookie
-	headers.  This allows us to construct cookie headers without depending
-	on the side effects of mutating a hash.
-
-Fri Jun 12 11:37:41 2015  Aaron Patterson <tenderlove@ruby-lang.org>
-
-	* Prevent extremely deep parameters from being parsed. CVE-2015-3225
-
-### May 6th, 2015, Thirty seventh public release 1.6.1
-  - Fix CVE-2014-9490, denial of service attack in OkJson ([8cd610](https://github.com/rack/rack/commit/8cd61062954f70e0a03e2855704e95ff4bdd4f6e))
-  - Use a monotonic time for Rack::Runtime, if available ([d170b2](https://github.com/rack/rack/commit/d170b2363c949dce60871f9d5a6bfc83da2bedb5))
-  - RACK_MULTIPART_LIMIT changed to RACK_MULTIPART_PART_LIMIT (RACK_MULTIPART_LIMIT is deprecated and will be removed in 1.7.0) ([c096c5](https://github.com/rack/rack/commit/c096c50c00230d8eee13ad5f79ad027d9a3f3ca9))
-  - See the full [git history](https://github.com/rack/rack/compare/1.6.0...1.6.1) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.6%22)
-
-### May 6th, 2015, Thirty seventh public release 1.5.3
-  - Fix CVE-2014-9490, denial of service attack in OkJson ([99f725](https://github.com/rack/rack/commit/99f725b583b357376ffbb7b3b042c5daa3106ad6))
-  - Backport bug fixes to 1.5 series ([#585](https://github.com/rack/rack/pull/585), [#711](https://github.com/rack/rack/pull/711), [#756](https://github.com/rack/rack/pull/756))
-  - See the full [git history](https://github.com/rack/rack/compare/1.5.2...1.5.3) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.5.3%22)
-
-### December 18th, 2014, Thirty sixth public release 1.6.0
-  - Response#unauthorized? helper ([#580](https://github.com/rack/rack/pull/580))
-  - Deflater now accepts an options hash to control compression on a per-request level ([#457](https://github.com/rack/rack/pull/457))
-  - Builder#warmup method for app preloading ([#617](https://github.com/rack/rack/pull/617))
-  - Request#accept_language method to extract HTTP_ACCEPT_LANGUAGE ([#623](https://github.com/rack/rack/pull/623))
-  - Add quiet mode of rack server, rackup --quiet ([#674](https://github.com/rack/rack/pull/674))
-  - Update HTTP Status Codes to RFC 7231 ([#754](https://github.com/rack/rack/pull/754))
-  - Less strict header name validation according to [RFC 2616](https://tools.ietf.org/html/rfc2616) ([#399](https://github.com/rack/rack/pull/399))
-    - SPEC updated to specify headers conform to RFC7230 specification ([6839fc](https://github.com/rack/rack/commit/6839fc203339f021cb3267fb09cba89410f086e9))
-  - Etag correctly marks etags as weak ([#681](https://github.com/rack/rack/issues/681))
-  - Request#port supports multiple x-http-forwarded-proto values ([#669](https://github.com/rack/rack/pull/669))
-  - Utils#multipart_part_limit configures the maximum number of parts a request can contain ([#684](https://github.com/rack/rack/pull/684))
-  - Default host to localhost when in development mode ([#514](https://github.com/rack/rack/pull/514))
-  - Various bugfixes and performance improvements (See the full [git history](https://github.com/rack/rack/compare/1.5.2...1.6.0) and [milestone tag](https://github.com/rack/rack/issues?utf8=%E2%9C%93&q=milestone%3A%22Rack+1.6%22))
-
-### February 7th, 2013, Thirty fifth public release 1.5.2
-  - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
-  - Fix CVE-2013-0262, symlink path traversal in Rack::File
-  - Add various methods to Session for enhanced Rails compatibility
-  - Request#trusted_proxy? now only matches whole stirngs
-  - Add JSON cookie coder, to be default in Rack 1.6+ due to security concerns
-  - URLMap host matching in environments that don't set the Host header fixed
-  - Fix a race condition that could result in overwritten pidfiles
-  - Various documentation additions
-
-### February 7th, 2013, Thirty fifth public release 1.4.5
-  - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
-  - Fix CVE-2013-0262, symlink path traversal in Rack::File
-
-### February 7th, Thirty fifth public release 1.1.6, 1.2.8, 1.3.10
-  - Fix CVE-2013-0263, timing attack against Rack::Session::Cookie
-
-### January 28th, 2013: Thirty fourth public release 1.5.1
-  - Rack::Lint check_hijack now conforms to other parts of SPEC
-  - Added hash-like methods to Abstract::ID::SessionHash for compatibility
-  - Various documentation corrections
-
-### January 21st, 2013: Thirty third public release 1.5.0
-  - Introduced hijack SPEC, for before-response and after-response hijacking
-  - SessionHash is no longer a Hash subclass
-  - Rack::File cache_control parameter is removed, in place of headers options
-  - 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
-  - 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
-  - Rack::Utils#best_q_match provides a helper for Accept headers
-  - Rack::Handler.pick provides convenience for finding available servers
-  - Puma added to the list of default servers (preferred over Webrick)
-  - Various middleware now correctly close body when replacing it
-  - Rack::Request#params is no longer persistent with only GET params
-  - Rack::Request#update_param and #delete_param provide persistent operations
-  - Rack::Request#trusted_proxy? now returns true for local unix sockets
-  - Rack::Response no longer forces Content-Types
-  - Rack::Sendfile provides local mapping configuration options
-  - Rack::Utils#rfc2109 provides old netscape style time output
-  - Updated HTTP status codes
-  - Ruby 1.8.6 likely no longer passes tests, and is no longer fully supported
-
-### January 13th, 2013: Thirty second public release 1.4.4, 1.3.9, 1.2.7, 1.1.5
-  - [SEC] Rack::Auth::AbstractRequest no longer symbolizes arbitrary strings
-  - Fixed erroneous test case in the 1.3.x series
-
-### January 7th, 2013: Thirty first public release 1.4.3
-  - Security: Prevent unbounded reads in large multipart boundaries
-
-### January 7th, 2013: Thirtieth public release 1.3.8
-  - Security: Prevent unbounded reads in large multipart boundaries
-
-### January 6th, 2013: Twenty ninth public release 1.4.2
-  - Add warnings when users do not provide a session secret
-  - Fix parsing performance for unquoted filenames
-  - Updated URI backports
-  - Fix URI backport version matching, and silence constant warnings
-  - Correct parameter parsing with empty values
-  - Correct rackup '-I' flag, to allow multiple uses
-  - Correct rackup pidfile handling
-  - Report rackup line numbers correctly
-  - Fix request loops caused by non-stale nonces with time limits
-  - Fix reloader on Windows
-  - Prevent infinite recursions from Response#to_ary
-  - Various middleware better conforms to the body close specification
-  - Updated language for the body close specification
-  - Additional notes regarding ECMA escape compatibility issues
-  - Fix the parsing of multiple ranges in range headers
-  - Prevent errors from empty parameter keys
-  - Added PATCH verb to Rack::Request
-  - Various documentation updates
-  - Fix session merge semantics (fixes rack-test)
-  - Rack::Static :index can now handle multiple directories
-  - All tests now utilize Rack::Lint (special thanks to Lars Gierth)
-  - Rack::File cache_control parameter is now deprecated, and removed by 1.5
-  - Correct Rack::Directory script name escaping
-  - Rack::Static supports header rules for sophisticated configurations
-  - Multipart parsing now works without a Content-Length header
-  - New logos courtesy of Zachary Scott!
-  - Rack::BodyProxy now explicitly defines #each, useful for C extensions
-  - Cookies that are not URI escaped no longer cause exceptions
-
-### January 6th, 2013: Twenty eighth public release 1.3.7
-  - Add warnings when users do not provide a session secret
-  - Fix parsing performance for unquoted filenames
-  - Updated URI backports
-  - Fix URI backport version matching, and silence constant warnings
-  - Correct parameter parsing with empty values
-  - Correct rackup '-I' flag, to allow multiple uses
-  - Correct rackup pidfile handling
-  - Report rackup line numbers correctly
-  - Fix request loops caused by non-stale nonces with time limits
-  - Fix reloader on Windows
-  - Prevent infinite recursions from Response#to_ary
-  - Various middleware better conforms to the body close specification
-  - Updated language for the body close specification
-  - Additional notes regarding ECMA escape compatibility issues
-  - Fix the parsing of multiple ranges in range headers
-
-### January 6th, 2013: Twenty seventh public release 1.2.6
-  - Add warnings when users do not provide a session secret
-  - Fix parsing performance for unquoted filenames
-
-### January 6th, 2013: Twenty sixth public release 1.1.4
-  - Add warnings when users do not provide a session secret
-
-### January 22nd, 2012: Twenty fifth public release 1.4.1
-  - Alter the keyspace limit calculations to reduce issues with nested params
-  - Add a workaround for multipart parsing where files contain unescaped "%"
-  - Added Rack::Response::Helpers#method_not_allowed? (code 405)
-  - Rack::File now returns 404 for illegal directory traversals
-  - Rack::File now returns 405 for illegal methods (non HEAD/GET)
-  - Rack::Cascade now catches 405 by default, as well as 404
-  - Cookies missing '--' no longer cause an exception to be raised
-  - Various style changes and documentation spelling errors
-  - Rack::BodyProxy always ensures to execute its block
-  - Additional test coverage around cookies and secrets
-  - Rack::Session::Cookie can now be supplied either secret or old_secret
-  - Tests are no longer dependent on set order
-  - Rack::Static no longer defaults to serving index files
-  - Rack.release was fixed
-
-### December 28th, 2011: Twenty fourth public release 1.4.0
-  - Ruby 1.8.6 support has officially been dropped. Not all tests pass.
-  - Raise sane error messages for broken config.ru
-  - Allow combining run and map in a config.ru
-  - Rack::ContentType will not set Content-Type for responses without a body
-  - Status code 205 does not send a response body
-  - Rack::Response::Helpers will not rely on instance variables
-  - Rack::Utils.build_query no longer outputs '=' for nil query values
-  - Various mime types added
-  - Rack::MockRequest now supports HEAD
-  - Rack::Directory now supports files that contain RFC3986 reserved chars
-  - Rack::File now only supports GET and HEAD requests
-  - Rack::Server#start now passes the block to Rack::Handler::<h>#run
-  - Rack::Static now supports an index option
-  - Added the Teapot status code
-  - rackup now defaults to Thin instead of Mongrel (if installed)
-  - Support added for HTTP_X_FORWARDED_SCHEME
-  - Numerous bug fixes, including many fixes for new and alternate rubies
-
-### December 28th, 2011: Twenty first public release: 1.1.3.
-  - Security fix. http://www.ocert.org/advisories/ocert-2011-003.html
-    Further information here: http://jruby.org/2011/12/27/jruby-1-6-5-1
-
-### October 17, 2011: Twentieth public release 1.3.5
-  - Fix annoying warnings caused by the backport in 1.3.4
-
-### October 1, 2011: Nineteenth public release 1.3.4
-  - Backport security fix from 1.9.3, also fixes some roundtrip issues in URI
-  - Small documentation update
-  - Fix an issue where BodyProxy could cause an infinite recursion
-  - Add some supporting files for travis-ci
-
-### September 16, 2011: Eighteenth public release 1.2.4
-  - Fix a bug with MRI regex engine to prevent XSS by malformed unicode
-
-### September 16, 2011: Seventeenth public release 1.3.3
-  - Fix bug with broken query parameters in Rack::ShowExceptions
-  - Rack::Request#cookies no longer swallows exceptions on broken input
-  - Prevents XSS attacks enabled by bug in Ruby 1.8's regexp engine
-  - Rack::ConditionalGet handles broken If-Modified-Since helpers
-
-### July 16, 2011: Sixteenth public release 1.3.2
-  - Fix for Rails and rack-test, Rack::Utils#escape calls to_s
-
-### July 13, 2011: Fifteenth public release 1.3.1
-  - Fix 1.9.1 support
-  - Fix JRuby support
-  - Properly handle $KCODE in Rack::Utils.escape
-  - Make method_missing/respond_to behavior consistent for Rack::Lock,
-    Rack::Auth::Digest::Request and Rack::Multipart::UploadedFile
-  - Reenable passing rack.session to session middleware
-  - Rack::CommonLogger handles streaming responses correctly
-  - Rack::MockResponse calls close on the body object
-  - Fix a DOS vector from MRI stdlib backport
-
-### May 22nd, 2011: Fourteenth public release 1.2.3
-  - Pulled in relevant bug fixes from 1.3
-  - Fixed 1.8.6 support
-
-### May 22nd, 2011: Thirteenth public release 1.3.0
-  - Various performance optimizations
-  - Various multipart fixes
-  - Various multipart refactors
-  - Infinite loop fix for multipart
-  - Test coverage for Rack::Server returns
-  - Allow files with '..', but not path components that are '..'
-  - rackup accepts handler-specific options on the command line
-  - Request#params no longer merges POST into GET (but returns the same)
-  - Use URI.encode_www_form_component instead. Use core methods for escaping.
-  - Allow multi-line comments in the config file
-  - Bug L#94 reported by Nikolai Lugovoi, query parameter unescaping.
-  - Rack::Response now deletes Content-Length when appropriate
-  - Rack::Deflater now supports streaming
-  - Improved Rack::Handler loading and searching
-  - Support for the PATCH verb
-  - env['rack.session.options'] now contains session options
-  - Cookies respect renew
-  - Session middleware uses SecureRandom.hex
-
-### March 13th, 2011: Twelfth public release 1.2.2/1.1.2.
-  - Security fix in Rack::Auth::Digest::MD5: when authenticator
-    returned nil, permission was granted on empty password.
-
-### June 15th, 2010: Eleventh public release 1.2.1.
-  - Make CGI handler rewindable
-  - Rename spec/ to test/ to not conflict with SPEC on lesser
-    operating systems
-
-### June 13th, 2010: Tenth public release 1.2.0.
-  - Removed Camping adapter: Camping 2.0 supports Rack as-is
-  - Removed parsing of quoted values
-  - Add Request.trace? and Request.options?
-  - Add mime-type for .webm and .htc
-  - Fix HTTP_X_FORWARDED_FOR
-  - Various multipart fixes
-  - Switch test suite to bacon
-
-### January 3rd, 2010: Ninth public release 1.1.0.
-  - Moved Auth::OpenID to rack-contrib.
-  - SPEC change that relaxes Lint slightly to allow subclasses of the
-    required types
-  - SPEC change to document rack.input binary mode in greator detail
-  - SPEC define optional rack.logger specification
-  - File servers support X-Cascade header
-  - Imported Config middleware
-  - Imported ETag middleware
-  - Imported Runtime middleware
-  - Imported Sendfile middleware
-  - New Logger and NullLogger middlewares
-  - Added mime type for .ogv and .manifest.
-  - Don't squeeze PATH_INFO slashes
-  - Use Content-Type to determine POST params parsing
-  - Update Rack::Utils::HTTP_STATUS_CODES hash
-  - Add status code lookup utility
-  - Response should call #to_i on the status
-  - Add Request#user_agent
-  - Request#host knows about forwared host
-  - Return an empty string for Request#host if HTTP_HOST and
-    SERVER_NAME are both missing
-  - Allow MockRequest to accept hash params
-  - Optimizations to HeaderHash
-  - Refactored rackup into Rack::Server
-  - Added Utils.build_nested_query to complement Utils.parse_nested_query
-  - Added Utils::Multipart.build_multipart to complement
-    Utils::Multipart.parse_multipart
-  - Extracted set and delete cookie helpers into Utils so they can be
-    used outside Response
-  - Extract parse_query and parse_multipart in Request so subclasses
-    can change their behavior
-  - Enforce binary encoding in RewindableInput
-  - Set correct external_encoding for handlers that don't use RewindableInput
-
-### October 18th, 2009: Eighth public release 1.0.1.
-  - Bump remainder of rack.versions.
-  - Support the pure Ruby FCGI implementation.
-  - Fix for form names containing "=": split first then unescape components
-  - Fixes the handling of the filename parameter with semicolons in names.
-  - Add anchor to nested params parsing regexp to prevent stack overflows
-  - Use more compatible gzip write api instead of "<<".
-  - Make sure that Reloader doesn't break when executed via ruby -e
-  - Make sure WEBrick respects the :Host option
-  - Many Ruby 1.9 fixes.
-
-### April 25th, 2009: Seventh public release 1.0.0.
-  - SPEC change: Rack::VERSION has been pushed to [1,0].
-  - SPEC change: header values must be Strings now, split on "\n".
-  - SPEC change: Content-Length can be missing, in this case chunked transfer
-    encoding is used.
-  - SPEC change: rack.input must be rewindable and support reading into
-    a buffer, wrap with Rack::RewindableInput if it isn't.
-  - SPEC change: rack.session is now specified.
-  - SPEC change: Bodies can now additionally respond to #to_path with
-    a filename to be served.
-  - NOTE: String bodies break in 1.9, use an Array consisting of a
-    single String instead.
-  - New middleware Rack::Lock.
-  - New middleware Rack::ContentType.
-  - Rack::Reloader has been rewritten.
-  - Major update to Rack::Auth::OpenID.
-  - Support for nested parameter parsing in Rack::Response.
-  - Support for redirects in Rack::Response.
-  - HttpOnly cookie support in Rack::Response.
-  - The Rakefile has been rewritten.
-  - Many bugfixes and small improvements.
-
-### January 9th, 2009: Sixth public release 0.9.1.
-  - Fix directory traversal exploits in Rack::File and Rack::Directory.
-
-### January 6th, 2009: Fifth public release 0.9.
-  - Rack is now managed by the Rack Core Team.
-  - Rack::Lint is stricter and follows the HTTP RFCs more closely.
-  - Added ConditionalGet middleware.
-  - Added ContentLength middleware.
-  - Added Deflater middleware.
-  - Added Head middleware.
-  - Added MethodOverride middleware.
-  - Rack::Mime now provides popular MIME-types and their extension.
-  - Mongrel Header now streams.
-  - Added Thin handler.
-  - Official support for swiftiplied Mongrel.
-  - Secure cookies.
-  - Made HeaderHash case-preserving.
-  - Many bugfixes and small improvements.
-
-### August 21st, 2008: Fourth public release 0.4.
-  - New middleware, Rack::Deflater, by Christoffer Sawicki.
-  - OpenID authentication now needs ruby-openid 2.
-  - New Memcache sessions, by blink.
-  - Explicit EventedMongrel handler, by Joshua Peek <josh@joshpeek.com>
-  - Rack::Reloader is not loaded in rackup development mode.
-  - rackup can daemonize with -D.
-  - Many bugfixes, especially for pool sessions, URLMap, thread safety
-    and tempfile handling.
-  - Improved tests.
-  - Rack moved to Git.
-
-### February 26th, 2008: Third public release 0.3.
-  - LiteSpeed handler, by Adrian Madrid.
-  - SCGI handler, by Jeremy Evans.
-  - Pool sessions, by blink.
-  - OpenID authentication, by blink.
-  - :Port and :File options for opening FastCGI sockets, by blink.
-  - Last-Modified HTTP header for Rack::File, by blink.
-  - Rack::Builder#use now accepts blocks, by Corey Jewett.
-    (See example/protectedlobster.ru)
-  - HTTP status 201 can contain a Content-Type and a body now.
-  - Many bugfixes, especially related to Cookie handling.
-
-### May 16th, 2007: Second public release 0.2.
-  - HTTP Basic authentication.
-  - Cookie Sessions.
-  - Static file handler.
-  - Improved Rack::Request.
-  - Improved Rack::Response.
-  - Added Rack::ShowStatus, for better default error messages.
-  - Bug fixes in the Camping adapter.
-  - Removed Rails adapter, was too alpha.
-
-### March 3rd, 2007: First public release 0.1.
-
-/* vim: set filetype=changelog */
diff --git a/COPYING b/MIT-LICENSE
similarity index 89%
rename from COPYING
rename to MIT-LICENSE
index 1f5c7013370574c7f023c1697ee2644cbf93a423..703d118f9a293d561fd731038ffd994777c19ed2 100644
--- a/COPYING
+++ b/MIT-LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2007-2016 Christian Neukirchen <purl.org/net/chneukirchen>
+The MIT License (MIT)
+
+Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to
@@ -13,6 +15,6 @@ all copies or substantial portions of the Software.
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 
+THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/README.rdoc b/README.rdoc
index a6100e95a3dbec0eaf6feab6922d3b1a3a461b0d..5d4ad7c4e394bc1c236014c7807d7c68553353ce 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -1,99 +1,103 @@
-= Rack, a modular Ruby webserver interface {<img src="https://secure.travis-ci.org/rack/rack.svg" alt="Build Status" />}[http://travis-ci.org/rack/rack] {<img src="https://gemnasium.com/rack/rack.svg" alt="Dependency Status" />}[https://gemnasium.com/rack/rack]
+= \Rack, a modular Ruby webserver interface
 
-Rack provides a minimal, modular, and adaptable interface for developing
-web applications in Ruby.  By wrapping HTTP requests and responses in
+{<img src="https://rack.github.io/logo.png" width="400" alt="rack powers web applications" />}[https://rack.github.io/]
+
+{<img src="https://circleci.com/gh/rack/rack.svg?style=svg" alt="CircleCI" />}[https://circleci.com/gh/rack/rack]
+{<img src="https://badge.fury.io/rb/rack.svg" alt="Gem Version" />}[http://badge.fury.io/rb/rack]
+{<img src="https://api.dependabot.com/badges/compatibility_score?dependency-name=rack&package-manager=bundler&version-scheme=semver" alt="SemVer Stability" />}[https://dependabot.com/compatibility-score.html?dependency-name=rack&package-manager=bundler&version-scheme=semver]
+
+\Rack provides a minimal, modular, and adaptable interface for developing
+web applications in Ruby. By wrapping HTTP requests and responses in
 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.
 
-The exact details of this are described in the Rack specification,
-which all Rack applications should conform to.
+The exact details of this are described in the \Rack specification,
+which all \Rack applications should conform to.
 
 == Supported web servers
 
-The included *handlers* connect all kinds of web servers to Rack:
-* WEBrick
+The included *handlers* connect all kinds of web servers to \Rack:
+
+* WEBrick[https://github.com/ruby/webrick]
 * FCGI
 * CGI
 * SCGI
-* LiteSpeed
-* Thin
-
-These web servers include Rack handlers in their distributions:
-* Ebb
-* Fuzed
-* Glassfish v3
-* Phusion Passenger (which is mod_rack for Apache and for nginx)
-* Puma
-* Reel
-* Unicorn
-* unixrack
-* uWSGI
-* yahns
-
-Any valid Rack app will run the same on all these handlers, without
+* LiteSpeed[https://www.litespeedtech.com/]
+* Thin[https://rubygems.org/gems/thin]
+
+These web servers include \Rack handlers in their distributions:
+
+* Agoo[https://github.com/ohler55/agoo]
+* Falcon[https://github.com/socketry/falcon]
+* {NGINX Unit}[https://unit.nginx.org/]
+* {Phusion Passenger}[https://www.phusionpassenger.com/] (which is mod_rack for Apache and for nginx)
+* Puma[https://puma.io/]
+* Unicorn[https://bogomips.org/unicorn/]
+* uWSGI[https://uwsgi-docs.readthedocs.io/en/latest/]
+
+Any valid \Rack app will run the same on all these handlers, without
 changing anything.
 
 == Supported web frameworks
 
-These frameworks include Rack adapters in their distributions:
-* Camping
-* Coset
-* Espresso
-* Halcyon
-* Mack
-* Maveric
-* Merb
+These frameworks include \Rack adapters in their distributions:
+
+* Camping[http://www.ruby-camping.com/]
+* Coset[http://leahneukirchen.org/repos/coset/]
+* Hanami[https://hanamirb.org/]
+* Padrino[http://padrinorb.com/]
 * Racktools::SimpleApplication
-* Ramaze
-* Ruby on Rails
-* Rum
-* Sinatra
-* Sin
-* Vintage
-* Waves
-* Wee
+* Ramaze[http://ramaze.net/]
+* Roda[https://github.com/jeremyevans/roda]
+* {Ruby on Rails}[https://rubyonrails.org/]
+* Rum[https://github.com/leahneukirchen/rum]
+* Sinatra[http://sinatrarb.com/]
+* Utopia[https://github.com/socketry/utopia]
+* WABuR[https://github.com/ohler55/wabur]
 * ... and many others.
 
 == Available middleware
 
-Between the server and the framework, Rack can be customized to your
+Between the server and the framework, \Rack can be customized to your
 applications needs using middleware, for example:
+
 * Rack::URLMap, to route to multiple applications inside the same process.
 * 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
-detail in the Rack specification.  These optional components can be
+detail in the \Rack specification.  These optional components can be
 used in any way you wish.
 
 == Convenience
 
 If you want to develop outside of existing frameworks, implement your
-own ones, or develop middleware, Rack provides many helpers to create
-Rack applications quickly and without doing the same web stuff all
+own ones, or develop middleware, \Rack provides many helpers to create
+\Rack applications quickly and without doing the same web stuff all
 over:
+
 * Rack::Request, which also provides query string parsing and
   multipart handling.
 * Rack::Response, for convenient generation of HTTP replies and
   cookie handling.
 * Rack::MockRequest and Rack::MockResponse for efficient and quick
-  testing of Rack application without real HTTP round-trips.
+  testing of \Rack application without real HTTP round-trips.
 
 == rack-contrib
 
 The plethora of useful middleware created the need for a project that
-collects fresh Rack middleware.  rack-contrib includes a variety of
-add-on components for Rack and it is easy to contribute new modules.
+collects fresh \Rack middleware.  rack-contrib includes a variety of
+add-on components for \Rack and it is easy to contribute new modules.
 
 * https://github.com/rack/rack-contrib
 
 == rackup
 
-rackup is a useful tool for running Rack applications, which uses the
+rackup is a useful tool for running \Rack applications, which uses the
 Rack::Builder DSL to configure middleware and build up applications
 easily.
 
@@ -117,18 +121,13 @@ By default, the lobster is found at http://localhost:9292.
 
 == Installing with RubyGems
 
-A Gem of Rack is available at rubygems.org.  You can install it with:
+A Gem of \Rack is available at {rubygems.org}[https://rubygems.org/gems/rack]. You can install it with:
 
     gem install rack
 
-I also provide a local mirror of the gems (and development snapshots)
-at my site:
-
-    gem install rack --source http://chneukirchen.org/releases/gems/
-
 == Running the tests
 
-Testing Rack requires the bacon testing framework:
+Testing \Rack requires the bacon testing framework:
 
     bundle install --without extra # to be able to run the fast tests
 
@@ -138,7 +137,7 @@ Or:
 
 There is a rake-based test task:
 
-    rake test       tests all the tests
+    rake test # tests all the tests
 
 The testsuite has no dependencies outside of the core Ruby
 installation and bacon.
@@ -146,37 +145,15 @@ installation and bacon.
 To run the test suite completely, you need:
 
   * fcgi
-  * memcache-client
+  * dalli
   * thin
 
-The full set of tests test FCGI access with lighttpd (on port
-9203) so you will need lighttpd installed as well as the FCGI
-libraries and the fcgi gem:
-
-Download and install lighttpd:
-
-    http://www.lighttpd.net/download
-
-Installing the FCGI libraries:
-
-    curl -O http://www.fastcgi.com/dist/fcgi-2.4.0.tar.gz
-    tar xzvf fcgi-2.4.0.tar.gz
-    cd fcgi-2.4.0
-    ./configure --prefix=/usr/local
-    make
-    sudo make install
-    cd ..
-
-Installing the Ruby fcgi gem:
-
-    gem install fcgi
-
-Furthermore, to test Memcache sessions, you need memcached (will be
-run on port 11211) and memcache-client installed.
+To test Memcache sessions, you need memcached (will be
+run on port 11211) and dalli installed.
 
 == Configuration
 
-Several parameters can be modified on Rack::Utils to configure Rack behaviour.
+Several parameters can be modified on Rack::Utils to configure \Rack behaviour.
 
 e.g:
 
@@ -198,27 +175,28 @@ The default is 128, which means that a single request can't upload more than 128
 
 Set to 0 for no limit.
 
-Can also be set via the RACK_MULTIPART_PART_LIMIT environment variable.
+Can also be set via the +RACK_MULTIPART_PART_LIMIT+ environment variable.
 
-== History
+== Changelog
 
-See <https://github.com/rack/rack/blob/master/HISTORY.md>.
+See {CHANGELOG.md}[https://github.com/rack/rack/blob/master/CHANGELOG.md].
 
 == Contact
 
 Please post bugs, suggestions and patches to
-the bug tracker at <https://github.com/rack/rack/issues>.
+the bug tracker at {issues}[https://github.com/rack/rack/issues].
 
 Please post security related bugs and suggestions to the core team at
-<https://groups.google.com/group/rack-core> or rack-core@googlegroups.com. This
+<https://groups.google.com/forum/#!forum/rack-core> or rack-core@googlegroups.com. This
 list is not public. Due to wide usage of the library, it is strongly preferred
 that we manage timing in order to provide viable patches at the time of
 disclosure. Your assistance in this matter is greatly appreciated.
 
 Mailing list archives are available at
-<https://groups.google.com/group/rack-devel>.
+<https://groups.google.com/forum/#!forum/rack-devel>.
 
 Git repository (send Git patches to the mailing list):
+
 * https://github.com/rack/rack
 * http://git.vuxu.org/cgi-bin/gitweb.cgi?p=rack-github.git
 
@@ -226,9 +204,9 @@ You are also welcome to join the #rack channel on irc.freenode.net.
 
 == Thanks
 
-The Rack Core Team, consisting of
+The \Rack Core Team, consisting of
 
-* Leah Neukirchen (chneukirchen[https://github.com/chneukirchen])
+* Leah Neukirchen (leahneukirchen[https://github.com/leahneukirchen])
 * James Tucker (raggi[https://github.com/raggi])
 * Josh Peek (josh[https://github.com/josh])
 * José Valim (josevalim[https://github.com/josevalim])
@@ -237,7 +215,7 @@ The Rack Core Team, consisting of
 * Santiago Pastorino (spastorino[https://github.com/spastorino])
 * Konstantin Haase (rkh[https://github.com/rkh])
 
-and the Rack Alumnis
+and the \Rack Alumnis
 
 * Ryan Tomayko (rtomayko[https://github.com/rtomayko])
 * Scytrin dai Kinthra (scytrin[https://github.com/scytrin])
@@ -269,34 +247,16 @@ would like to thank:
 * Alexander Kellett for testing the Gem and reviewing the announcement.
 * Marcus Rückert, for help with configuring and debugging lighttpd.
 * The WSGI team for the well-done and documented work they've done and
-  Rack builds up on.
+  \Rack builds up on.
 * All bug reporters and patch contributors not mentioned above.
 
-== Copyright
-
-Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <http://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
-deal in the Software without restriction, including without limitation the
-rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
-sell copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
+== Links
 
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
-THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+\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/forum/#!forum/rack-devel>
 
-== Links
+== License
 
-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>
-Rack's Rubyforge project:: <http://rubyforge.org/projects/rack>
+\Rack is released under the {MIT License}[https://opensource.org/licenses/MIT].
diff --git a/Rakefile b/Rakefile
index c112f1da8566685dcc68d5eec831ea931d9a2711..a365ff5420657f585f5ca8792f7fd0dc9f450a1f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,7 +1,9 @@
-# Rakefile for Rack.  -*-ruby-*-
+# frozen_string_literal: true
+
+require "rake/testtask"
 
 desc "Run all the tests"
-task :default => [:test]
+task default: :test
 
 desc "Install gem dependencies"
 task :deps do
@@ -16,7 +18,7 @@ task :deps do
 end
 
 desc "Make an archive as .tar.gz"
-task :dist => %w[chmod ChangeLog SPEC rdoc] do
+task dist: %w[chmod changelog spec rdoc] do
   sh "git archive --format=tar --prefix=#{release}/ HEAD^{tree} >#{release}.tar"
   sh "pax -waf #{release}.tar -s ':^:#{release}/:' SPEC ChangeLog doc rack.gemspec"
   sh "gzip -f -9 #{release}.tar"
@@ -31,7 +33,7 @@ task :officialrelease do
   sh "mv stage/#{release}.tar.gz stage/#{release}.gem ."
 end
 
-task :officialrelease_really => %w[SPEC dist gem] do
+task officialrelease_really: %w[spec dist gem] do
   sh "shasum #{release}.tar.gz #{release}.gem"
 end
 
@@ -46,7 +48,7 @@ task :chmod do
 end
 
 desc "Generate a ChangeLog"
-task :changelog => %w[ChangeLog]
+task changelog: "ChangeLog"
 
 file '.git/index'
 file "ChangeLog" => '.git/index' do
@@ -68,8 +70,10 @@ file "ChangeLog" => '.git/index' do
   }
 end
 
-file 'lib/rack/lint.rb'
 desc "Generate Rack Specification"
+task spec: "SPEC"
+
+file 'lib/rack/lint.rb'
 file "SPEC" => 'lib/rack/lint.rb' do
   File.open("SPEC", "wb") { |file|
     IO.foreach("lib/rack/lint.rb") { |line|
@@ -80,24 +84,27 @@ file "SPEC" => 'lib/rack/lint.rb' do
   }
 end
 
-desc "Run all the fast + platform agnostic tests"
-task :test => 'SPEC' do
-  opts     = ENV['TEST'] || ''
-  specopts = ENV['TESTOPTS']
-
-  sh "ruby -I./lib:./test -S minitest #{opts} #{specopts} test/gemloader.rb test/spec*.rb"
+Rake::TestTask.new("test:regular") do |t|
+  t.libs << "test"
+  t.test_files = FileList["test/**/*_test.rb", "test/**/spec_*.rb", "test/gemloader.rb"]
+  t.warning = false
+  t.verbose = true
 end
 
+desc "Run all the fast + platform agnostic tests"
+task test: %w[spec test:regular]
+
 desc "Run all the tests we run on CI"
-task :ci => :test
+task ci: :test
 
-task :gem => ["SPEC"] do
+task gem: :spec do
   sh "gem build rack.gemspec"
 end
 
-task :doc => :rdoc
+task doc: :rdoc
+
 desc "Generate RDoc documentation"
-task :rdoc => %w[ChangeLog SPEC] do
+task rdoc: %w[changelog spec] do
   sh(*%w{rdoc --line-numbers --main README.rdoc
               --title 'Rack\ Documentation' --charset utf-8 -U -o doc} +
               %w{README.rdoc KNOWN-ISSUES SPEC ChangeLog} +
@@ -105,11 +112,11 @@ task :rdoc => %w[ChangeLog SPEC] do
   cp "contrib/rdoc.css", "doc/rdoc.css"
 end
 
-task :pushdoc => %w[rdoc] do
+task pushdoc: :rdoc do
   sh "rsync -avz doc/ rack.rubyforge.org:/var/www/gforge-projects/rack/doc/"
 end
 
-task :pushsite => %w[pushdoc] do
+task pushsite: :pushdoc do
   sh "cd site && git gc"
   sh "rsync -avz site/ rack.rubyforge.org:/var/www/gforge-projects/rack/"
   sh "cd site && git push"
diff --git a/SECURITY_POLICY.md b/SECURITY_POLICY.md
new file mode 100644
index 0000000000000000000000000000000000000000..04fdd48839864f39c4735074875af33ff69524ea
--- /dev/null
+++ b/SECURITY_POLICY.md
@@ -0,0 +1,66 @@
+# Rack maintenance
+
+## Supported versions
+
+### New features
+
+New features will only be added to the master branch and will not be made available in point releases.
+
+### Bug fixes
+
+Only the latest release series will receive bug fixes. When enough bugs are fixed and its deemed worthy to release a new gem, this is the branch it happens from.
+
+* Current release series: 2.0.x
+
+### Security issues
+
+The current release series and the next most recent one will receive patches and new versions in case of a security issue.
+
+* Current release series: 2.0.x
+* Next most recent release series: 1.6.x
+
+### Severe security issues
+
+For severe security issues we will provide new versions as above, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team.
+
+* Current release series: 2.0.x
+* Next most recent release series: 1.6.x
+* Last most recent release series: 1.5.x
+
+### Unsupported Release Series
+
+When a release series is no longer supported, it’s your own responsibility to deal with bugs and security issues. We may provide back-ports of the fixes and publish them to git, however there will be no new versions released. If you are not comfortable maintaining your own versions, you should upgrade to a supported version.
+
+## Reporting a bug
+
+All security bugs in Rack should be reported to the core team through our private mailing list [rack-core@googlegroups.com](https://groups.google.com/forum/#!forum/rack-core). Your report will be acknowledged within 24 hours, and you’ll receive a more detailed response to your email within 48 hours indicating the next steps in handling your report.
+
+After the initial reply to your report the security team will endeavor to keep you informed of the progress being made towards a fix and full announcement. These updates will be sent at least every five days, in reality this is more likely to be every 24-48 hours.
+
+If you have not received a reply to your email within 48 hours, or have not heard from the security team for the past five days there are a few steps you can take:
+
+* Contact the current security coordinator [Aaron Patterson](mailto:tenderlove@ruby-lang.org) directly
+
+## Disclosure Policy
+
+Rack has a 5 step disclosure policy.
+
+1. Security report received and is assigned a primary handler. This person will coordinate the fix and release process.
+2. Problem is confirmed and, a list of all affected versions is determined. Code is audited to find any potential similar problems.
+3. Fixes are prepared for all releases which are still supported. These fixes are not committed to the public repository but rather held locally pending the announcement.
+4. A suggested embargo date for this vulnerability is chosen and distros@openwall is notified. This notification will include patches for all versions still under support and a contact address for packagers who need advice back-porting patches to older versions.
+5. On the embargo date, the [ruby security announcement mailing list](mailto:ruby-security-ann@googlegroups.com) is sent a copy of the announcement. The changes are pushed to the public repository and new gems released to rubygems.
+
+Typically the embargo date will be set 72 hours from the time vendor-sec is first notified, however this may vary depending on the severity of the bug or difficulty in applying a fix.
+
+This process can take some time, especially when coordination is required with maintainers of other projects. Every effort will be made to handle the bug in as timely a manner as possible, however it’s important that we follow the release process above to ensure that the disclosure is handled in a consistent manner.
+
+## Receiving Security Updates
+
+The best way to receive all the security announcements is to subscribe to the [ruby security announcement mailing list](mailto:ruby-security-ann@googlegroups.com). The mailing list is very low traffic, and it receives the public notifications the moment the embargo is lifted. If you produce packages of Rack and require prior notification of vulnerabilities, you should be subscribed to vendor-sec.
+
+No one outside the core team, the initial reporter or vendor-sec will be notified prior to the lifting of the embargo. We regret that we cannot make exceptions to this policy for high traffic or important sites, as any disclosure beyond the minimum required to coordinate a fix could cause an early leak of the vulnerability.
+
+## Comments on this Policy
+
+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/forum/#!forum/rack-core).
diff --git a/SPEC b/SPEC
index d3a7ca8111d54dc13b1e6eab4bcfb917b81c2f62..59ec9d7202608bec9fe9a7e136c44aaf2a151253 100644
--- a/SPEC
+++ b/SPEC
@@ -60,9 +60,8 @@ below.
                            the presence or absence of the
                            appropriate HTTP header in the
                            request. See
-                           <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
-                           RFC3875 section 4.1.18</a> for
-                           specific behavior.
+                           {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
+                           for specific behavior.
 In addition to this, the Rack environment must include these
 Rack-specific variables:
 <tt>rack.version</tt>:: The Array representing this version of Rack
@@ -226,9 +225,9 @@ This is an HTTP status. When parsed as integer (+to_i+), it must be
 greater than or equal to 100.
 === The Headers
 The header must respond to +each+, and yield values of key and value.
+The header keys must be Strings.
 Special headers starting "rack." are for communicating with the
 server, and must not be sent back to the client.
-The header keys must be Strings.
 The header must not contain a +Status+ key.
 The header must conform to RFC7230 token specification, i.e. cannot
 contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
diff --git a/bin/rackup b/bin/rackup
index ad94af4be3b4c41a0bdb7b356f4c96fdfd24fb2a..58988a0b32d16a93e818e02533fd5745b943dc09 100755
--- a/bin/rackup
+++ b/bin/rackup
@@ -1,4 +1,5 @@
 #!/usr/bin/env ruby
+# frozen_string_literal: true
 
 require "rack"
 Rack::Server.start
diff --git a/debian/changelog b/debian/changelog
index 2aa82b862938cb48dfcdebbfb42ff3c710c35d89..9989349ebdcf8b0a082c1ddd6b6931110c1d6a69 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,114 @@
+ruby-rack (2.1.4-2) unstable; urgency=medium
+
+  * Revert "Drop all patches"
+    - Rack::Builder::parse_file#test_0006_strips
+      leading unicode byte order mark when present still
+      fails in i386 and stuff. Meh, I'll take a look later.
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Sun, 03 Jan 2021 17:49:29 +0530
+
+ruby-rack (2.1.4-1) unstable; urgency=medium
+
+  [ Pirate Praveen ]
+  * New upstream version 2.1.4
+  * Bump Standards-Version to 4.5.1 (no changes needed)
+  * Drop patches applied upstream
+
+  [ Utkarsh Gupta ]
+  * Drop all patches
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Sun, 03 Jan 2021 17:25:43 +0530
+
+ruby-rack (2.1.1-6) unstable; urgency=medium
+
+  [ Cédric Boutillier ]
+  * [ci skip] Update team name
+  * [ci skip] Add .gitattributes to keep unwanted files out
+    of the source package
+
+  [ Debian Janitor ]
+  * Apply multi-arch hints. + ruby-rack: Add :all qualifier
+    for ruby dependency.
+
+  [ Utkarsh Gupta ]
+  * When parsing cookies, only decode the values.
+    Patch utils to fix cookie parsing. (Fixes: CVE-2020-8184)
+    (Closes: #963477)
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Sat, 02 Jan 2021 17:42:02 +0530
+
+ruby-rack (2.1.1-5) unstable; urgency=medium
+
+  * Add patch to use Dir.entries instead of Dir[glob] to prevent
+    user-specified glob metacharacters (Fixes: CVE-2020-8161)
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Thu, 21 May 2020 17:06:27 +0530
+
+ruby-rack (2.1.1-4) unstable; urgency=medium
+
+  * Remove ruby-minitest-global-expectations from Depends
+  * Add ruby-minitest-global-expectations for tests
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Fri, 10 Apr 2020 18:37:00 +0530
+
+ruby-rack (2.1.1-3) unstable; urgency=medium
+
+  * Add patch to skip random failure
+    (probably fixed in later upstream version)
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Fri, 10 Apr 2020 04:21:09 +0530
+
+ruby-rack (2.1.1-2) unstable; urgency=medium
+
+  [ Debian Janitor ]
+  * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository,
+    Repository-Browse.
+
+  [ Utkarsh Gupta ]
+  * Shoot to unstable
+  * Enable tests :D
+  * Add BD on ruby-minitest-global-expectations
+  * Add runtime dependency on ruby-minitest-global-expectations
+  * Fix package wrt cme
+  * Use AUTOPKGTEST_TMP in tests as ADTTMP is deprecated
+  * Add myself as an uploader
+  * Add Rules-Requires-Root: no
+  * Add Breaks for ruby-rack-oauth2
+
+ -- Utkarsh Gupta <utkarsh@debian.org>  Fri, 10 Apr 2020 03:43:38 +0530
+
+ruby-rack (2.1.1-1) experimental; urgency=medium
+
+  * Team upload
+  * New upstream version 2.1.1
+  * Bump Standards-Version to 4.4.1 (no changes needed)
+  * Switch test to minitest (but disable tests because build deps not packaged)
+  * Switch to github tarballs for tests
+  * Upload to experimental because autopkgtest for berkshelf-api coquelicot
+    nanoc rails redmine ruby-acts-as-api ruby-faye ruby-grape ruby-moneta
+    ruby-omniauth ruby-rack-attack ruby-rack-oauth2 ruby-rack-openid
+    ruby-voight-kampff failed and rebuilds of berkshelf-api coquelicot nanoc
+    redmine ruby-grape ruby-omniauth ruby-rack-oauth2 ruby-warden failed
+
+ -- Pirate Praveen <praveen@debian.org>  Sun, 12 Jan 2020 20:00:24 +0530
+
+ruby-rack (2.0.7-2) unstable; urgency=medium
+
+  * Team upload
+  * Re-upload to unstable
+  * Add salsa-ci.yml
+  * Bump Standards-Version to 4.4.0
+  * Bump debhelper-compat to 12
+
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Tue, 03 Sep 2019 00:22:18 +0530
+
+ruby-rack (2.0.7-1) experimental; urgency=medium
+
+  * Team upload
+  * New upstream version 2.0.7
+
+ -- Utkarsh Gupta <guptautkarsh2102@gmail.com>  Wed, 15 May 2019 21:13:44 +0530
+
 ruby-rack (2.0.6-3) unstable; urgency=medium
 
   * Team upload.
@@ -9,7 +120,7 @@ ruby-rack (2.0.6-3) unstable; urgency=medium
 ruby-rack (2.0.6-2) unstable; urgency=medium
 
   * Team upload
-  * Re-upload to unstable 
+  * Re-upload to unstable
 
  -- Sruthi Chandran <srud@disroot.org>  Thu, 03 Jan 2019 21:42:53 +0530
 
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index b4de3947675361a7770d29b8982c407b0ec6b2a0..0000000000000000000000000000000000000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-11
diff --git a/debian/control b/debian/control
index 6793b96eb41566e7833a9a37840be2bcd4040954..2c295eb26a7851e57cb50fd4d50340651c895c0e 100644
--- a/debian/control
+++ b/debian/control
@@ -1,34 +1,35 @@
 Source: ruby-rack
+Maintainer: Debian Ruby Team <pkg-ruby-extras-maintainers@lists.alioth.debian.org>
+Uploaders: Chris Lamb <lamby@debian.org>,
+           Lucas Nussbaum <lucas@debian.org>,
+           Youhei SASAKI <uwabami@gfd-dennou.org>,
+           Paul van Tilburg <paulvt@debian.org>,
+           Utkarsh Gupta <utkarsh@debian.org>
 Section: ruby
 Priority: optional
-Maintainer: Debian Ruby Extras Maintainers <pkg-ruby-extras-maintainers@lists.alioth.debian.org>
-Uploaders:
- Chris Lamb <lamby@debian.org>,
- Lucas Nussbaum <lucas@debian.org>,
- Youhei SASAKI <uwabami@gfd-dennou.org>,
- Paul van Tilburg <paulvt@debian.org>,
-Build-Depends:
- debhelper (>= 11~),
- gem2deb,
- rake,
- ruby-bacon,
- ruby-concurrent (>= 1.0.3~),
- ruby-dalli,
- thin,
-Standards-Version: 4.3.0
-Vcs-Git: https://salsa.debian.org/ruby-team/ruby-rack.git
+Build-Depends: debhelper-compat (= 12),
+               gem2deb,
+               rake,
+               ruby-bacon,
+               ruby-concurrent (>= 1.0.3~),
+               ruby-dalli,
+               ruby-minitest-global-expectations,
+               thin
+Standards-Version: 4.5.1
 Vcs-Browser: https://salsa.debian.org/ruby-team/ruby-rack
+Vcs-Git: https://salsa.debian.org/ruby-team/ruby-rack.git
 Homepage: https://rack.github.io/
 XS-Ruby-Versions: all
+Rules-Requires-Root: no
 
 Package: ruby-rack
 Architecture: all
 XB-Ruby-Versions: ${ruby:Versions}
-Depends:
- ruby | ruby-interpreter,
- ${misc:Depends},
- ${shlibs:Depends},
-Breaks: ruby-sinatra (<< 2)
+Depends: ruby:any | ruby-interpreter,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Breaks: ruby-sinatra (<< 2),
+        ruby-rack-oauth2 (<< 1.11)
 Description: modular Ruby webserver interface
  Rack provides a minimal, modular and adaptable interface for developing
  web applications in Ruby.  By wrapping HTTP requests and responses in
diff --git a/debian/patches/series b/debian/patches/series
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0144d346f188e4d26bc242fb2f2cd2abb56d37cf 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -0,0 +1 @@
+skip-random-failure.patch
diff --git a/debian/patches/skip-random-failure.patch b/debian/patches/skip-random-failure.patch
new file mode 100644
index 0000000000000000000000000000000000000000..4abb52fa409646ac30fb18f63590c6690737da08
--- /dev/null
+++ b/debian/patches/skip-random-failure.patch
@@ -0,0 +1,15 @@
+Description: Skip random failure.
+Author: Utkarsh Gupta <utkarsh@debian.org>
+Forwarded: not-needed
+Last-Update: 2020-04-09
+
+--- a/test/spec_builder.rb
++++ b/test/spec_builder.rb
+@@ -253,6 +253,7 @@
+     end
+ 
+     it "strips leading unicode byte order mark when present" do
++      skip
+       app, _ = Rack::Builder.parse_file config_file('bom.ru')
+       Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
+     end
diff --git a/debian/ruby-tests.rake b/debian/ruby-tests.rake
index 350d87c73cfbeac8c9763a5f9102a9c0dc4eb273..c747f15246ad18ad36b4f33f5a57539fcd6f45cc 100644
--- a/debian/ruby-tests.rake
+++ b/debian/ruby-tests.rake
@@ -1,7 +1,13 @@
 # -*- mode: ruby; coding: utf-8 -*-
-require 'rbconfig' unless defined? RbConfig
-ruby = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'])
 
-task :default do
-  sh "#{ruby} /usr/bin/bacon -I./test -w -a"
-end
+require "rake/testtask"
+
+desc "Run all the tests"
+task default: :test
+
+Rake::TestTask.new("test") do |t|
+  t.libs << "test"
+  t.test_files = FileList["test/**/*_test.rb", "test/**/spec_*.rb", "test/gemloader.rb"]
+  t.warning = false
+  t.verbose = true
+end
\ No newline at end of file
diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..33c3a640d2a84306b6a8b5640692ac3481739e65
--- /dev/null
+++ b/debian/salsa-ci.yml
@@ -0,0 +1,4 @@
+---
+include:
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
diff --git a/debian/tests/control b/debian/tests/control
index 09f541ea6ef01fe005531c639b1fb06f3c62af7b..3276f56d2d7a6ec77f0151a581ac651cfd7d5f0b 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -2,5 +2,5 @@ Tests: smoke-test
 Depends: @, curl
 
 Test-Command: gem2deb-test-runner
-Depends: @, gem2deb-test-runner, rake, ruby-bacon, thin
+Depends: @, gem2deb-test-runner, rake, ruby-bacon, thin, ruby-minitest-global-expectations
 Restrictions: allow-stderr
diff --git a/debian/tests/smoke-test b/debian/tests/smoke-test
index 8b5af5a33e6e3ba10c003329c5b9487d92c192bd..ad25a547d950fbcfa45b2fe143d4112498f23a0b 100755
--- a/debian/tests/smoke-test
+++ b/debian/tests/smoke-test
@@ -4,7 +4,7 @@ exec 2>&1
 
 set -ex
 
-appdir=${ADTTMP:-/tmp}/app
+appdir=${AUTOPKGTEST_TMP:-/tmp}/app
 mkdir $appdir
 cd $appdir
 
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
new file mode 100644
index 0000000000000000000000000000000000000000..62aed4764da483bd81b7a2110ca287a97599d2d4
--- /dev/null
+++ b/debian/upstream/metadata
@@ -0,0 +1,4 @@
+Bug-Database: https://github.com/rack/rack/issues
+Bug-Submit: https://github.com/rack/rack/issues/new
+Repository: https://github.com/rack/rack.git
+Repository-Browse: https://github.com/rack/rack
diff --git a/debian/watch b/debian/watch
index 18653c12621a73155633354d746a06e558f188ad..e5592986da8bb2cf79affaebdbe68bed9e58e3a7 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,2 +1,2 @@
 version=3
-https://gemwatch.debian.net/rack .*/rack-(.*).tar.gz
+  https://github.com/rack/rack/tags .*/v?(\d\S*)\.tar\.gz
\ No newline at end of file
diff --git a/example/lobster.ru b/example/lobster.ru
index cc7ffcae88901574b81c932af1622ef1f79b255c..901e18a53098a1e84785bd57b8e629187703b555 100644
--- a/example/lobster.ru
+++ b/example/lobster.ru
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/lobster'
 
 use Rack::ShowExceptions
diff --git a/example/protectedlobster.rb b/example/protectedlobster.rb
index 26b23661f8fc72282826978632c53006f87b25e4..fe4f0b0948756c7ced3774be889fe1942d49ed68 100644
--- a/example/protectedlobster.rb
+++ b/example/protectedlobster.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack'
 require 'rack/lobster'
 
@@ -11,4 +13,4 @@ protected_lobster.realm = 'Lobster 2.0'
 
 pretty_protected_lobster = Rack::ShowStatus.new(Rack::ShowExceptions.new(protected_lobster))
 
-Rack::Server.start :app => pretty_protected_lobster, :Port => 9292
+Rack::Server.start app: pretty_protected_lobster, Port: 9292
diff --git a/example/protectedlobster.ru b/example/protectedlobster.ru
index 1ba48702dc2fe982fce0749bf765b9425e22274e..0eb243cc6d0a8ea4f8e890a45a7d79eb580b9229 100644
--- a/example/protectedlobster.ru
+++ b/example/protectedlobster.ru
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/lobster'
 
 use Rack::ShowExceptions
diff --git a/lib/rack.rb b/lib/rack.rb
index edf80dd9fe1e1fc141f91dac334f4122d7672e42..634235ae318eecd1ba05f03e29b1cdb7378c3df4 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -1,7 +1,9 @@
-# Copyright (C) 2007, 2008, 2009, 2010 Christian Neukirchen <purl.org/net/chneukirchen>
+# frozen_string_literal: true
+
+# Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
 #
 # Rack is freely distributable under the terms of an MIT-style license.
-# See COPYING or http://www.opensource.org/licenses/mit-license.php.
+# See MIT-LICENSE or https://opensource.org/licenses/MIT.
 
 # The Rack main module, serving as a namespace for all core Rack
 # modules and classes.
@@ -11,80 +13,80 @@
 
 module Rack
   # The Rack protocol version number implemented.
-  VERSION = [1,3]
+  VERSION = [1, 3]
 
   # Return the Rack protocol version as a dotted string.
   def self.version
     VERSION.join(".")
   end
 
-  RELEASE = "2.0.6"
+  RELEASE = "2.1.4"
 
   # Return the Rack release as a dotted string.
   def self.release
     RELEASE
   end
 
-  HTTP_HOST         = 'HTTP_HOST'.freeze
-  HTTP_VERSION      = 'HTTP_VERSION'.freeze
-  HTTPS             = 'HTTPS'.freeze
-  PATH_INFO         = 'PATH_INFO'.freeze
-  REQUEST_METHOD    = 'REQUEST_METHOD'.freeze
-  REQUEST_PATH      = 'REQUEST_PATH'.freeze
-  SCRIPT_NAME       = 'SCRIPT_NAME'.freeze
-  QUERY_STRING      = 'QUERY_STRING'.freeze
-  SERVER_PROTOCOL   = 'SERVER_PROTOCOL'.freeze
-  SERVER_NAME       = 'SERVER_NAME'.freeze
-  SERVER_ADDR       = 'SERVER_ADDR'.freeze
-  SERVER_PORT       = 'SERVER_PORT'.freeze
-  CACHE_CONTROL     = 'Cache-Control'.freeze
-  CONTENT_LENGTH    = 'Content-Length'.freeze
-  CONTENT_TYPE      = 'Content-Type'.freeze
-  SET_COOKIE        = 'Set-Cookie'.freeze
-  TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
-  HTTP_COOKIE       = 'HTTP_COOKIE'.freeze
-  ETAG              = 'ETag'.freeze
+  HTTP_HOST         = 'HTTP_HOST'
+  HTTP_VERSION      = 'HTTP_VERSION'
+  HTTPS             = 'HTTPS'
+  PATH_INFO         = 'PATH_INFO'
+  REQUEST_METHOD    = 'REQUEST_METHOD'
+  REQUEST_PATH      = 'REQUEST_PATH'
+  SCRIPT_NAME       = 'SCRIPT_NAME'
+  QUERY_STRING      = 'QUERY_STRING'
+  SERVER_PROTOCOL   = 'SERVER_PROTOCOL'
+  SERVER_NAME       = 'SERVER_NAME'
+  SERVER_ADDR       = 'SERVER_ADDR'
+  SERVER_PORT       = 'SERVER_PORT'
+  CACHE_CONTROL     = 'Cache-Control'
+  CONTENT_LENGTH    = 'Content-Length'
+  CONTENT_TYPE      = 'Content-Type'
+  SET_COOKIE        = 'Set-Cookie'
+  TRANSFER_ENCODING = 'Transfer-Encoding'
+  HTTP_COOKIE       = 'HTTP_COOKIE'
+  ETAG              = 'ETag'
 
   # HTTP method verbs
-  GET     = 'GET'.freeze
-  POST    = 'POST'.freeze
-  PUT     = 'PUT'.freeze
-  PATCH   = 'PATCH'.freeze
-  DELETE  = 'DELETE'.freeze
-  HEAD    = 'HEAD'.freeze
-  OPTIONS = 'OPTIONS'.freeze
-  LINK    = 'LINK'.freeze
-  UNLINK  = 'UNLINK'.freeze
-  TRACE   = 'TRACE'.freeze
+  GET     = 'GET'
+  POST    = 'POST'
+  PUT     = 'PUT'
+  PATCH   = 'PATCH'
+  DELETE  = 'DELETE'
+  HEAD    = 'HEAD'
+  OPTIONS = 'OPTIONS'
+  LINK    = 'LINK'
+  UNLINK  = 'UNLINK'
+  TRACE   = 'TRACE'
 
   # Rack environment variables
-  RACK_VERSION                        = 'rack.version'.freeze
-  RACK_TEMPFILES                      = 'rack.tempfiles'.freeze
-  RACK_ERRORS                         = 'rack.errors'.freeze
-  RACK_LOGGER                         = 'rack.logger'.freeze
-  RACK_INPUT                          = 'rack.input'.freeze
-  RACK_SESSION                        = 'rack.session'.freeze
-  RACK_SESSION_OPTIONS                = 'rack.session.options'.freeze
-  RACK_SHOWSTATUS_DETAIL              = 'rack.showstatus.detail'.freeze
-  RACK_MULTITHREAD                    = 'rack.multithread'.freeze
-  RACK_MULTIPROCESS                   = 'rack.multiprocess'.freeze
-  RACK_RUNONCE                        = 'rack.run_once'.freeze
-  RACK_URL_SCHEME                     = 'rack.url_scheme'.freeze
-  RACK_HIJACK                         = 'rack.hijack'.freeze
-  RACK_IS_HIJACK                      = 'rack.hijack?'.freeze
-  RACK_HIJACK_IO                      = 'rack.hijack_io'.freeze
-  RACK_RECURSIVE_INCLUDE              = 'rack.recursive.include'.freeze
-  RACK_MULTIPART_BUFFER_SIZE          = 'rack.multipart.buffer_size'.freeze
-  RACK_MULTIPART_TEMPFILE_FACTORY     = 'rack.multipart.tempfile_factory'.freeze
-  RACK_REQUEST_FORM_INPUT             = 'rack.request.form_input'.freeze
-  RACK_REQUEST_FORM_HASH              = 'rack.request.form_hash'.freeze
-  RACK_REQUEST_FORM_VARS              = 'rack.request.form_vars'.freeze
-  RACK_REQUEST_COOKIE_HASH            = 'rack.request.cookie_hash'.freeze
-  RACK_REQUEST_COOKIE_STRING          = 'rack.request.cookie_string'.freeze
-  RACK_REQUEST_QUERY_HASH             = 'rack.request.query_hash'.freeze
-  RACK_REQUEST_QUERY_STRING           = 'rack.request.query_string'.freeze
-  RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'.freeze
-  RACK_SESSION_UNPACKED_COOKIE_DATA   = 'rack.session.unpacked_cookie_data'.freeze
+  RACK_VERSION                        = 'rack.version'
+  RACK_TEMPFILES                      = 'rack.tempfiles'
+  RACK_ERRORS                         = 'rack.errors'
+  RACK_LOGGER                         = 'rack.logger'
+  RACK_INPUT                          = 'rack.input'
+  RACK_SESSION                        = 'rack.session'
+  RACK_SESSION_OPTIONS                = 'rack.session.options'
+  RACK_SHOWSTATUS_DETAIL              = 'rack.showstatus.detail'
+  RACK_MULTITHREAD                    = 'rack.multithread'
+  RACK_MULTIPROCESS                   = 'rack.multiprocess'
+  RACK_RUNONCE                        = 'rack.run_once'
+  RACK_URL_SCHEME                     = 'rack.url_scheme'
+  RACK_HIJACK                         = 'rack.hijack'
+  RACK_IS_HIJACK                      = 'rack.hijack?'
+  RACK_HIJACK_IO                      = 'rack.hijack_io'
+  RACK_RECURSIVE_INCLUDE              = 'rack.recursive.include'
+  RACK_MULTIPART_BUFFER_SIZE          = 'rack.multipart.buffer_size'
+  RACK_MULTIPART_TEMPFILE_FACTORY     = 'rack.multipart.tempfile_factory'
+  RACK_REQUEST_FORM_INPUT             = 'rack.request.form_input'
+  RACK_REQUEST_FORM_HASH              = 'rack.request.form_hash'
+  RACK_REQUEST_FORM_VARS              = 'rack.request.form_vars'
+  RACK_REQUEST_COOKIE_HASH            = 'rack.request.cookie_hash'
+  RACK_REQUEST_COOKIE_STRING          = 'rack.request.cookie_string'
+  RACK_REQUEST_QUERY_HASH             = 'rack.request.query_hash'
+  RACK_REQUEST_QUERY_STRING           = 'rack.request.query_string'
+  RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
+  RACK_SESSION_UNPACKED_COOKIE_DATA   = 'rack.session.unpacked_cookie_data'
 
   autoload :Builder, "rack/builder"
   autoload :BodyProxy, "rack/body_proxy"
@@ -97,6 +99,7 @@ module Rack
   autoload :ContentType, "rack/content_type"
   autoload :ETag, "rack/etag"
   autoload :File, "rack/file"
+  autoload :Files, "rack/files"
   autoload :Deflater, "rack/deflater"
   autoload :Directory, "rack/directory"
   autoload :ForwardRequest, "rack/recursive"
diff --git a/lib/rack/auth/abstract/handler.rb b/lib/rack/auth/abstract/handler.rb
index c657691e1c14654da711cc038b33a55e11f2e3cf..3ed87091c7e7a33f8bcaa76e523c254a66e4f9fe 100644
--- a/lib/rack/auth/abstract/handler.rb
+++ b/lib/rack/auth/abstract/handler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Auth
     # Rack::Auth::AbstractHandler implements common authentication functionality.
@@ -8,7 +10,7 @@ module Rack
 
       attr_accessor :realm
 
-      def initialize(app, realm=nil, &authenticator)
+      def initialize(app, realm = nil, &authenticator)
         @app, @realm, @authenticator = app, realm, authenticator
       end
 
diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb
index b738cc98a07a66ab37f78abd5886ed577daccda5..23da4bf2717c0056fba03b10a9b7496f2678d507 100644
--- a/lib/rack/auth/abstract/request.rb
+++ b/lib/rack/auth/abstract/request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/request'
 
 module Rack
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index 9c5892141cd48d0298cad561fbd12d2540335911..d334939ca2b8a75fe438590a31286bcf730e8065 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -1,5 +1,8 @@
+# frozen_string_literal: true
+
 require 'rack/auth/abstract/handler'
 require 'rack/auth/abstract/request'
+require 'base64'
 
 module Rack
   module Auth
@@ -45,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 ddee35def328f25f9fc2b1a3e0b7cda3a4420a68..62bff9846ae6babbbb34a40c215f2459fafe21f1 100644
--- a/lib/rack/auth/digest/md5.rb
+++ b/lib/rack/auth/digest/md5.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/auth/abstract/handler'
 require 'rack/auth/digest/request'
 require 'rack/auth/digest/params'
@@ -21,7 +23,7 @@ module Rack
 
         attr_writer :passwords_hashed
 
-        def initialize(app, realm=nil, opaque=nil, &authenticator)
+        def initialize(app, realm = nil, opaque = nil, &authenticator)
           @passwords_hashed = nil
           if opaque.nil? and realm.respond_to? :values_at
             realm, opaque, @passwords_hashed = realm.values_at :realm, :opaque, :passwords_hashed
@@ -47,7 +49,7 @@ module Rack
 
           if valid?(auth)
             if auth.nonce.stale?
-              return unauthorized(challenge(:stale => true))
+              return unauthorized(challenge(stale: true))
             else
               env['REMOTE_USER'] = auth.username
 
@@ -61,7 +63,7 @@ module Rack
 
         private
 
-        QOP = 'auth'.freeze
+        QOP = 'auth'
 
         def params(hash = {})
           Params.new do |params|
@@ -106,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 57089cb30466713806244e093e3cc5ed5ee3fd9a..3216d973e0daddfe0493588fed27d35c4f46a665 100644
--- a/lib/rack/auth/digest/nonce.rb
+++ b/lib/rack/auth/digest/nonce.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
 require 'digest/md5'
+require 'base64'
 
 module Rack
   module Auth
@@ -18,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)
@@ -26,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/auth/digest/params.rb b/lib/rack/auth/digest/params.rb
index 2b226e62832129296bcb30a4640270c2bb47daa4..f611b3c35eb4dee2a05a1cfae8c7ceb66e5feb0d 100644
--- a/lib/rack/auth/digest/params.rb
+++ b/lib/rack/auth/digest/params.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Auth
     module Digest
@@ -38,12 +40,12 @@ 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
 
         def quote(str) # From WEBrick::HTTPUtils
-          '"' << str.gsub(/[\\\"]/o, "\\\1") << '"'
+          '"' + str.gsub(/[\\\"]/o, "\\\1") + '"'
         end
 
       end
diff --git a/lib/rack/auth/digest/request.rb b/lib/rack/auth/digest/request.rb
index 105c76747f92f4d333ee46ec58a929014c2060ad..a3ab47439b74d8ac0ad1ed9fcbf94e1783b897a1 100644
--- a/lib/rack/auth/digest/request.rb
+++ b/lib/rack/auth/digest/request.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/auth/abstract/request'
 require 'rack/auth/digest/params'
 require 'rack/auth/digest/nonce'
diff --git a/lib/rack/body_proxy.rb b/lib/rack/body_proxy.rb
index 7fcfe31671a2fafc76f0d096f2a14a4eb936702d..cb161980ed9420a90fdade7359f64224d8a5fa73 100644
--- a/lib/rack/body_proxy.rb
+++ b/lib/rack/body_proxy.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   class BodyProxy
     def initialize(body, &block)
@@ -6,11 +8,7 @@ module Rack
       @closed = false
     end
 
-    def respond_to?(method_name, include_all=false)
-      case method_name
-      when :to_ary, 'to_ary'
-        return false
-      end
+    def respond_to?(method_name, include_all = false)
       super or @body.respond_to?(method_name, include_all)
     end
 
@@ -37,7 +35,6 @@ module Rack
     end
 
     def method_missing(method_name, *args, &block)
-      super if :to_ary == method_name
       @body.__send__(method_name, *args, &block)
     end
   end
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 975cf1e19ce821e5dd1fd1ae5b9246881fbccdf8..ebfa1f1184d3afe924248065f94d0d0b21d9738e 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   # Rack::Builder implements a small DSL to iteratively construct Rack
   # applications.
@@ -29,29 +31,43 @@ 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)
-      options = {}
-      if config =~ /\.ru$/
-        cfgfile = ::File.read(config)
-        if cfgfile[/^#\\(.*)/] && opts
-          options = opts.parse! $1.split(/\s+/)
-        end
-        cfgfile.sub!(/^__END__\n.*\Z/m, '')
-        app = new_from_string cfgfile, config
+      if config.end_with?('.ru')
+        return self.load_file(config, opts)
       else
         require config
         app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
+        return app, {}
+      end
+    end
+
+    def self.load_file(path, opts = Server::Options.new)
+      options = {}
+
+      cfgfile = ::File.read(path)
+      cfgfile.slice!(/\A#{UTF_8_BOM}/) if cfgfile.encoding == Encoding::UTF_8
+
+      if cfgfile[/^#\\(.*)/] && opts
+        options = opts.parse! $1.split(/\s+/)
       end
+
+      cfgfile.sub!(/^__END__\n.*\Z/m, '')
+      app = new_from_string cfgfile, path
+
       return app, options
     end
 
-    def self.new_from_string(builder_script, file="(rackup)")
+    def self.new_from_string(builder_script, file = "(rackup)")
       eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
         TOPLEVEL_BINDING, file, 0
     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
 
@@ -81,10 +97,11 @@ 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
+    ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
 
     # Takes an argument that is an object that responds to #call and returns a Rack response.
     # The simplest form of this is a lambda object:
@@ -113,7 +130,7 @@ module Rack
     #
     #   use SomeMiddleware
     #   run MyApp
-    def warmup(prc=nil, &block)
+    def warmup(prc = nil, &block)
       @warmup = prc || block
     end
 
@@ -141,10 +158,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
@@ -156,8 +180,8 @@ module Rack
     private
 
     def generate_map(default_app, mapping)
-      mapped = default_app ? {'/' => default_app} : {}
-      mapping.each { |r,b| mapped[r] = self.class.new(default_app, &b).to_app }
+      mapped = default_app ? { '/' => default_app } : {}
+      mapping.each { |r, b| mapped[r] = self.class.new(default_app, &b).to_app }
       URLMap.new(mapped)
     end
   end
diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb
index 6b8f415aebf4b097c0f9e09a7b6c9ee9f7d3c864..1ed7ffa980330382a0c98e36464c845391d56169 100644
--- a/lib/rack/cascade.rb
+++ b/lib/rack/cascade.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
 module Rack
   # Rack::Cascade tries a request on several apps, and returns the
   # first response that is not 404 or 405 (or in a list of configurable
   # status codes).
 
   class Cascade
-    NotFound = [404, {CONTENT_TYPE => "text/plain"}, []]
+    NotFound = [404, { CONTENT_TYPE => "text/plain" }, []]
 
     attr_reader :apps
 
-    def initialize(apps, catch=[404, 405])
-      @apps = []; @has_app = {}
+    def initialize(apps, catch = [404, 405])
+      @apps = []
       apps.each { |app| add app }
 
       @catch = {}
@@ -39,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/chunked.rb b/lib/rack/chunked.rb
index 3076931c4028a6cf3c1965ad4fff26253f7f72d2..e7e7d8d1db1d2f0ca717b685c199532c4d2a5712 100644
--- a/lib/rack/chunked.rb
+++ b/lib/rack/chunked.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 
 module Rack
@@ -10,7 +12,7 @@ module Rack
     # A body wrapper that emits chunked responses
     class Body
       TERM = "\r\n"
-      TAIL = "0#{TERM}#{TERM}"
+      TAIL = "0#{TERM}"
 
       include Rack::Utils
 
@@ -18,7 +20,7 @@ module Rack
         @body = body
       end
 
-      def each
+      def each(&block)
         term = TERM
         @body.each do |chunk|
           size = chunk.bytesize
@@ -28,11 +30,28 @@ module Rack
           yield [size.to_s(16), term, chunk, term].join
         end
         yield TAIL
+        insert_trailers(&block)
+        yield TERM
       end
 
       def close
         @body.close if @body.respond_to?(:close)
       end
+
+      private
+
+      def insert_trailers(&block)
+      end
+    end
+
+    class TrailerBody < Body
+      private
+
+      def insert_trailers(&block)
+        @body.trailers.each_pair do |k, v|
+          yield "#{k}: #{v}\r\n"
+        end
+      end
     end
 
     def initialize(app)
@@ -43,7 +62,7 @@ module Rack
     # a version (nor response headers)
     def chunkable_version?(ver)
       case ver
-      when "HTTP/1.0", nil, "HTTP/0.9"
+      when 'HTTP/1.0', nil, 'HTTP/0.9'
         false
       else
         true
@@ -54,15 +73,19 @@ module Rack
       status, headers, body = @app.call(env)
       headers = HeaderHash.new(headers)
 
-      if ! chunkable_version?(env[HTTP_VERSION]) ||
-         STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
+      if ! chunkable_version?(env[SERVER_PROTOCOL]) ||
+         STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
          headers[CONTENT_LENGTH] ||
          headers[TRANSFER_ENCODING]
         [status, headers, body]
       else
         headers.delete(CONTENT_LENGTH)
         headers[TRANSFER_ENCODING] = 'chunked'
-        [status, headers, Body.new(body)]
+        if headers['Trailer']
+          [status, headers, TrailerBody.new(body)]
+        else
+          [status, headers, Body.new(body)]
+        end
       end
     end
   end
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index ae410430e8b24b0f45174bfb349fecf4660af8ca..a513ff6ea5eebf9e8504380d552a9ec8722aacb5 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/body_proxy'
 
 module Rack
@@ -23,13 +25,13 @@ module Rack
     #   %{%s - %s [%s] "%s %s%s %s" %d %s\n} %
     FORMAT = %{%s - %s [%s] "%s %s%s %s" %d %s %0.4f\n}
 
-    def initialize(app, logger=nil)
+    def initialize(app, logger = nil)
       @app = app
       @logger = logger
     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 +41,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[HTTP_VERSION],
+        env[SERVER_PROTOCOL],
         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
@@ -65,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/conditional_get.rb b/lib/rack/conditional_get.rb
index 441dd38238355d7a37ca2004c468193456924308..bda8daf6dcf0910d0070dc78d958d366e202e307 100644
--- a/lib/rack/conditional_get.rb
+++ b/lib/rack/conditional_get.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 
 module Rack
@@ -68,7 +70,7 @@ module Rack
       # anything shorter is invalid, this avoids exceptions for common cases
       # most common being the empty string
       if since && since.length >= 16
-        # NOTE: there is no trivial way to write this in a non execption way
+        # NOTE: there is no trivial way to write this in a non exception way
         #   _rfc2822 returns a hash but is not that usable
         Time.rfc2822(since) rescue nil
       else
diff --git a/lib/rack/config.rb b/lib/rack/config.rb
index dc255d27e6467acb31a8c5586a352511878506c4..41f6f7dd57314bea3092932a04c104570cded304 100644
--- a/lib/rack/config.rb
+++ b/lib/rack/config.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   # Rack::Config modifies the environment using the block given during
   # initialization.
diff --git a/lib/rack/content_length.rb b/lib/rack/content_length.rb
index 2df7dfc8125657f3d30ce70d581bcc5f1e5e6614..e37fc3058fef23fa1bfe58a378b27f91017b9f87 100644
--- a/lib/rack/content_length.rb
+++ b/lib/rack/content_length.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 require 'rack/body_proxy'
 
@@ -15,7 +17,7 @@ module Rack
       status, headers, body = @app.call(env)
       headers = HeaderHash.new(headers)
 
-      if !STATUS_WITH_NO_ENTITY_BODY.include?(status.to_i) &&
+      if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
          !headers[CONTENT_LENGTH] &&
          !headers[TRANSFER_ENCODING] &&
          body.respond_to?(:to_ary)
diff --git a/lib/rack/content_type.rb b/lib/rack/content_type.rb
index 78ba43b7155e10565dfde453c893efc7477c6de6..010cc37b706c5bf4d2a8455be2c846f443317bd1 100644
--- a/lib/rack/content_type.rb
+++ b/lib/rack/content_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 
 module Rack
@@ -19,7 +21,7 @@ module Rack
       status, headers, body = @app.call(env)
       headers = Utils::HeaderHash.new(headers)
 
-      unless STATUS_WITH_NO_ENTITY_BODY.include?(status)
+      unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
         headers[CONTENT_TYPE] ||= @content_type
       end
 
diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a32fcdf629a3fff24c5d680ed26ae23ac2ae29d2
--- /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 46d5b20af45e22e82bdb825995bcfc6a78bd8df7..9a30c017542f0442651f20eb2268d0d989ca72d6 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -1,7 +1,11 @@
+# frozen_string_literal: true
+
 require "zlib"
 require "time"  # for Time.httpdate
 require 'rack/utils'
 
+require_relative 'core_ext/regexp'
+
 module Rack
   # This middleware enables compression of http responses.
   #
@@ -15,19 +19,26 @@ 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.
     #
     # [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.map(&:bytesize).reduce(0, :+) > 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 flushed 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)
@@ -52,33 +63,34 @@ 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)]
+        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 = ::Zlib::GzipWriter.new(self)
+        gzip.mtime = @mtime if @mtime
         @body.each { |part|
-          gzip.write(part)
-          gzip.flush
+          len = gzip.write(part)
+          # Flushing empty parts would raise Zlib::BufError.
+          gzip.flush if @sync && len > 0
         }
       ensure
         gzip.close
@@ -90,9 +102,8 @@ module Rack
       end
 
       def close
-        return if @closed
-        @closed = true
         @body.close if @body.respond_to?(:close)
+        @body = nil
       end
     end
 
@@ -101,18 +112,22 @@ module Rack
     def should_deflate?(env, status, headers, body)
       # 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/ ||
+      if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
+          /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
          (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)
 
+      # No point in compressing empty body, also handles usage with
+      # Rack::Sendfile.
+      return false if headers[CONTENT_LENGTH] == '0'
+
       true
     end
   end
diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb
index 89cfe807a202578f929c5d5fbc0a55611fbb3bc7..4c1f4dd61fa2903a53da305b76dc48938a179a82 100644
--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
 require 'time'
 require 'rack/utils'
 require 'rack/mime'
+require 'rack/files'
 
 module Rack
   # Rack::Directory serves entries below the +root+ given, according to the
@@ -8,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>"
@@ -41,9 +44,9 @@ table { width:100%%; }
 
     class DirectoryBody < Struct.new(:root, :path, :files)
       def each
-        show_path = Rack::Utils.escape_html(path.sub(/^#{root}/,''))
-        listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) }*"\n"
-        page  = DIR_PAGE % [ show_path, show_path , listings ]
+        show_path = Rack::Utils.escape_html(path.sub(/^#{root}/, ''))
+        listings = files.map{|f| DIR_FILE % DIR_FILE_escape(*f) } * "\n"
+        page = DIR_PAGE % [ show_path, show_path, listings ]
         page.each_line{|l| yield l }
       end
 
@@ -56,9 +59,9 @@ table { width:100%%; }
 
     attr_reader :root, :path
 
-    def initialize(root, app=nil)
+    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
 
@@ -86,9 +89,9 @@ table { width:100%%; }
 
       body = "Bad Request\n"
       size = body.bytesize
-      return [400, {CONTENT_TYPE => "text/plain",
+      return [400, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => size.to_s,
-        "X-Cascade" => "pass"}, [body]]
+        "X-Cascade" => "pass" }, [body]]
     end
 
     def check_forbidden(path_info)
@@ -96,20 +99,20 @@ table { width:100%%; }
 
       body = "Forbidden\n"
       size = body.bytesize
-      return [403, {CONTENT_TYPE => "text/plain",
+      return [403, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => size.to_s,
-        "X-Cascade" => "pass"}, [body]]
+        "X-Cascade" => "pass" }, [body]]
     end
 
     def list_directory(path_info, path, script_name)
-      files = [['../','Parent Directory','','','']]
-      glob = ::File.join(path, '*')
+      files = [['../', 'Parent Directory', '', '', '']]
 
       url_head = (script_name.split('/') + path_info.split('/')).map do |part|
         Rack::Utils.escape_path part
       end
 
-      Dir[glob].sort.each do |node|
+      Dir.entries(path).reject { |e| e.start_with?('.') }.sort.each do |node|
+        node = ::File.join path, node
         stat = stat(node)
         next unless stat
         basename = ::File.basename(node)
@@ -126,7 +129,7 @@ table { width:100%%; }
         files << [ url, basename, size, type, mtime ]
       end
 
-      return [ 200, { CONTENT_TYPE =>'text/html; charset=utf-8'}, DirectoryBody.new(@root, path, files) ]
+      return [ 200, { CONTENT_TYPE => 'text/html; charset=utf-8' }, DirectoryBody.new(@root, path, files) ]
     end
 
     def stat(node)
@@ -154,9 +157,9 @@ table { width:100%%; }
     def entity_not_found(path_info)
       body = "Entity not found: #{path_info}\n"
       size = body.bytesize
-      return [404, {CONTENT_TYPE => "text/plain",
+      return [404, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => size.to_s,
-        "X-Cascade" => "pass"}, [body]]
+        "X-Cascade" => "pass" }, [body]]
     end
 
     # Stolen from Ramaze
diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb
index 5a8c6452a7dbc0858651abe9ddb7b4b97dbd8e3a..fd3de554a3ad35e7d865a7d8f2fdcaeb7f4922ec 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack'
 require 'digest/sha2'
 
@@ -13,7 +15,7 @@ module Rack
   # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
   class ETag
     ETAG_STRING = Rack::ETAG
-    DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate".freeze
+    DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
 
     def initialize(app, no_cache_control = nil, cache_control = DEFAULT_CACHE_CONTROL)
       @app = app
diff --git a/lib/rack/events.rb b/lib/rack/events.rb
index 3782a22ebb6b21658e75d3f56c0a67f8388f2bd1..77b716754f227c7676271ff3a003235c64cdbe8a 100644
--- a/lib/rack/events.rb
+++ b/lib/rack/events.rb
@@ -1,11 +1,13 @@
+# frozen_string_literal: true
+
 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".
+  # 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:
   #
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index 09eb0afb84ad48b7d6018771f5c1dacbbca1d953..52b48e8bac52c8fe786029a5a45c397d1534ad84 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -1,176 +1,7 @@
-require 'time'
-require 'rack/utils'
-require 'rack/mime'
-require 'rack/request'
-require 'rack/head'
+# frozen_string_literal: true
 
-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 = 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
+require 'rack/files'
 
-      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
+module Rack
+  File = Files
 end
diff --git a/lib/rack/files.rb b/lib/rack/files.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1a91c8bcef23a1538b3d030cb38291a5d55206f
--- /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::Files 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::Files, 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) if 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 70a77fa97fec0504731d1951d18e36a5fa6b139d..df17b238ddb73d599fce01a934149b539e4e695f 100644
--- a/lib/rack/handler.rb
+++ b/lib/rack/handler.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   # *Handlers* connect web servers with Rack.
   #
@@ -17,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
@@ -43,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")
@@ -52,7 +57,7 @@ module Rack
       elsif ENV.include?("RACK_HANDLER")
         self.get(ENV["RACK_HANDLER"])
       else
-        pick ['puma', 'thin', 'webrick']
+        pick SERVER_NAMES
       end
     end
 
diff --git a/lib/rack/handler/cgi.rb b/lib/rack/handler/cgi.rb
index 528076946daaf89fd5dabc58a7f99a06a4c3c875..a223c5453e98138d0e9d5084cc647d49bca14889 100644
--- a/lib/rack/handler/cgi.rb
+++ b/lib/rack/handler/cgi.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
 require 'rack/content_length'
 require 'rack/rewindable_input'
 
 module Rack
   module Handler
     class CGI
-      def self.run(app, options=nil)
+      def self.run(app, options = nil)
         $stdin.binmode
         serve app
       end
diff --git a/lib/rack/handler/fastcgi.rb b/lib/rack/handler/fastcgi.rb
index e918dc94bc0c78ff11d9b892e1f60ed8486c5232..b3f825dac990b1e2a9dbf0a14547cfc0cb19514b 100644
--- a/lib/rack/handler/fastcgi.rb
+++ b/lib/rack/handler/fastcgi.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'fcgi'
 require 'socket'
 require 'rack/content_length'
@@ -7,7 +9,7 @@ if defined? FCGI::Stream
   class FCGI::Stream
     alias _rack_read_without_buffer read
 
-    def read(n, buffer=nil)
+    def read(n, buffer = nil)
       buf = _rack_read_without_buffer n
       buffer.replace(buf.to_s)  if buffer
       buf
@@ -18,7 +20,7 @@ end
 module Rack
   module Handler
     class FastCGI
-      def self.run(app, options={})
+      def self.run(app, options = {})
         if options[:File]
           STDIN.reopen(UNIXServer.new(options[:File]))
         elsif options[:Port]
diff --git a/lib/rack/handler/lsws.rb b/lib/rack/handler/lsws.rb
index d2cfd793521a73bca3c400d3adc1d8bae6b262b0..803182a2dd310359bdce654e997ce79435234a78 100644
--- a/lib/rack/handler/lsws.rb
+++ b/lib/rack/handler/lsws.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'lsapi'
 require 'rack/content_length'
 require 'rack/rewindable_input'
@@ -5,7 +7,7 @@ require 'rack/rewindable_input'
 module Rack
   module Handler
     class LSWS
-      def self.run(app, options=nil)
+      def self.run(app, options = nil)
         while LSAPI.accept != nil
           serve app
         end
diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb
index e056a01d8bcbd7d416cbec18581f13ad5b8b3c92..c8e916061bbdac7a3a5b082757470b5e2306baf6 100644
--- a/lib/rack/handler/scgi.rb
+++ b/lib/rack/handler/scgi.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'scgi'
 require 'stringio'
 require 'rack/content_length'
@@ -8,12 +10,12 @@ module Rack
     class SCGI < ::SCGI::Processor
       attr_accessor :app
 
-      def self.run(app, options=nil)
+      def self.run(app, options = nil)
         options[:Socket] = UNIXServer.new(options[:File]) if options[:File]
-        new(options.merge(:app=>app,
-                          :host=>options[:Host],
-                          :port=>options[:Port],
-                          :socket=>options[:Socket])).listen
+        new(options.merge(app: app,
+                          host: options[:Host],
+                          port: options[:Port],
+                          socket: options[:Socket])).listen
       end
 
       def self.valid_options
@@ -41,7 +43,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/thin.rb b/lib/rack/handler/thin.rb
index ca8806463ee0cdfd7a996a5693c72ccdd0c632ef..100dfd11947bb326035ff6073dedb8c38f06df4b 100644
--- a/lib/rack/handler/thin.rb
+++ b/lib/rack/handler/thin.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "thin"
 require "thin/server"
 require "thin/logging"
@@ -8,7 +10,7 @@ require "rack/chunked"
 module Rack
   module Handler
     class Thin
-      def self.run(app, options={})
+      def self.run(app, options = {})
         environment  = ENV['RACK_ENV'] || 'development'
         default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
 
diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb
index d0fcd21362176f932a8dacdf0d9652dcc9a0053a..4affdbde66c6021a5b9acfc13d4990bfeea5a86e 100644
--- a/lib/rack/handler/webrick.rb
+++ b/lib/rack/handler/webrick.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'webrick'
 require 'stringio'
 require 'rack/content_length'
@@ -22,7 +24,7 @@ end
 module Rack
   module Handler
     class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
-      def self.run(app, options={})
+      def self.run(app, options = {})
         environment  = ENV['RACK_ENV'] || 'development'
         default_host = environment == 'development' ? 'localhost' : nil
 
@@ -79,7 +81,7 @@ module Rack
         env[QUERY_STRING] ||= ""
         unless env[PATH_INFO] == ""
           path, n = req.request_uri.path, env[SCRIPT_NAME].length
-          env[PATH_INFO] = path[n, path.length-n]
+          env[PATH_INFO] = path[n, path.length - n]
         end
         env[REQUEST_PATH] ||= [env[SCRIPT_NAME], env[PATH_INFO]].join
 
diff --git a/lib/rack/head.rb b/lib/rack/head.rb
index 6f1d7472817f7c3cd6cdd0bb73cd5277a8a67ed3..c257ae4d5d72120f088295275c0df37aee93dab4 100644
--- a/lib/rack/head.rb
+++ b/lib/rack/head.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/body_proxy'
 
 module Rack
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
index 683ba68411db0c332173455046fda0d9f91fe083..98ba9b44111939d1160bc40bd36c4e72c30510b0 100644
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 require 'forwardable'
 
@@ -33,7 +35,7 @@ module Rack
 
     ## A Rack application is a Ruby object (not a class) that
     ## responds to +call+.
-    def call(env=nil)
+    def call(env = nil)
       dup._call(env)
     end
 
@@ -123,9 +125,8 @@ module Rack
       ##                            the presence or absence of the
       ##                            appropriate HTTP header in the
       ##                            request. See
-      ##                            <a href="https://tools.ietf.org/html/rfc3875#section-4.1.18">
-      ##                            RFC3875 section 4.1.18</a> for
-      ##                            specific behavior.
+      ##                            {RFC3875 section 4.1.18}[https://tools.ietf.org/html/rfc3875#section-4.1.18]
+      ##                            for specific behavior.
 
       ## In addition to this, the Rack environment must include these
       ## Rack-specific variables:
@@ -263,7 +264,7 @@ module Rack
       ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
       ## (use the versions without <tt>HTTP_</tt>).
       %w[HTTP_CONTENT_TYPE HTTP_CONTENT_LENGTH].each { |header|
-        assert("env contains #{header}, must use #{header[5,-1]}") {
+        assert("env contains #{header}, must use #{header[5, -1]}") {
           not env.include? header
         }
       }
@@ -626,15 +627,17 @@ module Rack
       assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
          header.respond_to? :each
       }
-      header.each { |key, value|
-        ## Special headers starting "rack." are for communicating with the
-        ## server, and must not be sent back to the client.
-        next if key =~ /^rack\..+$/
 
+      header.each { |key, value|
         ## The header keys must be Strings.
         assert("header key must be a string, was #{key.class}") {
           key.kind_of? String
         }
+
+        ## Special headers starting "rack." are for communicating with the
+        ## server, and must not be sent back to the client.
+        next if key =~ /^rack\..+$/
+
         ## The header must not contain a +Status+ key.
         assert("header must not contain Status") { key.downcase != "status" }
         ## The header must conform to RFC7230 token specification, i.e. cannot
@@ -662,7 +665,7 @@ module Rack
         ## 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
+            not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
           }
           return
         end
@@ -676,7 +679,7 @@ module Rack
           ## There must not be a <tt>Content-Length</tt> header when the
           ## +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
+            not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
           }
           @content_length = value
         end
diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb
index 4d6e39f2bb567744a66de946892336c69b7682c9..77b607c3180d3cbde0c007e1d08fdf2198b10971 100644
--- a/lib/rack/lobster.rb
+++ b/lib/rack/lobster.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'zlib'
 
 require 'rack/request'
@@ -25,8 +27,8 @@ module Rack
       content = ["<title>Lobstericious!</title>",
                  "<pre>", lobster, "</pre>",
                  "<a href='#{href}'>flip!</a>"]
-      length = content.inject(0) { |a,e| a+e.size }.to_s
-      [200, {CONTENT_TYPE => "text/html", CONTENT_LENGTH => length}, content]
+      length = content.inject(0) { |a, e| a + e.size }.to_s
+      [200, { CONTENT_TYPE => "text/html", CONTENT_LENGTH => length }, content]
     }
 
     def call(env)
@@ -37,8 +39,8 @@ module Rack
             gsub('\\', 'TEMP').
             gsub('/', '\\').
             gsub('TEMP', '/').
-            gsub('{','}').
-            gsub('(',')')
+            gsub('{', '}').
+            gsub('(', ')')
         end.join("\n")
         href = "?flip=right"
       elsif req.GET["flip"] == "crash"
@@ -65,6 +67,6 @@ if $0 == __FILE__
   require 'rack'
   require 'rack/show_exceptions'
   Rack::Server.start(
-    :app => Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), :Port => 9292
+    app: Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), Port: 9292
   )
 end
diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb
index b5a41e8e1ad0b8c3ce4e854f94504dd4e45e58b5..96366cd306f7857cd8b1df0f81879638353ad763 100644
--- a/lib/rack/lock.rb
+++ b/lib/rack/lock.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'thread'
 require 'rack/body_proxy'
 
diff --git a/lib/rack/logger.rb b/lib/rack/logger.rb
index 01fc321c7212ffab8c454d0eb1105a1c457a069f..6c4bede0cf37eedf036dfd8290f0df43c2b10c13 100644
--- a/lib/rack/logger.rb
+++ b/lib/rack/logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'logger'
 
 module Rack
diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb
index 7e6cd3a85ae9bc7c8ba360f5d42babf21f6770dc..41937c9947e63aee85a2e4d8b48860bcce617ee0 100644
--- a/lib/rack/media_type.rb
+++ b/lib/rack/media_type.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   # Rack::MediaType parse media type and parameters out of content_type string
 
@@ -13,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
@@ -23,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/method_override.rb b/lib/rack/method_override.rb
index 14debe92b06fb544ac19b2f7e4ff99e91cfb3a7a..453901fc60692f43ac042e1f50e411d77b9df60d 100644
--- a/lib/rack/method_override.rb
+++ b/lib/rack/method_override.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
 module Rack
   class MethodOverride
     HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
 
-    METHOD_OVERRIDE_PARAM_KEY = "_method".freeze
-    HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE".freeze
+    METHOD_OVERRIDE_PARAM_KEY = "_method"
+    HTTP_METHOD_OVERRIDE_HEADER = "HTTP_X_HTTP_METHOD_OVERRIDE"
     ALLOWED_METHODS = %w[POST]
 
     def initialize(app)
diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb
index d82dc1319c937817ef72c33f738a4c3a31d13a6f..f6c02c1fd6ad871eb09e4bf3783a37c8dc6b91d0 100644
--- a/lib/rack/mime.rb
+++ b/lib/rack/mime.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Mime
     # Returns String with mime type if found, otherwise use +fallback+.
@@ -13,7 +15,7 @@ module Rack
     # This is a shortcut for:
     #     Rack::Mime::MIME_TYPES.fetch('.foo', 'application/octet-stream')
 
-    def mime_type(ext, fallback='application/octet-stream')
+    def mime_type(ext, fallback = 'application/octet-stream')
       MIME_TYPES.fetch(ext.to_s.downcase, fallback)
     end
     module_function :mime_type
@@ -306,6 +308,7 @@ module Rack
       ".lvp"       => "audio/vnd.lucent.voice",
       ".lwp"       => "application/vnd.lotus-wordpro",
       ".m3u"       => "audio/x-mpegurl",
+      ".m3u8"      => "application/x-mpegurl",
       ".m4a"       => "audio/mp4a-latm",
       ".m4v"       => "video/mp4",
       ".ma"        => "application/mathematica",
@@ -343,6 +346,7 @@ module Rack
       ".mp4s"      => "application/mp4",
       ".mp4v"      => "video/mp4",
       ".mpc"       => "application/vnd.mophun.certificate",
+      ".mpd"       => "application/dash+xml",
       ".mpeg"      => "video/mpeg",
       ".mpg"       => "video/mpeg",
       ".mpga"      => "audio/mpeg",
@@ -542,6 +546,7 @@ module Rack
       ".spp"       => "application/scvp-vp-response",
       ".spq"       => "application/scvp-vp-request",
       ".src"       => "application/x-wais-source",
+      ".srt"       => "text/srt",
       ".srx"       => "application/sparql-results+xml",
       ".sse"       => "application/vnd.kodak-descriptor",
       ".ssf"       => "application/vnd.epson.ssf",
@@ -576,6 +581,7 @@ module Rack
       ".tr"        => "text/troff",
       ".tra"       => "application/vnd.trueapp",
       ".trm"       => "application/x-msterminal",
+      ".ts"        => "video/mp2t",
       ".tsv"       => "text/tab-separated-values",
       ".ttf"       => "application/octet-stream",
       ".twd"       => "application/vnd.simtech-mindmapper",
@@ -600,9 +606,11 @@ module Rack
       ".vrml"      => "model/vrml",
       ".vsd"       => "application/vnd.visio",
       ".vsf"       => "application/vnd.vsf",
+      ".vtt"       => "text/vtt",
       ".vtu"       => "model/vnd.vtu",
       ".vxml"      => "application/voicexml+xml",
       ".war"       => "application/java-archive",
+      ".wasm"      => "application/wasm",
       ".wav"       => "audio/x-wav",
       ".wax"       => "audio/x-ms-wax",
       ".wbmp"      => "image/vnd.wap.wbmp",
diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb
index 10070f11380c09ab08ce34b07e676f1d248f2db4..3feaedd91882e8828d41061914f59b7a453519c7 100644
--- a/lib/rack/mock.rb
+++ b/lib/rack/mock.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
 require 'uri'
 require 'stringio'
 require 'rack'
 require 'rack/lint'
 require 'rack/utils'
 require 'rack/response'
+require 'cgi/cookie'
 
 module Rack
   # Rack::MockRequest helps testing your Rack application without
@@ -53,16 +56,16 @@ module Rack
       @app = app
     end
 
-    def get(uri, opts={})     request(GET, uri, opts)     end
-    def post(uri, opts={})    request(POST, uri, opts)    end
-    def put(uri, opts={})     request(PUT, uri, opts)     end
-    def patch(uri, opts={})   request(PATCH, uri, opts)   end
-    def delete(uri, opts={})  request(DELETE, uri, opts)  end
-    def head(uri, opts={})    request(HEAD, uri, opts)    end
-    def options(uri, opts={}) request(OPTIONS, uri, opts) end
+    def get(uri, opts = {})     request(GET, uri, opts)     end
+    def post(uri, opts = {})    request(POST, uri, opts)    end
+    def put(uri, opts = {})     request(PUT, uri, opts)     end
+    def patch(uri, opts = {})   request(PATCH, uri, opts)   end
+    def delete(uri, opts = {})  request(DELETE, uri, opts)  end
+    def head(uri, opts = {})    request(HEAD, uri, opts)    end
+    def options(uri, opts = {}) request(OPTIONS, uri, opts) end
 
-    def request(method=GET, uri="", opts={})
-      env = self.class.env_for(uri, opts.merge(:method => method))
+    def request(method = GET, uri = "", opts = {})
+      env = self.class.env_for(uri, opts.merge(method: method))
 
       if opts[:lint]
         app = Rack::Lint.new(@app)
@@ -71,7 +74,7 @@ module Rack
       end
 
       errors = env[RACK_ERRORS]
-      status, headers, body  = app.call(env)
+      status, headers, body = app.call(env)
       MockResponse.new(status, headers, body, errors)
     ensure
       body.close if body.respond_to?(:close)
@@ -85,7 +88,7 @@ module Rack
     end
 
     # Return the Rack environment used for a request to +uri+.
-    def self.env_for(uri="", opts={})
+    def self.env_for(uri = "", opts = {})
       uri = parse_uri_rfc2396(uri)
       uri.path = "/#{uri.path}" unless uri.path[0] == ?/
 
@@ -139,7 +142,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
@@ -155,16 +158,19 @@ module Rack
 
   class MockResponse < Rack::Response
     # Headers
-    attr_reader :original_headers
+    attr_reader :original_headers, :cookies
 
     # Errors
     attr_accessor :errors
 
-    def initialize(status, headers, body, errors=StringIO.new(""))
+    def initialize(status, headers, body, errors = StringIO.new(""))
       @original_headers = headers
       @errors           = errors.string if errors.respond_to?(:string)
+      @cookies = parse_cookies_from_header
 
       super(body, status, headers)
+
+      buffered_body!
     end
 
     def =~(other)
@@ -186,11 +192,64 @@ module Rack
       #     ...
       #     res.body.should == "foo!"
       #   end
-      super.join
+      buffer = String.new
+
+      super.each do |chunk|
+        buffer << chunk
+      end
+
+      return buffer
     end
 
     def empty?
       [201, 204, 304].include? status
     end
+
+    def cookie(name)
+      cookies.fetch(name, nil)
+    end
+
+    private
+
+    def parse_cookies_from_header
+      cookies = Hash.new
+      if original_headers.has_key? 'Set-Cookie'
+        set_cookie_header = original_headers.fetch('Set-Cookie')
+        set_cookie_header.split("\n").each do |cookie|
+          cookie_name, cookie_filling = cookie.split('=', 2)
+          cookie_attributes = identify_cookie_attributes cookie_filling
+          parsed_cookie = CGI::Cookie.new(
+            'name' => cookie_name.strip,
+            'value' => cookie_attributes.fetch('value'),
+            'path' => cookie_attributes.fetch('path', nil),
+            'domain' => cookie_attributes.fetch('domain', nil),
+            'expires' => cookie_attributes.fetch('expires', nil),
+            'secure' => cookie_attributes.fetch('secure', false)
+          )
+          cookies.store(cookie_name, parsed_cookie)
+        end
+      end
+      cookies
+    end
+
+    def identify_cookie_attributes(cookie_filling)
+      cookie_bits = cookie_filling.split(';')
+      cookie_attributes = Hash.new
+      cookie_attributes.store('value', cookie_bits[0].strip)
+      cookie_bits.each do |bit|
+        if bit.include? '='
+          cookie_attribute, attribute_value = bit.split('=')
+          cookie_attributes.store(cookie_attribute.strip, attribute_value.strip)
+          if cookie_attribute.include? 'max-age'
+            cookie_attributes.store('expires', Time.now + attribute_value.strip.to_i)
+          end
+        end
+        if bit.include? 'secure'
+          cookie_attributes.store('secure', true)
+        end
+      end
+      cookie_attributes
+    end
+
   end
 end
diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb
index db59ee59ad7b8a27810e810674d9dfa09da880b5..bd91f43f4fcaeacf8c6383c376f4421e278dbdfb 100644
--- a/lib/rack/multipart.rb
+++ b/lib/rack/multipart.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/multipart/parser'
 
 module Rack
@@ -14,10 +16,10 @@ module Rack
     TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
     CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
     VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
-    BROKEN_QUOTED = /^#{CONDISP}.*;\sfilename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
-    BROKEN_UNQUOTED = /^#{CONDISP}.*;\sfilename=(#{TOKEN})/i
+    BROKEN_QUOTED = /^#{CONDISP}.*;\s*filename="(.*?)"(?:\s*$|\s*;\s*#{TOKEN}=)/i
+    BROKEN_UNQUOTED = /^#{CONDISP}.*;\s*filename=(#{TOKEN})/i
     MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/ni
-    MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*\s+name=(#{VALUE})/ni
+    MULTIPART_CONTENT_DISPOSITION = /Content-Disposition:.*;\s*name=(#{VALUE})/ni
     MULTIPART_CONTENT_ID = /Content-ID:\s*([^#{EOL}]*)/ni
     # Updated definitions from RFC 2231
     ATTRIBUTE_CHAR = %r{[^ \t\v\n\r)(><@,;:\\"/\[\]?='*%]}
diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb
index f0b70a8d63107ae345a2adbc4eef9de57d28c50c..9ed2bb07cd4f35946d31937ada9ec67f93bf434c 100644
--- a/lib/rack/multipart/generator.rb
+++ b/lib/rack/multipart/generator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Multipart
     class Generator
@@ -27,21 +29,18 @@ module Rack
 
       private
       def multipart?
-        multipart = false
-
         query = lambda { |value|
           case value
           when Array
-            value.each(&query)
+            value.any?(&query)
           when Hash
-            value.values.each(&query)
+            value.values.any?(&query)
           when Rack::Multipart::UploadedFile
-            multipart = true
+            true
           end
         }
-        @params.values.each(&query)
 
-        multipart
+        @params.values.any?(&query)
       end
 
       def flattened_params
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index a19d6ea32f38d29ea3e7fa50978afd96a7390542..3e30a6b4ac250ab59d13c73c4584720d2096911b 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -1,16 +1,24 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
+require 'strscan'
+require 'rack/core_ext/regexp'
 
 module Rack
   module Multipart
     class MultipartPartLimitError < Errno::EMFILE; end
 
     class Parser
-      BUFSIZE = 16384
+      using ::Rack::RegexpExtensions
+
+      BUFSIZE = 1_048_576
       TEXT_PLAIN = "text/plain"
       TEMPFILE_FACTORY = lambda { |filename, content_type|
-        Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
+        Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0", '%00'))])
       }
 
+      BOUNDARY_REGEX = /\A([^\n]*(?:\n|\Z))/
+
       class BoundedIO # :nodoc:
         def initialize(io, content_length)
           @io             = io
@@ -18,15 +26,15 @@ module Rack
           @cursor = 0
         end
 
-        def read(size)
+        def read(size, outbuf = nil)
           return if @cursor >= @content_length
 
           left = @content_length - @cursor
 
           str = if left < size
-                  @io.read left
+                  @io.read left, outbuf
                 else
-                  @io.read size
+                  @io.read size, outbuf
                 end
 
           if str
@@ -39,8 +47,6 @@ module Rack
           str
         end
 
-        def eof?; @content_length == @cursor; end
-
         def rewind
           @io.rewind
         end
@@ -63,13 +69,14 @@ module Rack
         return EMPTY unless boundary
 
         io = BoundedIO.new(io, content_length) if content_length
+        outbuf = String.new
 
         parser = new(boundary, tmpfile, bufsize, qp)
-        parser.on_read io.read(bufsize), io.eof?
+        parser.on_read io.read(bufsize, outbuf)
 
         loop do
           break if parser.state == :DONE
-          parser.on_read io.read(bufsize), io.eof?
+          parser.on_read io.read(bufsize, outbuf)
         end
 
         io.rewind
@@ -92,14 +99,14 @@ module Rack
               # those which give the lone filename.
               fn = filename.split(/[\/\\]/).last
 
-              data = {:filename => fn, :type => content_type,
-                      :name => name, :tempfile => body, :head => head}
+              data = { filename: fn, type: content_type,
+                      name: name, tempfile: body, head: head }
             elsif !filename && content_type && body.is_a?(IO)
               body.rewind
 
               # Generic multipart cases, not coming from a form
-              data = {:type => content_type,
-                      :name => name, :tempfile => body, :head => head}
+              data = { type: content_type,
+                      name: name, tempfile: body, head: head }
             end
 
             yield data
@@ -140,6 +147,7 @@ module Rack
           end
 
           @mime_parts[mime_index] = klass.new(body, head, filename, content_type, name)
+
           check_open_files
         end
 
@@ -165,25 +173,26 @@ module Rack
       attr_reader :state
 
       def initialize(boundary, tempfile, bufsize, query_parser)
-        @buf            = String.new
-
         @query_parser   = query_parser
         @params         = query_parser.make_params
         @boundary       = "--#{boundary}"
         @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
         @mime_index = 0
         @collector = Collector.new tempfile
+
+        @sbuf = StringScanner.new("".dup)
+        @body_regex = /(?:#{EOL})?#{Regexp.quote(@boundary)}(?:#{EOL}|--)/m
+        @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
+        @head_regex = /(.*?#{EOL})#{EOL}/m
       end
 
-      def on_read content, eof
-        handle_empty_content!(content, eof)
-        @buf << content
+      def on_read content
+        handle_empty_content!(content)
+        @sbuf.concat content
         run_parser
       end
 
@@ -194,7 +203,6 @@ module Rack
             @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
           end
         end
-
         MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
       end
 
@@ -221,7 +229,7 @@ module Rack
         if consume_boundary
           @state = :MIME_HEAD
         else
-          raise EOFError, "bad content body" if @buf.bytesize >= @bufsize
+          raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
           :want_read
         end
       end
@@ -229,19 +237,16 @@ module Rack
       def handle_consume_token
         tok = consume_boundary
         # break if we're at the end of a buffer, but not if it is the end of a field
-        if tok == :END_BOUNDARY || (@buf.empty? && tok != :BOUNDARY)
-          @state = :DONE
+        @state = if tok == :END_BOUNDARY || (@sbuf.eos? && tok != :BOUNDARY)
+          :DONE
         else
-          @state = :MIME_HEAD
+          :MIME_HEAD
         end
       end
 
       def handle_mime_head
-        if @buf.index(EOL + EOL)
-          i = @buf.index(EOL+EOL)
-          head = @buf.slice!(0, i+2) # First \r\n
-          @buf.slice!(0, 2)          # Second \r\n
-
+        if @sbuf.scan_until(@head_regex)
+          head = @sbuf[1]
           content_type = head[MULTIPART_CONTENT_TYPE, 1]
           if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
             name = Rack::Auth::Digest::Params::dequote(name)
@@ -252,7 +257,7 @@ module Rack
           filename = get_filename(head)
 
           if name.nil? || name.empty?
-            name = filename || "#{content_type || TEXT_PLAIN}[]"
+            name = filename || "#{content_type || TEXT_PLAIN}[]".dup
           end
 
           @collector.on_mime_head @mime_index, head, filename, content_type, name
@@ -263,16 +268,19 @@ module Rack
       end
 
       def handle_mime_body
-        if i = @buf.index(rx)
-          # Save the rest.
-          @collector.on_mime_body @mime_index, @buf.slice!(0, i)
-          @buf.slice!(0, 2) # Remove \r\n after the content
+        if (body_with_boundary = @sbuf.check_until(@body_regex)) # check but do not advance the pointer yet
+          body = body_with_boundary.sub(/#{@body_regex}\z/m, '') # remove the boundary from the string
+          @collector.on_mime_body @mime_index, body
+          @sbuf.pos += body.length + 2 # skip \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)
+          # Save what we have so far
+          if @rx_max_size < @sbuf.rest_size
+            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
@@ -280,16 +288,13 @@ module Rack
 
       def full_boundary; @full_boundary; end
 
-      def rx; @rx; end
-
       def consume_boundary
-        while @buf.gsub!(/\A([^\n]*(?:\n|\Z))/, '')
-          read_buffer = $1
+        while read_buffer = @sbuf.scan_until(BOUNDARY_REGEX)
           case read_buffer.strip
           when full_boundary then return :BOUNDARY
           when @end_boundary then return :END_BOUNDARY
           end
-          return if @buf.empty?
+          return if @sbuf.eos?
         end
       end
 
@@ -310,8 +315,8 @@ module Rack
 
         return unless filename
 
-        if filename.scan(/%.?.?/).all? { |s| s =~ /%[0-9a-fA-F]{2}/ }
-          filename = Utils.unescape(filename)
+        if filename.scan(/%.?.?/).all? { |s| /%[0-9a-fA-F]{2}/.match?(s) }
+          filename = Utils.unescape_path(filename)
         end
 
         filename.scrub!
@@ -327,7 +332,7 @@ module Rack
         filename
       end
 
-      CHARSET   = "charset"
+      CHARSET = "charset"
 
       def tag_multipart_encoding(filename, content_type, name, body)
         name = name.to_s
@@ -344,10 +349,10 @@ module Rack
           if TEXT_PLAIN == type_subtype
             rest         = list.drop 1
             rest.each do |param|
-              k,v = param.split('=', 2)
+              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
@@ -357,11 +362,9 @@ module Rack
         body.force_encoding(encoding)
       end
 
-
-      def handle_empty_content!(content, eof)
+      def handle_empty_content!(content)
         if content.nil? || content.empty?
-          raise EOFError if eof
-          return true
+          raise EOFError
         end
       end
     end
diff --git a/lib/rack/multipart/uploaded_file.rb b/lib/rack/multipart/uploaded_file.rb
index 924b1f089b26a6019348b0c095cf5e500f318293..d01f2d6f32fc414ac6af533f30723d8a0634bb3d 100644
--- a/lib/rack/multipart/uploaded_file.rb
+++ b/lib/rack/multipart/uploaded_file.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Multipart
     class UploadedFile
diff --git a/lib/rack/null_logger.rb b/lib/rack/null_logger.rb
index abc61206298e3fc34cf2cd33e1440b761ed52fe6..3eff73d683fd336dca8f436b9f3cb7a4a6eee5cd 100644
--- a/lib/rack/null_logger.rb
+++ b/lib/rack/null_logger.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   class NullLogger
     def initialize(app)
diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb
index be74bc069cca3b36ea235d2d4cb73b9c27ceed50..2a4eb24496d71c5b0e29c3614158826e1368c3df 100644
--- a/lib/rack/query_parser.rb
+++ b/lib/rack/query_parser.rb
@@ -1,5 +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 }
 
@@ -36,7 +42,7 @@ module Rack
 
       (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
         next if p.empty?
-        k, v = p.split('='.freeze, 2).map!(&unescaper)
+        k, v = p.split('=', 2).map!(&unescaper)
 
         if cur = params[k]
           if cur.class == Array
@@ -49,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
@@ -61,13 +67,13 @@ module Rack
       return {} if qs.nil? || qs.empty?
       params = make_params
 
-      (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
-        k, v = p.split('='.freeze, 2).map! { |s| unescape(s) }
+      qs.split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
+        k, v = p.split('=', 2).map! { |s| unescape(s) }
 
         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
@@ -79,22 +85,22 @@ module Rack
       raise RangeError if depth <= 0
 
       name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
-      k = $1 || ''.freeze
-      after = $' || ''.freeze
+      k = $1 || ''
+      after = $' || ''
 
       if k.empty?
-        if !v.nil? && name == "[]".freeze
+        if !v.nil? && name == "[]"
           return Array(v)
         else
           return
         end
       end
 
-      if after == ''.freeze
+      if after == ''
         params[k] = v
-      elsif after == "[".freeze
+      elsif after == "["
         params[name] = v
-      elsif after == "[]".freeze
+      elsif after == "[]"
         params[k] ||= []
         raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
         params[k] << v
@@ -135,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 == ''
@@ -171,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/recursive.rb b/lib/rack/recursive.rb
index 7645d284a8a64a4ae23e2c1923d3992c7c92ac51..6a94ca83d39bcfaf1bcdb39cdf0d4431a76ff763 100644
--- a/lib/rack/recursive.rb
+++ b/lib/rack/recursive.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'uri'
 
 module Rack
@@ -10,14 +12,14 @@ module Rack
   class ForwardRequest < Exception
     attr_reader :url, :env
 
-    def initialize(url, env={})
+    def initialize(url, env = {})
       @url = URI(url)
       @env = env
 
-      @env[PATH_INFO] =       @url.path
-      @env[QUERY_STRING] =    @url.query  if @url.query
-      @env[HTTP_HOST] =       @url.host   if @url.host
-      @env["HTTP_PORT"] =     @url.port   if @url.port
+      @env[PATH_INFO]       = @url.path
+      @env[QUERY_STRING]    = @url.query  if @url.query
+      @env[HTTP_HOST]       = @url.host   if @url.host
+      @env["HTTP_PORT"]     = @url.port   if @url.port
       @env[RACK_URL_SCHEME] = @url.scheme if @url.scheme
 
       super "forwarding to #{url}"
diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb
index 296dd6a1fb351dda4371ce672262876846c0f257..e23ed1fbea7a31f1b28a2ede83fc5d9ea20942d8 100644
--- a/lib/rack/reloader.rb
+++ b/lib/rack/reloader.rb
@@ -1,9 +1,13 @@
-#          Copyright (c) 2009 Michael Fellinger m.fellinger@gmail.com
-#       Rack::Reloader is subject to the terms of an MIT-style license.
-#      See COPYING or http://www.opensource.org/licenses/mit-license.php.
+# frozen_string_literal: true
+
+# Copyright (C) 2009-2018 Michael Fellinger <m.fellinger@gmail.com>
+# Rack::Reloader is subject to the terms of an MIT-style license.
+# See MIT-LICENSE or https://opensource.org/licenses/MIT.
 
 require 'pathname'
 
+require_relative 'core_ext/regexp'
+
 module Rack
 
   # High performant source reloader
@@ -20,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
@@ -69,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 6307b6142cf800a0f8e5e9cabe51ed8236cb4c91..54ea86c4f6878d051ce56a6bfdc809cbfd2c80ae 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
 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
@@ -11,7 +15,18 @@ module Rack
   #   req.params["data"]
 
   class Request
-    SCHEME_WHITELIST = %w(https http).freeze
+    using ::Rack::RegexpExtensions
+
+    class << self
+      attr_accessor :ip_filter
+    end
+
+    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)
+      deprecate_constant :SCHEME_WHITELIST
+    end
 
     def initialize(env)
       @params = nil
@@ -100,7 +115,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',
@@ -108,7 +123,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',
@@ -119,11 +134,11 @@ module Rack
       # to include the port in a generated URI.
       DEFAULT_PORTS = { 'http' => 80, 'https' => 443, 'coffee' => 80 }
 
-      HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'.freeze
-      HTTP_X_FORWARDED_PROTO  = 'HTTP_X_FORWARDED_PROTO'.freeze
-      HTTP_X_FORWARDED_HOST   = 'HTTP_X_FORWARDED_HOST'.freeze
-      HTTP_X_FORWARDED_PORT   = 'HTTP_X_FORWARDED_PORT'.freeze
-      HTTP_X_FORWARDED_SSL    = 'HTTP_X_FORWARDED_SSL'.freeze
+      HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
+      HTTP_X_FORWARDED_PROTO  = 'HTTP_X_FORWARDED_PROTO'
+      HTTP_X_FORWARDED_HOST   = 'HTTP_X_FORWARDED_HOST'
+      HTTP_X_FORWARDED_PORT   = 'HTTP_X_FORWARDED_PORT'
+      HTTP_X_FORWARDED_SSL    = 'HTTP_X_FORWARDED_SSL'
 
       def body;            get_header(RACK_INPUT)                         end
       def script_name;     get_header(SCRIPT_NAME).to_s                   end
@@ -159,10 +174,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
@@ -208,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
@@ -232,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
@@ -260,8 +280,9 @@ module Rack
         return remote_addrs.first if remote_addrs.any?
 
         forwarded_ips = split_ip_addresses(get_header('HTTP_X_FORWARDED_FOR'))
+          .map { |ip| strip_port(ip) }
 
-        return reject_trusted_ip_addresses(forwarded_ips).last || get_header("REMOTE_ADDR")
+        return reject_trusted_ip_addresses(forwarded_ips).last || forwarded_ips.first || get_header("REMOTE_ADDR")
       end
 
       # The media type (type/subtype) portion of the CONTENT_TYPE header
@@ -337,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, '&')
@@ -386,12 +407,13 @@ 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
         url = "#{scheme}://#{host}"
-        url << ":#{port}" if port != DEFAULT_PORTS[scheme]
+        url = "#{url}:#{port}" if port != DEFAULT_PORTS[scheme]
         url
       end
 
@@ -417,7 +439,7 @@ module Rack
       end
 
       def trusted_proxy?(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
+        Rack::Request.ip_filter.call(ip)
       end
 
       # shortcut for <tt>request.params[key]</tt>
@@ -464,7 +486,7 @@ module Rack
         Utils.default_query_parser
       end
 
-      def parse_query(qs, d='&')
+      def parse_query(qs, d = '&')
         query_parser.parse_nested_query(qs, d)
       end
 
@@ -476,21 +498,52 @@ module Rack
         ip_addresses ? ip_addresses.strip.split(/[,\s]+/) : []
       end
 
+      def strip_port(ip_address)
+        # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
+        # returns: "2001:db8:cafe::17"
+        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"
+        sep = ip_address.index(':')
+        if (sep && ip_address.count(':') == 1)
+          return ip_address[0, sep]
+        end
+
+        ip_address
+      end
+
       def reject_trusted_ip_addresses(ip_addresses)
         ip_addresses.reject { |ip| trusted_proxy?(ip) }
       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
 
-        scheme_headers.each do |header|
-          return header if SCHEME_WHITELIST.include?(header)
+      def allowed_scheme(header)
+        header if ALLOWED_SCHEMES.include?(header)
+      end
+
+      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 a9f0c2a3617902fbe63c29e5400c852d0679b830..5c8dbab96f45de80e215afee5ef55d1bfb399e5f 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/request'
 require 'rack/utils'
 require 'rack/body_proxy'
@@ -23,32 +25,34 @@ module Rack
     attr_reader :header
     alias headers header
 
-    CHUNKED = 'chunked'.freeze
+    CHUNKED = 'chunked'
+    STATUS_WITH_NO_ENTITY_BODY = Utils::STATUS_WITH_NO_ENTITY_BODY
 
-    def initialize(body=[], status=200, header={})
+    def initialize(body = nil, 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
-      @length  = 0
+      @writer = self.method(:append)
 
-      @body = []
+      @block = nil
+      @length = 0
 
-      if body.respond_to? :to_str
-        write body.to_str
-      elsif body.respond_to?(:each)
-        body.each { |part|
-          write part.to_s
-        }
+      # Keep track of whether we have expanded the user supplied body.
+      if body.nil?
+        @body = []
+        @buffered = true
+      elsif body.respond_to?(:to_str)
+        @body = [body]
+        @buffered = true
       else
-        raise TypeError, "stringable or iterable required"
+        @body = body
+        @buffered = false
       end
 
-      yield self  if block_given?
+      yield self if block_given?
     end
 
-    def redirect(target, status=302)
+    def redirect(target, status = 302)
       self.status = status
       self.location = target
     end
@@ -58,41 +62,45 @@ module Rack
     end
 
     def finish(&block)
-      @block = block
-
-      if [204, 304].include?(status.to_i)
+      if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
         delete_header CONTENT_TYPE
         delete_header CONTENT_LENGTH
         close
         [status.to_i, header, []]
       else
-        [status.to_i, header, BodyProxy.new(self){}]
+        if block_given?
+          @block = block
+          [status.to_i, header, self]
+        else
+          [status.to_i, header, @body]
+        end
       end
     end
+
     alias to_a finish           # For *response
-    alias to_ary finish         # For implicit-splat on Ruby 1.9.2
 
     def each(&callback)
       @body.each(&callback)
-      @writer = callback
-      @block.call(self)  if @block
+      @buffered = true
+
+      if @block
+        @writer = callback
+        @block.call(self)
+      end
     end
 
     # Append to body and update Content-Length.
     #
     # NOTE: Do not mix #write and direct #body access!
     #
-    def write(str)
-      s = str.to_s
-      @length += s.bytesize unless chunked?
-      @writer.call s
+    def write(chunk)
+      buffered_body!
 
-      set_header(CONTENT_LENGTH, @length.to_s) unless chunked?
-      str
+      @writer.call(chunk.to_s)
     end
 
     def close
-      body.close if body.respond_to?(:close)
+      @body.close if @body.respond_to?(:close)
     end
 
     def empty?
@@ -184,7 +192,7 @@ module Rack
         set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
       end
 
-      def delete_cookie(key, value={})
+      def delete_cookie(key, value = {})
         set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
       end
 
@@ -211,6 +219,38 @@ module Rack
       def etag= v
         set_header ETAG, v
       end
+
+    protected
+
+      def buffered_body!
+        return if @buffered
+
+        if @body.is_a?(Array)
+          # The user supplied body was an array:
+          @body = @body.compact
+        else
+          # Turn the user supplied body into a buffered array:
+          body = @body
+          @body = Array.new
+
+          body.each do |part|
+            @writer.call(part.to_s)
+          end
+        end
+
+        @buffered = true
+      end
+
+      def append(chunk)
+        @body << chunk
+
+        unless chunked?
+          @length += chunk.bytesize
+          set_header(CONTENT_LENGTH, @length.to_s)
+        end
+
+        return chunk
+      end
     end
 
     include Helpers
diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb
index dd6b78439d58e01d03a54ab51b329d2da9287808..352bbeaa30f429bc200c68106aa6d09a5c04b358 100644
--- a/lib/rack/rewindable_input.rb
+++ b/lib/rack/rewindable_input.rb
@@ -1,4 +1,6 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: true
+
 require 'tempfile'
 require 'rack/utils'
 
@@ -40,7 +42,7 @@ module Rack
     end
 
     # Closes this RewindableInput object without closing the originally
-    # wrapped IO oject. Cleans up any temporary resources that this RewindableInput
+    # wrapped IO object. Cleans up any temporary resources that this RewindableInput
     # has created.
     #
     # This method may be called multiple times. It does nothing on subsequent calls.
@@ -72,7 +74,7 @@ module Rack
         @unlinked = true
       end
 
-      buffer = ""
+      buffer = "".dup
       while @io.read(1024 * 4, buffer)
         entire_buffer_written_out = false
         while !entire_buffer_written_out
diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb
index bb15bdb1bc5f6c066c10aff41cd84795186fbd4b..d2bca9e5e4992eb0088769c685d9a12a5f63f4ac 100644
--- a/lib/rack/runtime.rb
+++ b/lib/rack/runtime.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/utils'
 
 module Rack
@@ -8,8 +10,8 @@ module Rack
   # time, or before all the other middlewares to include time for them,
   # too.
   class Runtime
-    FORMAT_STRING = "%0.6f".freeze # :nodoc:
-    HEADER_NAME = "X-Runtime".freeze # :nodoc:
+    FORMAT_STRING = "%0.6f" # :nodoc:
+    HEADER_NAME = "X-Runtime" # :nodoc:
 
     def initialize(app, name = nil)
       @app = app
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index bdb7cf2fb7960b769d3920a41f0b2cc0ea947e63..3774b26067a420cab846a35fe3bab18320a9f3db 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -1,4 +1,6 @@
-require 'rack/file'
+# frozen_string_literal: true
+
+require 'rack/files'
 require 'rack/body_proxy'
 
 module Rack
@@ -14,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
@@ -53,7 +55,7 @@ module Rack
   # that it maps to. The middleware performs a simple substitution on the
   # resulting path.
   #
-  # See Also: http://wiki.codemongers.com/NginxXSendfile
+  # See Also: https://www.nginx.com/resources/wiki/start/topics/examples/xsendfile
   #
   # === lighttpd
   #
@@ -99,7 +101,7 @@ module Rack
   # will be matched with case indifference.
 
   class Sendfile
-    def initialize(app, variation=nil, mappings=[])
+    def initialize(app, variation = nil, mappings = [])
       @app = app
       @variation = variation
       @mappings = mappings.map do |internal, external|
@@ -115,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)
@@ -147,11 +150,15 @@ module Rack
     end
 
     def map_accel_path(env, path)
-      if mapping = @mappings.find { |internal,_| internal =~ path }
+      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 1f37aacb5eb65005f3b2b7a9faf36fdce7409d9a..6137f043b3c312f99d2e62e48ff33928e4190c18 100644
--- a/lib/rack/server.rb
+++ b/lib/rack/server.rb
@@ -1,10 +1,14 @@
+# frozen_string_literal: true
+
 require 'optparse'
 require 'fileutils'
 
+require_relative 'core_ext/regexp'
 
 module Rack
 
   class Server
+    using ::Rack::RegexpExtensions
 
     class Options
       def parse!(args)
@@ -21,10 +25,6 @@ module Rack
             lineno += 1
           }
 
-          opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
-            options[:builder] = line
-          }
-
           opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
             options[:debug] = true
           }
@@ -47,7 +47,11 @@ module Rack
 
           opts.separator ""
           opts.separator "Rack options:"
-          opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick/mongrel)") { |s|
+          opts.on("-b", "--builder BUILDER_LINE", "evaluate a BUILDER_LINE of code as a builder script") { |line|
+            options[:builder] = line
+          }
+
+          opts.on("-s", "--server SERVER", "serve using SERVER (thin/puma/webrick)") { |s|
             options[:server] = s
           }
 
@@ -77,6 +81,24 @@ module Rack
             options[:pid] = ::File.expand_path(f)
           }
 
+          opts.separator ""
+          opts.separator "Profiling options:"
+
+          opts.on("--heap HEAPFILE", "Build the application, then dump the heap to HEAPFILE") do |e|
+            options[:heapfile] = e
+          end
+
+          opts.on("--profile PROFILE", "Dump CPU or Memory profile to PROFILE (defaults to a tempfile)") do |e|
+            options[:profile_file] = e
+          end
+
+          opts.on("--profile-mode MODE", "Profile mode (cpu|wall|object)") do |e|
+            { cpu: true, wall: true, object: true }.fetch(e.to_sym) do
+              raise OptionParser::InvalidOption, "unknown profile mode: #{e}"
+            end
+            options[:profile_mode] = e.to_sym
+          end
+
           opts.separator ""
           opts.separator "Common options:"
 
@@ -114,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
@@ -152,7 +174,9 @@ module Rack
 
     # Options may include:
     # * :app
-    #     a rack application to run (overrides :config)
+    #     a rack application to run (overrides :config and :builder)
+    # * :builder
+    #     a string to evaluate a Rack::Builder from
     # * :config
     #     a rackup configuration file path to load (.ru)
     # * :environment
@@ -182,6 +206,14 @@ module Rack
     #     add given paths to $LOAD_PATH
     # * :require
     #     require the given libraries
+    #
+    # Additional options for profiling app initialization include:
+    # * :heapfile
+    #     location for ObjectSpace.dump_all to write the output to
+    # * :profile_file
+    #     location for CPU/Memory (StackProf) profile output (defaults to a tempfile)
+    # * :profile_mode
+    #     StackProf profile mode (cpu|wall|object)
     def initialize(options = nil)
       @ignore_options = []
 
@@ -206,12 +238,12 @@ module Rack
       default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
 
       {
-        :environment => environment,
-        :pid         => nil,
-        :Port        => 9292,
-        :Host        => default_host,
-        :AccessLog   => [],
-        :config      => "config.ru"
+        environment: environment,
+        pid: nil,
+        Port: 9292,
+        Host: default_host,
+        AccessLog: [],
+        config: "config.ru"
       }
     end
 
@@ -222,21 +254,19 @@ 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
 
       def default_middleware_by_environment
-        m = Hash.new {|h,k| h[k] = []}
+        m = Hash.new {|h, k| h[k] = []}
         m["deployment"] = [
           [Rack::ContentLength],
-          [Rack::Chunked],
           logging_middleware,
           [Rack::TempfileReaper]
         ]
         m["development"] = [
           [Rack::ContentLength],
-          [Rack::Chunked],
           logging_middleware,
           [Rack::ShowExceptions],
           [Rack::Lint],
@@ -280,7 +310,9 @@ module Rack
 
       # Touch the wrapped app, so that the config.ru is loaded before
       # daemonization (i.e. before chdir, etc).
-      wrapped_app
+      handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
+        wrapped_app
+      end
 
       daemonize_app if options[:daemonize]
 
@@ -321,6 +353,44 @@ module Rack
         app
       end
 
+      def handle_profiling(heapfile, profile_mode, filename)
+        if heapfile
+          require "objspace"
+          ObjectSpace.trace_object_allocations_start
+          yield
+          GC.start
+          ::File.open(heapfile, "w") { |f| ObjectSpace.dump_all(output: f) }
+          exit
+        end
+
+        if profile_mode
+          require "stackprof"
+          require "tempfile"
+
+          make_profile_name(filename) do |filename|
+            ::File.open(filename, "w") do |f|
+              StackProf.run(mode: profile_mode, out: f) do
+                yield
+              end
+              puts "Profile written to: #{filename}"
+            end
+          end
+          exit
+        end
+
+        yield
+      end
+
+      def make_profile_name(filename)
+        if filename
+          yield filename
+        else
+          ::Dir::Tmpname.create("profile.dump") do |tmpname, _, _|
+            yield tmpname
+          end
+        end
+      end
+
       def build_app_from_string
         Rack::Builder.new_from_string(self.options[:builder])
       end
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
index 1bb8d5d060a8bce6a33218f71d97de23b9b9984f..20ef8b833e93d40f94dfffc6f00e1a9cb8c9aeb1 100644
--- a/lib/rack/session/abstract/id.rb
+++ b/lib/rack/session/abstract/id.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
 # bugrep: Andreas Zehnder
 
@@ -6,15 +8,54 @@ require 'time'
 require 'rack/request'
 require 'rack/response'
 require 'securerandom'
+require 'digest/sha2'
 
 module Rack
 
   module Session
 
+    class SessionId
+      ID_VERSION = 2
+
+      attr_reader :public_id
+
+      def initialize(public_id)
+        @public_id = public_id
+      end
+
+      def private_id
+        "#{ID_VERSION}::#{hash_sid(public_id)}"
+      end
+
+      alias :cookie_value :public_id
+      alias :to_s :public_id
+
+      def empty?; false; end
+      def inspect; public_id.inspect; end
+
+      private
+
+      def hash_sid(sid)
+        Digest::SHA256.hexdigest(sid)
+      end
+    end
+
     module Abstract
       # 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)
+
         include Enumerable
         attr_writer :id
 
@@ -57,7 +98,7 @@ module Rack
           @data[key.to_s]
         end
 
-        def fetch(key, default=Unspecified, &block)
+        def fetch(key, default = Unspecified, &block)
           load_for_read!
           if default == Unspecified
             @data.fetch(key.to_s, &block)
@@ -160,11 +201,7 @@ module Rack
         end
 
         def stringify_keys(other)
-          hash = {}
-          other.each do |key, value|
-            hash[key.to_s] = value
-          end
-          hash
+          other.to_hash.transform_keys(&:to_s)
         end
       end
 
@@ -199,22 +236,22 @@ module Rack
 
       class Persisted
         DEFAULT_OPTIONS = {
-          :key =>           RACK_SESSION,
-          :path =>          '/',
-          :domain =>        nil,
-          :expire_after =>  nil,
-          :secure =>        false,
-          :httponly =>      true,
-          :defer =>         false,
-          :renew =>         false,
-          :sidbits =>       128,
-          :cookie_only =>   true,
-          :secure_random => ::SecureRandom
+          key: RACK_SESSION,
+          path: '/',
+          domain: nil,
+          expire_after: nil,
+          secure: false,
+          httponly: true,
+          defer: false,
+          renew: false,
+          sidbits: 128,
+          cookie_only: true,
+          secure_random: ::SecureRandom
         }.freeze
 
         attr_reader :key, :default_options, :sid_secure
 
-        def initialize(app, options={})
+        def initialize(app, options = {})
           @app = app
           @default_options = self.class::DEFAULT_OPTIONS.merge(options)
           @key = @default_options.delete(:key)
@@ -226,7 +263,7 @@ module Rack
           context(env)
         end
 
-        def context(env, app=@app)
+        def context(env, app = @app)
           req = make_request env
           prepare_session(req)
           status, headers, body = app.call(req.env)
@@ -349,7 +386,7 @@ module Rack
 
           session.send(:load!) unless loaded_session?(session)
           session_id ||= session.id
-          session_data = session.to_hash.delete_if { |k,v| v.nil? }
+          session_data = session.to_hash.delete_if { |k, v| v.nil? }
 
           if not data = write_session(req, session_id, session_data, options)
             req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
@@ -357,7 +394,7 @@ module Rack
             req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
           else
             cookie = Hash.new
-            cookie[:value] = data
+            cookie[:value] = cookie_value(data)
             cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
             cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
             set_cookie(req, res, cookie.merge!(options))
@@ -365,6 +402,10 @@ module Rack
         end
         public :commit_session
 
+        def cookie_value(data)
+          data
+        end
+
         # 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.
 
@@ -406,6 +447,40 @@ module Rack
         end
       end
 
+      class PersistedSecure < Persisted
+        class SecureSessionHash < SessionHash
+          def [](key)
+            if key == "session_id"
+              load_for_read!
+              id.public_id if id
+            else
+              super
+            end
+          end
+        end
+
+        def generate_sid(*)
+          public_id = super
+
+          SessionId.new(public_id)
+        end
+
+        def extract_session_id(*)
+          public_id = super
+          public_id && SessionId.new(public_id)
+        end
+
+        private
+
+        def session_class
+          SecureSessionHash
+        end
+
+        def cookie_value(data)
+          data.cookie_value
+        end
+      end
+
       class ID < Persisted
         def self.inherited(klass)
           k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
index 71bb96f4f1150c046595db1105cfc141d65c098f..d110aee249ecec9d40e862e7a7e65185c7cd8899 100644
--- a/lib/rack/session/cookie.rb
+++ b/lib/rack/session/cookie.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
 require 'openssl'
 require 'zlib'
 require 'rack/request'
 require 'rack/response'
 require 'rack/session/abstract/id'
 require 'json'
+require 'base64'
 
 module Rack
 
@@ -45,15 +48,15 @@ module Rack
     #   })
     #
 
-    class Cookie < Abstract::Persisted
+    class Cookie < Abstract::PersistedSecure
       # Encode session cookies as Base64
       class Base64
         def encode(str)
-          [str].pack('m')
+          ::Base64.strict_encode64(str)
         end
 
         def decode(str)
-          str.unpack('m').first
+          ::Base64.decode64(str)
         end
 
         # Encode session cookies as Marshaled Base64 data
@@ -103,7 +106,7 @@ module Rack
 
       attr_reader :coder
 
-      def initialize(app, options={})
+      def initialize(app, options = {})
         @secrets = options.values_at(:secret, :old_secret).compact
         @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
 
@@ -116,8 +119,8 @@ module Rack
 
         Called from: #{caller[0]}.
         MSG
-        @coder  = options[:coder] ||= Base64::Marshal.new
-        super(app, options.merge!(:cookie_only => true))
+        @coder = options[:coder] ||= Base64::Marshal.new
+        super(app, options.merge!(cookie_only: true))
       end
 
       private
@@ -137,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
 
@@ -147,12 +148,21 @@ module Rack
         end
       end
 
-      def persistent_session_id!(data, sid=nil)
+      def persistent_session_id!(data, sid = nil)
         data ||= {}
         data["session_id"] ||= sid || generate_sid
         data
       end
 
+      class SessionId < DelegateClass(Session::SessionId)
+        attr_reader :cookie_value
+
+        def initialize(session_id, cookie_value)
+          super(session_id)
+          @cookie_value = cookie_value
+        end
+      end
+
       def write_session(req, session_id, session, options)
         session = session.merge("session_id" => session_id)
         session_data = coder.encode(session)
@@ -165,7 +175,7 @@ module Rack
           req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
           nil
         else
-          session_data
+          SessionId.new(session_id, session_data)
         end
       end
 
diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb
index 4cf5ea09e35c4e9035d00dd1879f610657c9bc54..6a601174075b257a061b754b4b12748efbda32d5 100644
--- a/lib/rack/session/memcache.rb
+++ b/lib/rack/session/memcache.rb
@@ -1,93 +1,10 @@
-# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
+# frozen_string_literal: true
 
-require 'rack/session/abstract/id'
-require 'memcache'
+require 'rack/session/dalli'
 
 module Rack
   module Session
-    # Rack::Session::Memcache provides simple cookie based session management.
-    # Session data is stored in memcached. The corresponding session key is
-    # maintained in the cookie.
-    # You may treat Session::Memcache as you would Session::Pool with the
-    # following caveats.
-    #
-    # * Setting :expire_after to 0 would note to the Memcache server to hang
-    #   onto the session data until it would drop it according to it's own
-    #   specifications. However, the cookie sent to the client would expire
-    #   immediately.
-    #
-    # Note that memcache does drop data before it may be listed to expire. For
-    # a full description of behaviour, please see memcache's documentation.
-
-    class Memcache < Abstract::ID
-      attr_reader :mutex, :pool
-
-      DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge \
-        :namespace => 'rack:session',
-        :memcache_server => 'localhost:11211'
-
-      def initialize(app, options={})
-        super
-
-        @mutex = Mutex.new
-        mserv = @default_options[:memcache_server]
-        mopts = @default_options.reject{|k,v| !MemCache::DEFAULT_OPTIONS.include? k }
-
-        @pool = options[:cache] || MemCache.new(mserv, mopts)
-        unless @pool.active? and @pool.servers.any?(&:alive?)
-          raise 'No memcache servers'
-        end
-      end
-
-      def generate_sid
-        loop do
-          sid = super
-          break sid unless @pool.get(sid, true)
-        end
-      end
-
-      def get_session(env, sid)
-        with_lock(env) do
-          unless sid and session = @pool.get(sid)
-            sid, session = generate_sid, {}
-            unless /^STORED/ =~ @pool.add(sid, session)
-              raise "Session collision on '#{sid.inspect}'"
-            end
-          end
-          [sid, session]
-        end
-      end
-
-      def set_session(env, session_id, new_session, options)
-        expiry = options[:expire_after]
-        expiry = expiry.nil? ? 0 : expiry + 1
-
-        with_lock(env) do
-          @pool.set session_id, new_session, expiry
-          session_id
-        end
-      end
-
-      def destroy_session(env, session_id, options)
-        with_lock(env) do
-          @pool.delete(session_id)
-          generate_sid unless options[:drop]
-        end
-      end
-
-      def with_lock(env)
-        @mutex.lock if env[RACK_MULTITHREAD]
-        yield
-      rescue MemCache::MemCacheError, Errno::ECONNREFUSED
-        if $VERBOSE
-          warn "#{self} is unable to find memcached server."
-          warn $!.inspect
-        end
-        raise
-      ensure
-        @mutex.unlock if @mutex.locked?
-      end
-
-    end
+    warn "Rack::Session::Memcache is deprecated, please use Rack::Session::Dalli from 'dalli' gem instead."
+    Memcache = Dalli
   end
 end
diff --git a/lib/rack/session/pool.rb b/lib/rack/session/pool.rb
index 4c9c25c7a0e248218ac6e2305a1d8f4b6497c304..f5b6265046f92d3a84c29a6b63e01643c89121ac 100644
--- a/lib/rack/session/pool.rb
+++ b/lib/rack/session/pool.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 # AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
 # THANKS:
 #   apeiros, for session id generation, expiry setup, and threadiness
@@ -24,11 +26,11 @@ module Rack
     #   )
     #   Rack::Handler::WEBrick.run sessioned
 
-    class Pool < Abstract::Persisted
+    class Pool < Abstract::PersistedSecure
       attr_reader :mutex, :pool
-      DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge :drop => false
+      DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge drop: false
 
-      def initialize(app, options={})
+      def initialize(app, options = {})
         super
         @pool = Hash.new
         @mutex = Mutex.new
@@ -37,15 +39,15 @@ module Rack
       def generate_sid
         loop do
           sid = super
-          break sid unless @pool.key? sid
+          break sid unless @pool.key? sid.private_id
         end
       end
 
       def find_session(req, sid)
         with_lock(req) do
-          unless sid and session = @pool[sid]
+          unless sid and session = get_session_with_fallback(sid)
             sid, session = generate_sid, {}
-            @pool.store sid, session
+            @pool.store sid.private_id, session
           end
           [sid, session]
         end
@@ -53,14 +55,15 @@ module Rack
 
       def write_session(req, session_id, new_session, options)
         with_lock(req) do
-          @pool.store session_id, new_session
+          @pool.store session_id.private_id, new_session
           session_id
         end
       end
 
       def delete_session(req, session_id, options)
         with_lock(req) do
-          @pool.delete(session_id)
+          @pool.delete(session_id.public_id)
+          @pool.delete(session_id.private_id)
           generate_sid unless options[:drop]
         end
       end
@@ -71,6 +74,12 @@ module Rack
       ensure
         @mutex.unlock if @mutex.locked?
       end
+
+      private
+
+      def get_session_with_fallback(sid)
+        @pool[sid.private_id] || @pool[sid.public_id]
+      end
     end
   end
 end
diff --git a/lib/rack/show_exceptions.rb b/lib/rack/show_exceptions.rb
index ef30fce4325fb2fb521d554fda35de8ac692a3a3..843af607afb717eab3445f3740d7b65909a4ef2b 100644
--- a/lib/rack/show_exceptions.rb
+++ b/lib/rack/show_exceptions.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'ostruct'
 require 'erb'
 require 'rack/request'
@@ -55,7 +57,7 @@ module Rack
     private :accepts_html?
 
     def dump_exception(exception)
-      string = "#{exception.class}: #{exception.message}\n"
+      string = "#{exception.class}: #{exception.message}\n".dup
       string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
       string
     end
@@ -77,13 +79,13 @@ module Rack
           frame.function = $4
 
           begin
-            lineno = frame.lineno-1
+            lineno = frame.lineno - 1
             lines = ::File.readlines(frame.filename)
-            frame.pre_context_lineno = [lineno-CONTEXT, 0].max
+            frame.pre_context_lineno = [lineno - CONTEXT, 0].max
             frame.pre_context = lines[frame.pre_context_lineno...lineno]
             frame.context_line = lines[lineno].chomp
-            frame.post_context_lineno = [lineno+CONTEXT, lines.size].min
-            frame.post_context = lines[lineno+1..frame.post_context_lineno]
+            frame.post_context_lineno = [lineno + CONTEXT, lines.size].min
+            frame.post_context = lines[lineno + 1..frame.post_context_lineno]
           rescue
           end
 
@@ -93,7 +95,11 @@ module Rack
         end
       }.compact
 
-      TEMPLATE.result(binding)
+      template.result(binding)
+    end
+
+    def template
+      TEMPLATE
     end
 
     def h(obj)                  # :nodoc:
@@ -107,8 +113,8 @@ module Rack
 
     # :stopdoc:
 
-    # adapted from Django <djangoproject.com>
-    # Copyright (c) 2005, the Lawrence Journal-World
+    # adapted from Django <www.djangoproject.com>
+    # Copyright (c) Django Software Foundation and individual contributors.
     # Used under the modified BSD license:
     # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
     TEMPLATE = ERB.new(<<-'HTML'.gsub(/^      /, ''))
@@ -363,7 +369,7 @@ module Rack
                 <% env.sort_by { |k, v| k.to_s }.each { |key, val| %>
                 <tr>
                   <td><%=h key %></td>
-                  <td class="code"><div><%=h val %></div></td>
+                  <td class="code"><div><%=h val.inspect %></div></td>
                 </tr>
                 <% } %>
             </tbody>
diff --git a/lib/rack/show_status.rb b/lib/rack/show_status.rb
index 54db8f4715599ed10668fcf0ed82a82807f212cd..3fdfca5e6f3b2109387b68131eb865f00ab80e54 100644
--- a/lib/rack/show_status.rb
+++ b/lib/rack/show_status.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'erb'
 require 'rack/request'
 require 'rack/utils'
@@ -52,8 +54,8 @@ module Rack
 
     # :stopdoc:
 
-# adapted from Django <djangoproject.com>
-# Copyright (c) 2005, the Lawrence Journal-World
+# adapted from Django <www.djangoproject.com>
+# Copyright (c) Django Software Foundation and individual contributors.
 # Used under the modified BSD license:
 # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
 TEMPLATE = <<'HTML'
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 17f47649422c91cacead9fea634f2d8abad3cb29..9a0017db942d03ce1ebbc2fb7eb6b33c555e5b34 100644
--- a/lib/rack/static.rb
+++ b/lib/rack/static.rb
@@ -1,11 +1,15 @@
-require "rack/file"
+# frozen_string_literal: true
+
+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:
@@ -82,8 +86,9 @@ module Rack
   #         ]
   #
   class Static
+    using ::Rack::RegexpExtensions
 
-    def initialize(app, options={})
+    def initialize(app, options = {})
       @app = app
       @urls = options[:urls] || ["/favicon.ico"]
       @index = options[:index]
@@ -93,13 +98,13 @@ module Rack
       # HTTP Headers
       @header_rules = options[:header_rules] || []
       # 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]
+      @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 && path =~ /\/$/
+      @index && route_file(path) && path.end_with?('/')
     end
 
     def overwrite_file_path(path)
@@ -120,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)
@@ -157,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/tempfile_reaper.rb b/lib/rack/tempfile_reaper.rb
index d829980619b2258a47b84589cd6bc5df012718da..73b6c1c8df2d2b76793b6dd24f0c13fdea6d9e36 100644
--- a/lib/rack/tempfile_reaper.rb
+++ b/lib/rack/tempfile_reaper.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rack/body_proxy'
 
 module Rack
diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb
index 510b4b5003e1d7758a624f499a99171b5c1ebbd0..c5d9c44f5392d7620e5166080d6c71dfb1b89892 100644
--- a/lib/rack/urlmap.rb
+++ b/lib/rack/urlmap.rb
@@ -1,3 +1,7 @@
+# frozen_string_literal: true
+
+require 'set'
+
 module Rack
   # Rack::URLMap takes a hash mapping urls or paths to apps, and
   # dispatches accordingly.  Support for HTTP/1.1 host names exists if
@@ -20,9 +24,11 @@ module Rack
     end
 
     def remap(map)
+      @known_hosts = Set[]
       @mapping = map.map { |location, app|
         if location =~ %r{\Ahttps?://(.*?)(/.*)}
           host, location = $1, $2
+          @known_hosts << host
         else
           host = nil
         end
@@ -50,10 +56,13 @@ module Rack
       is_same_server = casecmp?(http_host, server_name) ||
                        casecmp?(http_host, "#{server_name}:#{server_port}")
 
+      is_host_known = @known_hosts.include? http_host
+
       @mapping.each do |host, location, match, app|
         unless casecmp?(http_host, host) \
             || casecmp?(server_name, host) \
-            || (!host && is_same_server)
+            || (!host && is_same_server) \
+            || (!host && !is_host_known) # If we don't have a matching host, default to the first without a specified host
           next
         end
 
@@ -68,7 +77,7 @@ module Rack
         return app.call(env)
       end
 
-      [404, {CONTENT_TYPE => "text/plain", "X-Cascade" => "pass"}, ["Not Found: #{path}"]]
+      [404, { CONTENT_TYPE => "text/plain", "X-Cascade" => "pass" }, ["Not Found: #{path}"]]
 
     ensure
       env[PATH_INFO]   = path
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index c253f3cf24e4c94400114de897f4b337a62fc902..492f9bcfe245b52a25383bedd38dd351faec4a89 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -1,4 +1,6 @@
 # -*- encoding: binary -*-
+# frozen_string_literal: true
+
 require 'uri'
 require 'fileutils'
 require 'set'
@@ -6,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
@@ -118,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
@@ -132,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]
@@ -175,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
 
@@ -210,8 +215,12 @@ module Rack
       #   the Cookie header such that those with more specific Path attributes
       #   precede those with less specific.  Ordering with respect to other
       #   attributes (e.g., Domain) is unspecified.
-      cookies = parse_query(header, ';,') { |s| unescape(s) rescue s }
-      cookies.each_with_object({}) { |(k,v), hash| hash[k] = Array === v ? v.first : v }
+      return {} unless header
+      header.split(/[;,] */n).each_with_object({}) do |cookie, cookies|
+        next if cookie.empty?
+        key, value = cookie.split('=', 2)
+        cookies[key] = (unescape(value) rescue value) unless cookies.key?(key)
+      end
     end
     module_function :parse_cookies_header
 
@@ -221,41 +230,19 @@ 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 :none, 'None', :None
+            '; SameSite=None'
           when :lax, 'Lax', :Lax
-            '; SameSite=Lax'.freeze
+            '; SameSite=Lax'
           when true, :strict, 'Strict', :Strict
-            '; SameSite=Strict'.freeze
+            '; SameSite=Strict'
           else
             raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
           end
@@ -295,15 +282,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
@@ -321,9 +308,9 @@ module Rack
       new_header = make_delete_cookie_header(header, key, value)
 
       add_cookie_to_header(new_header, key,
-                 {:value => '', :path => nil, :domain => nil,
-                   :max_age => '0',
-                   :expires => Time.at(0) }.merge(value))
+                 { value: '', path: nil, domain: nil,
+                   max_age: '0',
+                   expires: Time.at(0) }.merge(value))
 
     end
     module_function :add_remove_cookie_to_header
@@ -364,7 +351,7 @@ module Rack
       ranges = []
       $1.split(/,\s*/).each do |range_spec|
         return nil  unless range_spec =~ /(\d*)-(\d*)/
-        r0,r1 = $1, $2
+        r0, r1 = $1, $2
         if r0.empty?
           return nil  if r1.empty?
           # suffix-byte-range-spec, represents trailing suffix of file
@@ -378,7 +365,7 @@ module Rack
           else
             r1 = r1.to_i
             return nil  if r1 < r0  # backwards range is syntactically invalid
-            r1 = size-1  if r1 >= size
+            r1 = size - 1  if r1 >= size
           end
         end
         ranges << (r0..r1)  if r0 <= r1
@@ -399,7 +386,7 @@ module Rack
       l = a.unpack("C*")
 
       r, i = 0, -1
-      b.each_byte { |v| r |= v ^ l[i+=1] }
+      b.each_byte { |v| r |= v ^ l[i += 1] }
       r == 0
     end
     module_function :secure_compare
@@ -425,19 +412,17 @@ module Rack
         self.class.new(@for, app)
       end
 
-      def context(env, app=@app)
+      def context(env, app = @app)
         recontext(app).call(env)
       end
     end
 
     # 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
-
-      def initialize(hash={})
+    #
+    # @api private
+    class HeaderHash < Hash # :nodoc:
+      def initialize(hash = {})
         super()
         @names = {}
         hash.each { |k, v| self[k] = v }
@@ -457,7 +442,7 @@ module Rack
 
       def to_hash
         hash = {}
-        each { |k,v| hash[k] = v }
+        each { |k, v| hash[k] = v }
         hash
       end
 
@@ -510,13 +495,14 @@ 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',
       102 => 'Processing',
+      103 => 'Early Hints',
       200 => 'OK',
       201 => 'Created',
       202 => 'Accepted',
@@ -533,6 +519,7 @@ module Rack
       303 => 'See Other',
       304 => 'Not Modified',
       305 => 'Use Proxy',
+      306 => '(Unused)',
       307 => 'Temporary Redirect',
       308 => 'Permanent Redirect',
       400 => 'Bad Request',
@@ -557,6 +544,7 @@ module Rack
       422 => 'Unprocessable Entity',
       423 => 'Locked',
       424 => 'Failed Dependency',
+      425 => 'Too Early',
       426 => 'Upgrade Required',
       428 => 'Precondition Required',
       429 => 'Too Many Requests',
@@ -571,12 +559,13 @@ module Rack
       506 => 'Variant Also Negotiates',
       507 => 'Insufficient Storage',
       508 => 'Loop Detected',
+      509 => 'Bandwidth Limit Exceeded',
       510 => 'Not Extended',
       511 => 'Network Authentication Required'
     }
 
     # Responses with HTTP status codes that should not have an entity body
-    STATUS_WITH_NO_ENTITY_BODY = Set.new((100..199).to_a << 204 << 304)
+    STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
 
     SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
       [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
@@ -584,7 +573,7 @@ module Rack
 
     def status_code(status)
       if status.is_a?(Symbol)
-        SYMBOL_TO_STATUS_CODE[status] || 500
+        SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
       else
         status.to_i
       end
@@ -605,11 +594,11 @@ 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
+    NULL_BYTE = "\0"
 
     def valid_path?(path)
       path.valid_encoding? && !path.include?(NULL_BYTE)
diff --git a/rack.gemspec b/rack.gemspec
index d7f01d188f72bb780c6fc6dae05db955dbe7d5ee..f7b13b17074078a4b14c6f3aef7f316283866e97 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
 Gem::Specification.new do |s|
-  s.name            = "rack"
+  s.name = "rack"
   s.version = File.read('lib/rack.rb')[/RELEASE += +([\"\'])([\d][\w\.]+)\1/, 2]
   s.platform        = Gem::Platform::RUBY
   s.summary         = "a modular Ruby webserver interface"
@@ -15,20 +17,28 @@ middleware) into a single method call.
 Also see https://rack.github.io/.
 EOF
 
-  s.files           = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
-                        %w(COPYING rack.gemspec Rakefile README.rdoc SPEC)
+  s.files           = Dir['{bin/*,contrib/*,example/*,lib/**/*}'] +
+                        %w(MIT-LICENSE rack.gemspec Rakefile README.rdoc SPEC)
   s.bindir          = 'bin'
-  s.executables     << 'rackup'
-  s.require_path    = 'lib'
-  s.extra_rdoc_files = ['README.rdoc', 'HISTORY.md']
-  s.test_files      = Dir['test/spec_*.rb']
+  s.executables << 'rackup'
+  s.require_path = 'lib'
+  s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.md']
 
   s.author          = 'Leah Neukirchen'
   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/.bacon b/test/.bacon
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/builder/an_underscore_app.rb b/test/builder/an_underscore_app.rb
index 7ce1a0cc9a01a483155f992497bf13ed12121965..f58a2be509f0e422202dfd35fcbb0e120a730aa1 100644
--- a/test/builder/an_underscore_app.rb
+++ b/test/builder/an_underscore_app.rb
@@ -1,5 +1,7 @@
+# frozen_string_literal: true
+
 class AnUnderscoreApp
   def self.call(env)
-    [200, {'Content-Type' => 'text/plain'}, ['OK']]
+    [200, { 'Content-Type' => 'text/plain' }, ['OK']]
   end
 end
diff --git a/test/builder/anything.rb b/test/builder/anything.rb
deleted file mode 100644
index c07f82cda5307912156061a3209c1cc32f0efba8..0000000000000000000000000000000000000000
--- a/test/builder/anything.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-class Anything
-  def self.call(env)
-    [200, {'Content-Type' => 'text/plain'}, ['OK']]
-  end
-end
diff --git a/test/builder/bom.ru b/test/builder/bom.ru
new file mode 100644
index 0000000000000000000000000000000000000000..5740f9a13826a4865da00ec40dcefd548d617caf
--- /dev/null
+++ b/test/builder/bom.ru
@@ -0,0 +1 @@
+run -> (env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
diff --git a/test/builder/comment.ru b/test/builder/comment.ru
index 0722f0a0e958a50a7033fa43f9ff55a23980de93..894ba5d017927d96c19dcbeb82aa70711e82cce0 100644
--- a/test/builder/comment.ru
+++ b/test/builder/comment.ru
@@ -1,4 +1,6 @@
+# frozen_string_literal: true
+
 =begin
 
 =end
-run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
+run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
diff --git a/test/builder/end.ru b/test/builder/end.ru
index 7f36d8cbbe18c1df1ef8bb29b707c2954843315b..dd8d45a9255e0c59bcdb45065cec4e2a6ceb282c 100644
--- a/test/builder/end.ru
+++ b/test/builder/end.ru
@@ -1,4 +1,6 @@
-run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
+# frozen_string_literal: true
+
+run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
 __END__
 Should not be evaluated
 Neither should
diff --git a/test/builder/line.ru b/test/builder/line.ru
index f4c84aded503f5271fa61004a96dc0513a4149a7..9ad88986087ce02ab6776dc9dc49d9e5d5323bbe 100644
--- a/test/builder/line.ru
+++ b/test/builder/line.ru
@@ -1 +1,3 @@
-run lambda{ |env| [200, {'Content-Type' => 'text/plain'}, [__LINE__.to_s]] }
+# frozen_string_literal: true
+
+run lambda{ |env| [200, { 'Content-Type' => 'text/plain' }, [__LINE__.to_s]] }
diff --git a/test/builder/options.ru b/test/builder/options.ru
index 4af324404677d7d69eed5438603f205bfe165751..dca48fd9190c5dd55d895322d72858f6771446e7 100644
--- a/test/builder/options.ru
+++ b/test/builder/options.ru
@@ -1,2 +1,4 @@
+# frozen_string_literal: true
+
 #\ -d -p 2929 --env test
-run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
+run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
diff --git a/test/cgi/lighttpd.conf b/test/cgi/lighttpd.conf
deleted file mode 100755
index c195f78cdc15cdbcefd2bf744af429b08b5ba44a..0000000000000000000000000000000000000000
--- a/test/cgi/lighttpd.conf
+++ /dev/null
@@ -1,26 +0,0 @@
-server.modules = ("mod_fastcgi", "mod_cgi")
-server.document-root = "."
-server.errorlog = var.CWD + "/lighttpd.errors"
-server.port = 9203
-server.bind = "127.0.0.1"
-
-server.event-handler = "select"
-
-cgi.assign = ("/test" => "",
-#              ".ru" => ""
-             )
-
-fastcgi.server = (
-                  "test.fcgi" => ("localhost" =>
-                    ("min-procs" => 1,
-                     "socket" => "/tmp/rack-test-fcgi",
-                     "bin-path" => "test.fcgi")),
-                  "test.ru" => ("localhost" =>
-                    ("min-procs" => 1,
-                     "socket" => "/tmp/rack-test-ru-fcgi",
-                     "bin-path" => CWD + "/rackup_stub.rb test.ru")),
-                  "sample_rackup.ru" => ("localhost" =>
-                    ("min-procs" => 1,
-                     "socket" => "/tmp/rack-test-rackup-fcgi",
-                     "bin-path" => CWD + "/rackup_stub.rb sample_rackup.ru")),
-                 )
diff --git a/test/cgi/rackup_stub.rb b/test/cgi/rackup_stub.rb
index a216cdc393773c70055e2d0961fafd84e277864d..5f0e4365e6d74b4a3bb712f5541669920cefb618 100755
--- a/test/cgi/rackup_stub.rb
+++ b/test/cgi/rackup_stub.rb
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# -*- ruby -*-
+# frozen_string_literal: true
 
 $:.unshift '../../lib'
 require 'rack'
diff --git a/test/cgi/sample_rackup.ru b/test/cgi/sample_rackup.ru
index a73df81c134bc51882925ad04d0e1b1b86d3486d..c8e94c9f1527d859870b095ec22ab7974d3fe42c 100755
--- a/test/cgi/sample_rackup.ru
+++ b/test/cgi/sample_rackup.ru
@@ -1,4 +1,4 @@
-# -*- ruby -*-
+# frozen_string_literal: true
 
 require '../testrequest'
 
diff --git a/test/cgi/test b/test/cgi/test
index e4837a4ebbf8cd2b58b47b44c440f9dbed7b7f75..a1de2fbe39a84cb7800adc392ffcfbacbe2d0658 100755
--- a/test/cgi/test
+++ b/test/cgi/test
@@ -1,5 +1,5 @@
 #!/usr/bin/env ruby
-# -*- ruby -*-
+# frozen_string_literal: true
 
 $: << File.join(File.dirname(__FILE__), "..", "..", "lib")
 
diff --git a/test/cgi/test.fcgi b/test/cgi/test.fcgi
deleted file mode 100755
index 31f433996dbc70cc7cbcfaa8916a3682aef20488..0000000000000000000000000000000000000000
--- a/test/cgi/test.fcgi
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env ruby
-# -*- ruby -*-
-
-require 'uri'
-$:.unshift '../../lib'
-require 'rack'
-require '../testrequest'
-
-Rack::Handler::FastCGI.run(Rack::Lint.new(TestRequest.new))
diff --git a/test/cgi/test.gz b/test/cgi/test.gz
old mode 100755
new mode 100644
index 312c60e2ab5a4b6dddb30f2adf6783d7a199eb89..a23c856c8e9f2b68ace458f61f8343e6c7f0d131
Binary files a/test/cgi/test.gz and b/test/cgi/test.gz differ
diff --git a/test/cgi/test.ru b/test/cgi/test.ru
index 7913ef781d068572a2a9b10a767464bb6fe35ddd..1263778df2d458483d78060efe49e4f9fe151bd3 100755
--- a/test/cgi/test.ru
+++ b/test/cgi/test.ru
@@ -1,5 +1,5 @@
 #!../../bin/rackup
-# -*- ruby -*-
+# frozen_string_literal: true
 
 require '../testrequest'
 run Rack::Lint.new(TestRequest.new)
diff --git a/test/gemloader.rb b/test/gemloader.rb
index 22be6975829ecaf163fd1dbd3a56a853fe68dff4..f38c803607cb95a8eb599a6de01c775ba17d5433 100644
--- a/test/gemloader.rb
+++ b/test/gemloader.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'rubygems'
 project = 'rack'
 gemspec = File.expand_path("#{project}.gemspec", Dir.pwd)
diff --git a/test/helper.rb b/test/helper.rb
index aa9c0e0af0727fde531852b659646ed01d75f70c..38f7df405b8bb84d68be22dfa16696568e908382 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,34 +1,8 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 
 module Rack
   class TestCase < Minitest::Test
-    # Check for Lighttpd and launch it for tests if available.
-    `which lighttpd`
-
-    if $?.success?
-      begin
-        # Keep this first.
-        LIGHTTPD_PID = fork {
-          ENV['RACK_ENV'] = 'deployment'
-          ENV['RUBYLIB'] = [
-            ::File.expand_path('../../lib', __FILE__),
-            ENV['RUBYLIB'],
-          ].compact.join(':')
-
-          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_plus b/test/multipart/filename_with_plus
new file mode 100644
index 0000000000000000000000000000000000000000..aa75022b937827fd16a0c77bc1e230e2b436b8e2
--- /dev/null
+++ b/test/multipart/filename_with_plus
@@ -0,0 +1,6 @@
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="foo+bar"
+Content-Type: application/octet-stream
+
+contents
+--AaB03x--
diff --git a/test/multipart/robust_field_separation b/test/multipart/robust_field_separation
new file mode 100644
index 0000000000000000000000000000000000000000..34956b150c3263f6329bd620255375c77e23d895
--- /dev/null
+++ b/test/multipart/robust_field_separation
@@ -0,0 +1,6 @@
+--AaB03x
+Content-Disposition: form-data;name="text"
+Content-Type: text/plain
+
+contents
+--AaB03x--
diff --git a/test/rackup/.gitignore b/test/rackup/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..2f9d279a62f672883b107397c0ce0a5d19fc234a
--- /dev/null
+++ b/test/rackup/.gitignore
@@ -0,0 +1 @@
+log_output
diff --git a/test/rackup/config.ru b/test/rackup/config.ru
index f1e2e1f3000a5cf9c21d27a29184d19b52296d32..fa9b6ecab50cbd3b7a1eabafa520d07710823738 100644
--- a/test/rackup/config.ru
+++ b/test/rackup/config.ru
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require "#{File.dirname(__FILE__)}/../testrequest"
 
 $stderr = File.open("#{File.dirname(__FILE__)}/log_output", "w")
diff --git a/test/registering_handler/rack/handler/registering_myself.rb b/test/registering_handler/rack/handler/registering_myself.rb
index 4964953b8d8d6e3f84d5e4b5c0fd004912b6eb58..21b6051676cb8ffe2cf2a3bb1e226accb30ae634 100644
--- a/test/registering_handler/rack/handler/registering_myself.rb
+++ b/test/registering_handler/rack/handler/registering_myself.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Handler
     class RegisteringMyself
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb
index 45d28576f5a9dddabf6f589b28b6099580fd56fc..3e479ace9b8ae13fec45aef8383d0841fd2e19a2 100644
--- a/test/spec_auth_basic.rb
+++ b/test/spec_auth_basic.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/auth/basic'
 require 'rack/lint'
 require 'rack/mock'
@@ -10,7 +12,7 @@ describe Rack::Auth::Basic do
 
   def unprotected_app
     Rack::Lint.new lambda { |env|
-      [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}"] ]
+      [ 200, { 'Content-Type' => 'text/plain' }, ["Hi #{env['REMOTE_USER']}"] ]
     }
   end
 
diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb
index 7230bb684743c110d6356cf30c0da725f3a8b975..cc205aa9f0dc8c867ca636ea0907bcb305af903a 100644
--- a/test/spec_auth_digest.rb
+++ b/test/spec_auth_digest.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/auth/digest/md5'
 require 'rack/lint'
 require 'rack/mock'
@@ -11,12 +13,12 @@ describe Rack::Auth::Digest::MD5 do
   def unprotected_app
     Rack::Lint.new lambda { |env|
       friend = Rack::Utils.parse_query(env["QUERY_STRING"])["friend"]
-      [ 200, {'Content-Type' => 'text/plain'}, ["Hi #{env['REMOTE_USER']}#{friend ? " and #{friend}" : ''}"] ]
+      [ 200, { 'Content-Type' => 'text/plain' }, ["Hi #{env['REMOTE_USER']}#{friend ? " and #{friend}" : ''}"] ]
     }
   end
 
   def protected_app
-    Rack::Auth::Digest::MD5.new(unprotected_app, :realm => realm, :opaque => 'this-should-be-secret') do |username|
+    Rack::Auth::Digest::MD5.new(unprotected_app, realm: realm, opaque: 'this-should-be-secret') do |username|
       { 'Alice' => 'correct-password' }[username]
     end
   end
@@ -158,7 +160,7 @@ describe Rack::Auth::Digest::MD5 do
     begin
       Rack::Auth::Digest::Nonce.time_limit = 10
 
-      request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 1 do |response|
+      request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', wait: 1 do |response|
         response.status.must_equal 200
         response.body.to_s.must_equal 'Hi Alice'
         response.headers['WWW-Authenticate'].wont_match(/\bstale=true\b/)
@@ -172,7 +174,7 @@ describe Rack::Auth::Digest::MD5 do
     begin
       Rack::Auth::Digest::Nonce.time_limit = 1
 
-      request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', :wait => 2 do |response|
+      request_with_digest_auth 'GET', '/', 'Alice', 'correct-password', wait: 2 do |response|
         assert_digest_auth_challenge response
         response.headers['WWW-Authenticate'].must_match(/\bstale=true\b/)
       end
@@ -247,7 +249,7 @@ describe Rack::Auth::Digest::MD5 do
 
   it 'return application output if correct credentials given for PUT (using method override of POST)' do
     @request = Rack::MockRequest.new(protected_app_with_method_override)
-    request_with_digest_auth 'POST', '/', 'Alice', 'correct-password', :input => "_method=put" do |response|
+    request_with_digest_auth 'POST', '/', 'Alice', 'correct-password', input: "_method=put" do |response|
       response.status.must_equal 200
       response.body.to_s.must_equal 'Hi Alice'
     end
diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb
index 4db447a0acab273d8102bf6eba8775f3a3e79d84..d3853e1e9f269c5a5473fbf1eae594124e2398c5 100644
--- a/test/spec_body_proxy.rb
+++ b/test/spec_body_proxy.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/body_proxy'
 require 'stringio'
 
@@ -40,7 +42,7 @@ describe Rack::BodyProxy do
     called = false
 
     begin
-      proxy  = Rack::BodyProxy.new(object) { called = true }
+      proxy = Rack::BodyProxy.new(object) { called = true }
       called.must_equal false
       proxy.close
     rescue RuntimeError => e
@@ -61,8 +63,8 @@ describe Rack::BodyProxy do
     body.respond_to?(:to_ary).must_equal true
 
     proxy = Rack::BodyProxy.new(body) { }
-    proxy.respond_to?(:to_ary).must_equal false
-    proxy.respond_to?("to_ary").must_equal false
+    x = [proxy]
+    assert_equal x, x.flatten
   end
 
   it 'not close more than one time' do
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index ae1c40065ce51d1363a7ee0620db773ba40f6e95..06918616dfdb701832668bd8422dface154b07fa 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/builder'
 require 'rack/lint'
 require 'rack/mock'
@@ -6,7 +8,7 @@ require 'rack/show_exceptions'
 require 'rack/urlmap'
 
 class NothingMiddleware
-  def initialize(app)
+  def initialize(app, **)
     @app = app
   end
   def call(env)
@@ -31,10 +33,10 @@ describe Rack::Builder do
   it "supports mapping" do
     app = builder_to_app do
       map '/' do |outer_env|
-        run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['root']] }
+        run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['root']] }
       end
       map '/sub' do
-        run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['sub']] }
+        run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] }
       end
     end
     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root'
@@ -43,11 +45,11 @@ describe Rack::Builder do
 
   it "doesn't dupe env even when mapping" do
     app = builder_to_app do
-      use NothingMiddleware
+      use NothingMiddleware, noop: :noop
       map '/' do |outer_env|
         run lambda { |inner_env|
           inner_env['new_key'] = 'new_value'
-          [200, {"Content-Type" => "text/plain"}, ['root']]
+          [200, { "Content-Type" => "text/plain" }, ['root']]
         }
       end
     end
@@ -55,6 +57,19 @@ describe Rack::Builder do
     NothingMiddleware.env['new_key'].must_equal 'new_value'
   end
 
+  it "dupe #to_app when mapping so Rack::Reloader can reload the application on each request" do
+    app = builder do
+      map '/' do |outer_env|
+        run lambda { |env|  [200, { "Content-Type" => "text/plain" }, [object_id.to_s]] }
+      end
+    end
+
+    builder_app1_id = Rack::MockRequest.new(app).get("/").body.to_s
+    builder_app2_id = Rack::MockRequest.new(app).get("/").body.to_s
+
+    builder_app2_id.wont_equal builder_app1_id
+  end
+
   it "chains apps by default" do
     app = builder_to_app do
       use Rack::ShowExceptions
@@ -84,7 +99,7 @@ describe Rack::Builder do
         'secret' == password
       end
 
-      run lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hi Boss']] }
+      run lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hi Boss']] }
     end
 
     response = Rack::MockRequest.new(app).get("/")
@@ -112,9 +127,9 @@ describe Rack::Builder do
   it "can mix map and run for endpoints" do
     app = builder do
       map '/sub' do
-        run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['sub']] }
+        run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] }
       end
-      run lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, ['root']] }
+      run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['root']] }
     end
 
     Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'root'
@@ -151,7 +166,7 @@ describe Rack::Builder do
         def call(env)
           raise "bzzzt"  if @called > 0
         @called += 1
-          [200, {'Content-Type' => 'text/plain'}, ['OK']]
+          [200, { 'Content-Type' => 'text/plain' }, ['OK']]
         end
       end
 
@@ -174,6 +189,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 }
@@ -204,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'
@@ -220,7 +249,12 @@ describe Rack::Builder do
 
     it "sets __LINE__ correctly" do
       app, _ = Rack::Builder.parse_file config_file('line.ru')
-      Rack::MockRequest.new(app).get("/").body.to_s.must_equal '1'
+      Rack::MockRequest.new(app).get("/").body.to_s.must_equal '3'
+    end
+
+    it "strips leading unicode byte order mark when present" do
+      app, _ = Rack::Builder.parse_file config_file('bom.ru')
+      Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
     end
   end
 
diff --git a/test/spec_cascade.rb b/test/spec_cascade.rb
index 180ce46eb05444cdfd80a9afd50b8be7ae21972f..b372a56d2f46c345a63903f0a18ccbaf4be4f3b7 100644
--- a/test/spec_cascade.rb
+++ b/test/spec_cascade.rb
@@ -1,7 +1,9 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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'
@@ -12,12 +14,12 @@ 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" })
 
   app3 = Rack::URLMap.new("/foo" => lambda { |env|
-                            [200, { "Content-Type" => "text/plain"}, [""]]})
+                            [200, { "Content-Type" => "text/plain" }, [""]]})
 
   it "dispatch onward on 404 and 405 by default" do
     cascade = cascade([app1, app2, app3])
@@ -26,7 +28,7 @@ describe Rack::Cascade do
     Rack::MockRequest.new(cascade).get("/toobad").must_be :not_found?
     Rack::MockRequest.new(cascade).get("/cgi/../..").must_be :client_error?
 
-    # Put is not allowed by Rack::File so it'll 405.
+    # Put is not allowed by Rack::Files so it'll 405.
     Rack::MockRequest.new(cascade).put("/foo").must_be :ok?
   end
 
diff --git a/test/spec_cgi.rb b/test/spec_cgi.rb
deleted file mode 100644
index 77020c2f6f26b10bad89ed8ba00f9ca1d26168e9..0000000000000000000000000000000000000000
--- a/test/spec_cgi.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-require 'helper'
-
-if defined? LIGHTTPD_PID
-
-require File.expand_path('../testrequest', __FILE__)
-require 'rack/handler/cgi'
-
-describe Rack::Handler::CGI do
-  include TestRequest::Helpers
-
-  before do
-    @host = '127.0.0.1'
-    @port = 9203
-  end
-
-  if `which lighttpd` && !$?.success?
-    raise "lighttpd not found"
-  end
-
-  it "respond" do
-    sleep 1
-    GET("/test")
-    response.wont_be :nil?
-  end
-
-  it "be a lighttpd" do
-    GET("/test")
-    status.must_equal 200
-    response["SERVER_SOFTWARE"].must_match(/lighttpd/)
-    response["HTTP_VERSION"].must_equal "HTTP/1.1"
-    response["SERVER_PROTOCOL"].must_equal "HTTP/1.1"
-    response["SERVER_PORT"].must_equal @port.to_s
-    response["SERVER_NAME"].must_equal @host
-  end
-
-  it "have rack headers" do
-    GET("/test")
-    response["rack.version"].must_equal [1,3]
-    assert_equal false, response["rack.multithread"]
-    assert_equal true, response["rack.multiprocess"]
-    assert_equal true, response["rack.run_once"]
-  end
-
-  it "have CGI headers on GET" do
-    GET("/test")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["SCRIPT_NAME"].must_equal "/test"
-    response["REQUEST_PATH"].must_equal "/"
-    response["PATH_INFO"].must_be_nil
-    response["QUERY_STRING"].must_equal ""
-    response["test.postdata"].must_equal ""
-
-    GET("/test/foo?quux=1")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["SCRIPT_NAME"].must_equal "/test"
-    response["REQUEST_PATH"].must_equal "/"
-    response["PATH_INFO"].must_equal "/foo"
-    response["QUERY_STRING"].must_equal "quux=1"
-  end
-
-  it "have CGI headers on POST" do
-    POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
-    status.must_equal 200
-    response["REQUEST_METHOD"].must_equal "POST"
-    response["SCRIPT_NAME"].must_equal "/test"
-    response["REQUEST_PATH"].must_equal "/"
-    response["QUERY_STRING"].must_equal ""
-    response["HTTP_X_TEST_HEADER"].must_equal "42"
-    response["test.postdata"].must_equal "rack-form-data=23"
-  end
-
-  it "support HTTP auth" do
-    GET("/test", {:user => "ruth", :passwd => "secret"})
-    response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ="
-  end
-
-  it "set status" do
-    GET("/test?secret")
-    status.must_equal 403
-    response["rack.url_scheme"].must_equal "http"
-  end
-end
-
-end # if defined? LIGHTTPD_PID
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index dc6e8c9d2abc6bed1ac58806c7ba6955c74245cc..23f640a5bf1915c570d75dc380e7a99916c3b45b 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/chunked'
 require 'rack/lint'
 require 'rack/mock'
@@ -16,11 +18,31 @@ describe Rack::Chunked do
 
   before do
     @env = Rack::MockRequest.
-      env_for('/', 'HTTP_VERSION' => '1.1', 'REQUEST_METHOD' => 'GET')
+      env_for('/', 'SERVER_PROTOCOL' => 'HTTP/1.1', 'REQUEST_METHOD' => 'GET')
+  end
+
+  class TrailerBody
+    def each(&block)
+      ['Hello', ' ', 'World!'].each(&block)
+    end
+
+    def trailers
+      { "Expires" => "tomorrow" }
+    end
+  end
+
+  it 'yields trailer headers after the response' do
+    app = lambda { |env|
+      [200, { "Content-Type" => "text/plain", "Trailer" => "Expires" }, TrailerBody.new]
+    }
+    response = Rack::MockResponse.new(*chunked(app).call(@env))
+    response.headers.wont_include 'Content-Length'
+    response.headers['Transfer-Encoding'].must_equal 'chunked'
+    response.body.must_equal "5\r\nHello\r\n1\r\n \r\n6\r\nWorld!\r\n0\r\nExpires: tomorrow\r\n\r\n"
   end
 
   it 'chunk responses with no Content-Length' do
-    app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
+    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
     response = Rack::MockResponse.new(*chunked(app).call(@env))
     response.headers.wont_include 'Content-Length'
     response.headers['Transfer-Encoding'].must_equal 'chunked'
@@ -28,7 +50,7 @@ describe Rack::Chunked do
   end
 
   it 'chunks empty bodies properly' do
-    app = lambda { |env| [200, {"Content-Type" => "text/plain"}, []] }
+    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, []] }
     response = Rack::MockResponse.new(*chunked(app).call(@env))
     response.headers.wont_include 'Content-Length'
     response.headers['Transfer-Encoding'].must_equal 'chunked'
@@ -37,18 +59,18 @@ describe Rack::Chunked do
 
   it 'chunks encoded bodies properly' do
     body = ["\uFFFEHello", " ", "World"].map {|t| t.encode("UTF-16LE") }
-    app  = lambda { |env| [200, {"Content-Type" => "text/plain"}, body] }
+    app  = lambda { |env| [200, { "Content-Type" => "text/plain" }, body] }
     response = Rack::MockResponse.new(*chunked(app).call(@env))
     response.headers.wont_include 'Content-Length'
     response.headers['Transfer-Encoding'].must_equal 'chunked'
     response.body.encoding.to_s.must_equal "ASCII-8BIT"
-    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding("BINARY")
-    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".force_encoding(Encoding::BINARY)
+    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding("BINARY")
+    response.body.must_equal "c\r\n\xFE\xFFH\x00e\x00l\x00l\x00o\x00\r\n2\r\n \x00\r\na\r\nW\x00o\x00r\x00l\x00d\x00\r\n0\r\n\r\n".dup.force_encoding(Encoding::BINARY)
   end
 
   it 'not modify response when Content-Length header present' do
     app = lambda { |env|
-      [200, {"Content-Type" => "text/plain", 'Content-Length'=>'12'}, ['Hello', ' ', 'World!']]
+      [200, { "Content-Type" => "text/plain", 'Content-Length' => '12' }, ['Hello', ' ', 'World!']]
     }
     status, headers, body = chunked(app).call(@env)
     status.must_equal 200
@@ -58,8 +80,8 @@ describe Rack::Chunked do
   end
 
   it 'not modify response when client is HTTP/1.0' do
-    app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
-    @env['HTTP_VERSION'] = 'HTTP/1.0'
+    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
+    @env['SERVER_PROTOCOL'] = 'HTTP/1.0'
     status, headers, body = chunked(app).call(@env)
     status.must_equal 200
     headers.wont_include 'Transfer-Encoding'
@@ -67,7 +89,7 @@ describe Rack::Chunked do
   end
 
   it 'not modify response when client is ancient, pre-HTTP/1.0' do
-    app = lambda { |env| [200, {"Content-Type" => "text/plain"}, ['Hello', ' ', 'World!']] }
+    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
     check = lambda do
       status, headers, body = chunked(app).call(@env.dup)
       status.must_equal 200
@@ -75,16 +97,16 @@ describe Rack::Chunked do
       body.join.must_equal 'Hello World!'
     end
 
-    @env.delete('HTTP_VERSION') # unicorn will do this on pre-HTTP/1.0 requests
+    @env.delete('SERVER_PROTOCOL') # unicorn will do this on pre-HTTP/1.0 requests
     check.call
 
-    @env['HTTP_VERSION'] = 'HTTP/0.9' # not sure if this happens in practice
+    @env['SERVER_PROTOCOL'] = 'HTTP/0.9' # not sure if this happens in practice
     check.call
   end
 
   it 'not modify response when Transfer-Encoding header already present' do
     app = lambda { |env|
-      [200, {"Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity'}, ['Hello', ' ', 'World!']]
+      [200, { "Content-Type" => "text/plain", 'Transfer-Encoding' => 'identity' }, ['Hello', ' ', 'World!']]
     }
     status, headers, body = chunked(app).call(@env)
     status.must_equal 200
diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb
index 3589576b879a011d78d28c057f3d653acd85b2b8..330a6480b8ebf8b8ab0937d14998e6ff2a7f5c55 100644
--- a/test/spec_common_logger.rb
+++ b/test/spec_common_logger.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/common_logger'
 require 'rack/lint'
 require 'rack/mock'
@@ -11,15 +13,15 @@ describe Rack::CommonLogger do
 
   app = Rack::Lint.new lambda { |env|
     [200,
-     {"Content-Type" => "text/html", "Content-Length" => length.to_s},
+     { "Content-Type" => "text/html", "Content-Length" => length.to_s },
      [obj]]}
   app_without_length = Rack::Lint.new lambda { |env|
     [200,
-     {"Content-Type" => "text/html"},
+     { "Content-Type" => "text/html" },
      []]}
   app_with_zero_length = Rack::Lint.new lambda { |env|
     [200,
-     {"Content-Type" => "text/html", "Content-Length" => "0"},
+     { "Content-Type" => "text/html", "Content-Length" => "0" },
      []]}
 
   it "log to rack.errors by default" do
diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb
index 58f37ad5ec98614095fd0d365cf1f63453b29442..8402f04e86654e204510e312b2f6c2d2b1956538 100644
--- a/test/spec_conditional_get.rb
+++ b/test/spec_conditional_get.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'time'
 require 'rack/conditional_get'
 require 'rack/mock'
@@ -11,7 +13,7 @@ describe Rack::ConditionalGet do
   it "set a 304 status and truncate body when If-Modified-Since hits" do
     timestamp = Time.now.httpdate
     app = conditional_get(lambda { |env|
-      [200, {'Last-Modified'=>timestamp}, ['TEST']] })
+      [200, { 'Last-Modified' => timestamp }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp)
@@ -22,7 +24,7 @@ describe Rack::ConditionalGet do
 
   it "set a 304 status and truncate body when If-Modified-Since hits and is higher than current time" do
     app = conditional_get(lambda { |env|
-      [200, {'Last-Modified'=>(Time.now - 3600).httpdate}, ['TEST']] })
+      [200, { 'Last-Modified' => (Time.now - 3600).httpdate }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
@@ -33,7 +35,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')
@@ -45,7 +47,7 @@ describe Rack::ConditionalGet do
   it "not set a 304 status if If-Modified-Since hits but Etag does not" do
     timestamp = Time.now.httpdate
     app = conditional_get(lambda { |env|
-      [200, {'Last-Modified'=>timestamp, 'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] })
+      [200, { 'Last-Modified' => timestamp, 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => timestamp, 'HTTP_IF_NONE_MATCH' => '4321')
@@ -57,7 +59,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')
@@ -68,7 +70,7 @@ describe Rack::ConditionalGet do
 
   it "not affect non-GET/HEAD requests" do
     app = conditional_get(lambda { |env|
-      [200, {'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] })
+      [200, { 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       post("/", 'HTTP_IF_NONE_MATCH' => '1234')
@@ -79,7 +81,7 @@ describe Rack::ConditionalGet do
 
   it "not affect non-200 requests" do
     app = conditional_get(lambda { |env|
-      [302, {'Etag'=>'1234', 'Content-Type' => 'text/plain'}, ['TEST']] })
+      [302, { 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_NONE_MATCH' => '1234')
@@ -91,7 +93,7 @@ describe Rack::ConditionalGet do
   it "not affect requests with malformed HTTP_IF_NONE_MATCH" do
     bad_timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S %z')
     app = conditional_get(lambda { |env|
-      [200,{'Last-Modified'=>(Time.now - 3600).httpdate, 'Content-Type' => 'text/plain'}, ['TEST']] })
+      [200, { 'Last-Modified' => (Time.now - 3600).httpdate, 'Content-Type' => 'text/plain' }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => bad_timestamp)
diff --git a/test/spec_config.rb b/test/spec_config.rb
index 16f0a6649e0f0c646f16ff6d89a5a907a23389cd..d97107b68c5c95ce78f5e18c32bdfc0716819e72 100644
--- a/test/spec_config.rb
+++ b/test/spec_config.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/builder'
 require 'rack/config'
 require 'rack/content_length'
@@ -13,7 +15,7 @@ describe Rack::Config do
         env['greeting'] = 'hello'
       end
       run lambda { |env|
-        [200, {'Content-Type' => 'text/plain'}, [env['greeting'] || '']]
+        [200, { 'Content-Type' => 'text/plain' }, [env['greeting'] || '']]
       }
     end
 
diff --git a/test/spec_content_length.rb b/test/spec_content_length.rb
index 89752bbec6a4895d08fed0e9851758f4c8a3718d..2e7a858155fe6121bad6dea6dbd1ec0a9f18ead6 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/content_length'
 require 'rack/lint'
 require 'rack/mock'
@@ -13,7 +15,7 @@ describe Rack::ContentLength do
   end
 
   it "set Content-Length on Array bodies if none is set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = content_length(app).call(request)
     response[1]['Content-Length'].must_equal '13'
   end
@@ -22,13 +24,13 @@ describe Rack::ContentLength do
     body = lambda { "Hello World!" }
     def body.each ; yield call ; end
 
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
     response = content_length(app).call(request)
     response[1]['Content-Length'].must_be_nil
   end
 
   it "not change Content-Length if it is already set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Length' => '1'}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Content-Length' => '1' }, "Hello, World!"] }
     response = content_length(app).call(request)
     response[1]['Content-Length'].must_equal '1'
   end
@@ -40,7 +42,7 @@ describe Rack::ContentLength do
   end
 
   it "not set Content-Length when Transfer-Encoding is chunked" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked'}, []] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked' }, []] }
     response = content_length(app).call(request)
     response[1]['Content-Length'].must_be_nil
   end
@@ -62,7 +64,7 @@ describe Rack::ContentLength do
       def to_ary; end
     end.new(%w[one two three])
 
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
     response = content_length(app).call(request)
     body.closed.must_be_nil
     response[2].close
@@ -77,7 +79,7 @@ describe Rack::ContentLength do
       def to_ary; end
     end.new(%w[one two three])
 
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
     response = content_length(app).call(request)
     expected = %w[one two three]
     response[1]['Content-Length'].must_equal expected.join.size.to_s
diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb
index daf75355dd473d47d0822a81b61dfa6bcd1abafd..53f1d1728c3a6a2f2a3b0b2fe3452662e0f9dc05 100644
--- a/test/spec_content_type.rb
+++ b/test/spec_content_type.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/content_type'
 require 'rack/lint'
 require 'rack/mock'
@@ -26,21 +28,31 @@ describe Rack::ContentType do
   end
 
   it "not change Content-Type if it is already set" do
-    app = lambda { |env| [200, {'Content-Type' => 'foo/bar'}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'Content-Type' => 'foo/bar' }, "Hello, World!"] }
     headers = content_type(app).call(request)[1]
     headers['Content-Type'].must_equal 'foo/bar'
   end
 
   it "detect Content-Type case insensitive" do
-    app = lambda { |env| [200, {'CONTENT-Type' => 'foo/bar'}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'CONTENT-Type' => 'foo/bar' }, "Hello, World!"] }
     headers = content_type(app).call(request)[1]
-    headers.to_a.select { |k,v| k.downcase == "content-type" }.
-      must_equal [["CONTENT-Type","foo/bar"]]
+    headers.to_a.select { |k, v| k.downcase == "content-type" }.
+      must_equal [["CONTENT-Type", "foo/bar"]]
+  end
+
+  [100, 204, 304].each do |code|
+    it "not set Content-Type on #{code} responses" do
+      app = lambda { |env| [code, {}, []] }
+      response = content_type(app, "text/html").call(request)
+      response[1]['Content-Type'].must_be_nil
+    end
   end
 
-  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_be_nil
+  ['100', '204', '304'].each do |code|
+    it "not set Content-Type on #{code} responses if status is a string" do
+      app = lambda { |env| [code, {}, []] }
+      response = content_type(app, "text/html").call(request)
+      response[1]['Content-Type'].must_be_nil
+    end
   end
 end
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 0f27c859fb69a88fb018da78189fad7cd5462277..378e2cf30a3d8ebcc792f454fb0e6e6c2d8dec9a 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'time'  # for Time#httpdate
 require 'rack/deflater'
@@ -44,6 +46,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,
@@ -57,7 +61,7 @@ describe Rack::Deflater do
 
     # verify body
     unless options['skip_body_verify']
-      body_text = ''
+      body_text = ''.dup
       body.each { |part| body_text << part }
 
       deflated_body = case expected_encoding
@@ -67,6 +71,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
@@ -87,7 +98,7 @@ describe Rack::Deflater do
   end
 
   def deflate_or_gzip
-    {'deflate, gzip' => 'gzip'}
+    { 'deflate, gzip' => 'gzip' }
   end
 
   it 'be able to deflate bodies that respond to each' do
@@ -103,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
@@ -299,7 +323,7 @@ describe Rack::Deflater do
         'Content-Type' => 'text/plain'
       },
       'deflater_options' => {
-        :include => %w(text/plain)
+        include: %w(text/plain)
       }
     }
     verify(200, 'Hello World!', 'gzip', options)
@@ -311,7 +335,7 @@ describe Rack::Deflater do
         'Content-Type' => 'text/plain; charset=us-ascii'
       },
       'deflater_options' => {
-        :include => %w(text/plain)
+        include: %w(text/plain)
       }
     }
     verify(200, 'Hello World!', 'gzip', options)
@@ -320,7 +344,7 @@ describe Rack::Deflater do
   it "not deflate if content-type is not set but given in :include" do
     options = {
       'deflater_options' => {
-        :include => %w(text/plain)
+        include: %w(text/plain)
       }
     }
     verify(304, 'Hello World!', { 'gzip' => nil }, options)
@@ -332,16 +356,25 @@ describe Rack::Deflater do
         'Content-Type' => 'text/plain'
       },
       'deflater_options' => {
-        :include => %w(text/json)
+        include: %w(text/json)
       }
     }
     verify(200, 'Hello World!', { 'gzip' => nil }, options)
   end
 
+  it "not deflate if content-length is 0" do
+    options = {
+      'response_headers' => {
+        'Content-Length' => '0'
+      },
+    }
+    verify(200, '', { 'gzip' => nil }, options)
+  end
+
   it "deflate response if :if lambda evaluates to true" do
     options = {
       'deflater_options' => {
-        :if => lambda { |env, status, headers, body| true }
+        if: lambda { |env, status, headers, body| true }
       }
     }
     verify(200, 'Hello World!', deflate_or_gzip, options)
@@ -350,7 +383,7 @@ describe Rack::Deflater do
   it "not deflate if :if lambda evaluates to false" do
     options = {
       'deflater_options' => {
-        :if => lambda { |env, status, headers, body| false }
+        if: lambda { |env, status, headers, body| false }
       }
     }
     verify(200, 'Hello World!', { 'gzip' => nil }, options)
@@ -364,7 +397,7 @@ describe Rack::Deflater do
         'Content-Length' => response_len.to_s
       },
       'deflater_options' => {
-        :if => lambda { |env, status, headers, body|
+        if: lambda { |env, status, headers, body|
           headers['Content-Length'].to_i >= response_len
         }
       }
@@ -372,4 +405,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" }
+      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 = ''.dup
+      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 42bdea9f6cae697bed000f907b5f553897aa1589..6d84a2bffa6e0f2fb0f5892bde89e9b69a985e07 100644
--- a/test/spec_directory.rb
+++ b/test/spec_directory.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/directory'
 require 'rack/lint'
 require 'rack/mock'
@@ -7,7 +9,7 @@ require 'fileutils'
 
 describe Rack::Directory do
   DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
-  FILE_CATCH = proc{|env| [200, {'Content-Type'=>'text/plain', "Content-Length" => "7"}, ['passed!']] }
+  FILE_CATCH = proc{|env| [200, { 'Content-Type' => 'text/plain', "Content-Length" => "7" }, ['passed!']] }
 
   attr_reader :app
 
@@ -23,11 +25,11 @@ describe Rack::Directory do
       FileUtils.touch File.join(full_dir, "omg.txt")
       app = Rack::Directory.new(dir, FILE_CATCH)
       env = Rack::MockRequest.env_for("/#{plus_dir}/")
-      status,_,body = app.call env
+      status, _, body = app.call env
 
       assert_equal 200, status
 
-      str = ''
+      str = ''.dup
       body.each { |x| str << x }
       assert_match "foo+bar", str
     end
@@ -82,6 +84,18 @@ describe Rack::Directory do
     res.must_be :forbidden?
   end
 
+  it "not allow dir globs" do
+    Dir.mktmpdir do |dir|
+      weirds = "uploads/.?/.?"
+      full_dir = File.join(dir, weirds)
+      FileUtils.mkdir_p full_dir
+      FileUtils.touch File.join(dir, "secret.txt")
+      app = Rack::Directory.new(File.join(dir, "uploads"))
+      res = Rack::MockRequest.new(app).get("/.%3F")
+      refute_match "secret.txt", res.body
+    end
+  end
+
   it "404 if it can't find the file" do
     res = Rack::MockRequest.new(Rack::Lint.new(app)).
       get("/cgi/blubb")
@@ -109,11 +123,11 @@ describe Rack::Directory do
       FileUtils.touch File.join(full_dir, "omg omg.txt")
       app = Rack::Directory.new(dir, FILE_CATCH)
       env = Rack::MockRequest.env_for(Rack::Utils.escape_path("/#{space_dir}/"))
-      status,_,body = app.call env
+      status, _, body = app.call env
 
       assert_equal 200, status
 
-      str = ''
+      str = ''.dup
       body.each { |x| str << x }
       assert_match "/foo%20bar/omg%20omg.txt", str
     end
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index 74795759b4a664cd9f441ba3f4e2e6539ee8a4cd..750ceaac846ee99505c9a739bde41d2552860431 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/etag'
 require 'rack/lint'
 require 'rack/mock'
@@ -20,79 +22,79 @@ describe Rack::ETag do
   end
 
   it "set ETag if none is set if status is 200" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app).call(request)
     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!"]] }
+    app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app).call(request)
     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
-    app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+    app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app).call(request)
     response[1]['Cache-Control'].must_equal 'max-age=0, private, must-revalidate'
   end
 
   it "set Cache-Control to chosen one if none is set" do
-    app = lambda { |env| [201, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+    app = lambda { |env| [201, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app, nil, 'public').call(request)
     response[1]['Cache-Control'].must_equal 'public'
   end
 
   it "set a given Cache-Control even if digest could not be calculated" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, []] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
     response = etag(app, 'no-cache').call(request)
     response[1]['Cache-Control'].must_equal 'no-cache'
   end
 
   it "not set Cache-Control if it is already set" do
-    app = lambda { |env| [201, {'Content-Type' => 'text/plain', 'Cache-Control' => 'public'}, ["Hello, World!"]] }
+    app = lambda { |env| [201, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'public' }, ["Hello, World!"]] }
     response = etag(app).call(request)
     response[1]['Cache-Control'].must_equal 'public'
   end
 
   it "not set Cache-Control if directive isn't present" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app, nil, nil).call(request)
     response[1]['Cache-Control'].must_be_nil
   end
 
   it "not change ETag if it is already set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'ETag' => '"abc"'}, ["Hello, World!"]] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'ETag' => '"abc"' }, ["Hello, World!"]] }
     response = etag(app).call(request)
     response[1]['ETag'].must_equal "\"abc\""
   end
 
   it "not set ETag if body is empty" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, []] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, []] }
     response = etag(app).call(request)
     response[1]['ETag'].must_be_nil
   end
 
   it "not set ETag if Last-Modified is set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate}, ["Hello, World!"]] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, ["Hello, World!"]] }
     response = etag(app).call(request)
     response[1]['ETag'].must_be_nil
   end
 
   it "not set ETag if a sendfile_body is given" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, sendfile_body] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, sendfile_body] }
     response = etag(app).call(request)
     response[1]['ETag'].must_be_nil
   end
 
   it "not set ETag if a status is not 200 or 201" do
-    app = lambda { |env| [401, {'Content-Type' => 'text/plain'}, ['Access denied.']] }
+    app = lambda { |env| [401, { 'Content-Type' => 'text/plain' }, ['Access denied.']] }
     response = etag(app).call(request)
     response[1]['ETag'].must_be_nil
   end
 
   it "not set ETag if no-cache is given" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate'}, ['Hello, World!']] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate' }, ['Hello, World!']] }
     response = etag(app).call(request)
     response[1]['ETag'].must_be_nil
   end
diff --git a/test/spec_events.rb b/test/spec_events.rb
index 7fc7b055cded6e59a8dc9a88b99c7518cae1109a..8c079361cff197058f821a85556f7de23a041639 100644
--- a/test/spec_events.rb
+++ b/test/spec_events.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'helper'
 require 'rack/events'
 
diff --git a/test/spec_fastcgi.rb b/test/spec_fastcgi.rb
deleted file mode 100644
index 5a48327b15bdcefa1d2223b070a020c3521ef665..0000000000000000000000000000000000000000
--- a/test/spec_fastcgi.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-require 'helper'
-
-if defined? LIGHTTPD_PID
-
-require File.expand_path('../testrequest', __FILE__)
-require 'rack/handler/fastcgi'
-
-describe Rack::Handler::FastCGI do
-  include TestRequest::Helpers
-
-  before do
-    @host = '127.0.0.1'
-    @port = 9203
-  end
-
-  it "respond" do
-    sleep 1
-    GET("/test")
-    response.wont_be :nil?
-  end
-
-  it "respond via rackup server" do
-    GET("/sample_rackup.ru")
-    status.must_equal 200
-  end
-
-  it "be a lighttpd" do
-    GET("/test.fcgi")
-    status.must_equal 200
-    response["SERVER_SOFTWARE"].must_match(/lighttpd/)
-    response["HTTP_VERSION"].must_equal "HTTP/1.1"
-    response["SERVER_PROTOCOL"].must_equal "HTTP/1.1"
-    response["SERVER_PORT"].must_equal @port.to_s
-    response["SERVER_NAME"].must_equal @host
-  end
-
-  it "have rack headers" do
-    GET("/test.fcgi")
-    response["rack.version"].must_equal [1,3]
-    assert_equal false, response["rack.multithread"]
-    assert_equal true, response["rack.multiprocess"]
-    assert_equal false, response["rack.run_once"]
-  end
-
-  it "have CGI headers on GET" do
-    GET("/test.fcgi")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["SCRIPT_NAME"].must_equal "/test.fcgi"
-    response["REQUEST_PATH"].must_equal "/"
-    response["PATH_INFO"].must_equal ""
-    response["QUERY_STRING"].must_equal ""
-    response["test.postdata"].must_equal ""
-
-    GET("/test.fcgi/foo?quux=1")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["SCRIPT_NAME"].must_equal "/test.fcgi"
-    response["REQUEST_PATH"].must_equal "/"
-    response["PATH_INFO"].must_equal "/foo"
-    response["QUERY_STRING"].must_equal "quux=1"
-  end
-
-  it "have CGI headers on POST" do
-    POST("/test.fcgi", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
-    status.must_equal 200
-    response["REQUEST_METHOD"].must_equal "POST"
-    response["SCRIPT_NAME"].must_equal "/test.fcgi"
-    response["REQUEST_PATH"].must_equal "/"
-    response["QUERY_STRING"].must_equal ""
-    response["HTTP_X_TEST_HEADER"].must_equal "42"
-    response["test.postdata"].must_equal "rack-form-data=23"
-  end
-
-  it "support HTTP auth" do
-    GET("/test.fcgi", {:user => "ruth", :passwd => "secret"})
-    response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ="
-  end
-
-  it "set status" do
-    GET("/test.fcgi?secret")
-    status.must_equal 403
-    response["rack.url_scheme"].must_equal "http"
-  end
-end
-
-end # if defined? LIGHTTPD_PID
diff --git a/test/spec_file.rb b/test/spec_files.rb
similarity index 69%
rename from test/spec_file.rb
rename to test/spec_files.rb
index 48c0ab909abbcb0ca7c2a2ebbd2882c6bfdfb55f..6e75c073a00f509a571d890b0e9d60d885599c89 100644
--- a/test/spec_file.rb
+++ b/test/spec_files.rb
@@ -1,39 +1,55 @@
-require 'minitest/autorun'
-require 'rack/file'
+# frozen_string_literal: true
+
+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 "can be used without root" do
+    # https://github.com/rack/rack/issues/1464
+
+    app = Rack::Files.new(nil)
+
+    request = Rack::Request.new(
+      Rack::MockRequest.env_for("/cgi/test")
+    )
+    
+    file_path = File.expand_path("cgi/test", __dir__)
+    status, headers, body = app.serving(request, file_path)
+    assert_equal 200, status
   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
+      status, _, body = app.call env
 
       assert_equal 200, status
 
-      str = ''
+      str = ''.dup
       body.each { |x| str << x }
       assert_match "hello world", str
     end
   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")
 
@@ -43,7 +59,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
@@ -52,14 +68,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
@@ -69,12 +85,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?
@@ -87,7 +103,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?
@@ -102,7 +118,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?
 
@@ -114,33 +130,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")
 
@@ -152,26 +168,26 @@ 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"
-    res["Content-Range"].must_equal "bytes 22-33/193"
-    res.body.must_equal "-*- ruby -*-"
+    res["Content-Range"].must_equal "bytes 22-33/208"
+    res.body.must_equal "frozen_strin"
   end
 
   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 */193"
+    res["Content-Range"].must_equal "bytes */208"
   end
 
   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
@@ -181,7 +197,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
@@ -189,7 +205,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|
@@ -207,7 +223,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
@@ -215,35 +231,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 "193"
+    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 dff474c989fb3ca86272d61c53543c51cff70b77..5746dc2251a7bf6e0513442d47d6098ae293c5b9 100644
--- a/test/spec_handler.rb
+++ b/test/spec_handler.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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 17b4a3497cc0a4f808db2d77584a8960c22a944d..f6f41a5d9d506e247db1abeda25ee1568d71dd37 100644
--- a/test/spec_head.rb
+++ b/test/spec_head.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/head'
 require 'rack/lint'
 require 'rack/mock'
@@ -8,7 +10,7 @@ describe Rack::Head do
   def test_response(headers = {})
     body = StringIO.new "foo"
     app = lambda do |env|
-      [200, {"Content-type" => "test/plain", "Content-length" => "3"}, body]
+      [200, { "Content-type" => "test/plain", "Content-length" => "3" }, body]
     end
     request = Rack::MockRequest.env_for("/", headers)
     response = Rack::Lint.new(Rack::Head.new(app)).call(request)
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
index d99c1aa3150c4e6040d175e39d5ef9d58558dbc0..192f260f09ff8a922f820dc58a82cd4725fb3ebb 100644
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'tempfile'
 require 'rack/lint'
@@ -11,7 +13,7 @@ describe Rack::Lint do
 
   it "pass valid request" do
     Rack::Lint.new(lambda { |env|
-                     [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
+                     [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]]
                    }).call(env({})).first.must_equal 200
   end
 
@@ -187,14 +189,14 @@ describe Rack::Lint do
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {true=>false}, []]
+                       [200, { true => false }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "header key must be a string, was TrueClass"
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Status" => "404"}, []]
+                       [200, { "Status" => "404" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/must not contain Status/)
@@ -216,7 +218,7 @@ describe Rack::Lint do
     invalid_headers.each do |invalid_header|
       lambda {
         Rack::Lint.new(lambda { |env|
-          [200, {invalid_header => "text/plain"}, []]
+          [200, { invalid_header => "text/plain" }, []]
         }).call(env({}))
       }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}").
       message.must_equal("invalid header name: #{invalid_header}")
@@ -224,20 +226,20 @@ describe Rack::Lint do
     valid_headers = 0.upto(127).map(&:chr) - invalid_headers
     valid_headers.each do |valid_header|
       Rack::Lint.new(lambda { |env|
-                       [200, {valid_header => "text/plain"}, []]
+                       [200, { valid_header => "text/plain" }, []]
                      }).call(env({})).first.must_equal 200
     end
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Foo" => Object.new}, []]
+                       [200, { "Foo" => Object.new }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "a header value must be a String, but the value of 'Foo' is a Object"
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Foo" => [1, 2, 3]}, []]
+                       [200, { "Foo" => [1, 2, 3] }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "a header value must be a String, but the value of 'Foo' is a Array"
@@ -245,14 +247,14 @@ describe Rack::Lint do
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Foo-Bar" => "text\000plain"}, []]
+                       [200, { "Foo-Bar" => "text\000plain" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/invalid header/)
 
     # line ends (010).must_be :allowed in header values.?
     Rack::Lint.new(lambda { |env|
-                     [200, {"Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []]
+                     [200, { "Foo-Bar" => "one\ntwo\nthree", "Content-Length" => "0", "Content-Type" => "text/plain" }, []]
                    }).call(env({})).first.must_equal 200
 
     # non-Hash header responses.must_be :allowed?
@@ -272,7 +274,7 @@ describe Rack::Lint do
     [100, 101, 204, 304].each do |status|
       lambda {
         Rack::Lint.new(lambda { |env|
-                         [status, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                         [status, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                        }).call(env({}))
       }.must_raise(Rack::Lint::LintError).
         message.must_match(/Content-Type header found/)
@@ -283,7 +285,7 @@ describe Rack::Lint do
     [100, 101, 204, 304].each do |status|
       lambda {
         Rack::Lint.new(lambda { |env|
-                         [status, {"Content-length" => "0"}, []]
+                         [status, { "Content-length" => "0" }, []]
                        }).call(env({}))
       }.must_raise(Rack::Lint::LintError).
         message.must_match(/Content-Length header found/)
@@ -291,7 +293,7 @@ describe Rack::Lint do
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Content-type" => "text/plain", "Content-Length" => "1"}, []]
+                       [200, { "Content-type" => "text/plain", "Content-Length" => "1" }, []]
                      }).call(env({}))[2].each { }
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/Content-Length header was 1, but should be 0/)
@@ -300,7 +302,7 @@ describe Rack::Lint do
   it "notice body errors" do
     lambda {
       body = Rack::Lint.new(lambda { |env|
-                               [200, {"Content-type" => "text/plain","Content-length" => "3"}, [1,2,3]]
+                               [200, { "Content-type" => "text/plain", "Content-length" => "3" }, [1, 2, 3]]
                              }).call(env({}))[2]
       body.each { |part| }
     }.must_raise(Rack::Lint::LintError).
@@ -311,7 +313,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].gets("\r\n")
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/gets called with arguments/)
@@ -319,7 +321,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read(1, 2, 3)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read called with too many arguments/)
@@ -327,7 +329,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read("foo")
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read called with non-integer and non-nil length/)
@@ -335,7 +337,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read(-1)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read called with a negative length/)
@@ -343,7 +345,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read(nil, nil)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read called with non-String buffer/)
@@ -351,7 +353,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read(nil, 1)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read called with non-String buffer/)
@@ -359,7 +361,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].rewind(0)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/rewind called with arguments/)
@@ -404,7 +406,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].gets
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env("rack.input" => weirdio))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/gets didn't return a String/)
@@ -412,7 +414,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].each { |x| }
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env("rack.input" => weirdio))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/each didn't yield a String/)
@@ -420,7 +422,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env("rack.input" => weirdio))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read didn't return nil or a String/)
@@ -428,7 +430,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].read
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env("rack.input" => eof_weirdio))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/read\(nil\) returned nil on EOF/)
@@ -436,7 +438,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].rewind
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env("rack.input" => weirdio))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/rewind raised Errno::ESPIPE/)
@@ -445,7 +447,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].close
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/close must not be called/)
@@ -455,7 +457,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.errors"].write(42)
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/write not called with a String/)
@@ -463,7 +465,7 @@ describe Rack::Lint do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env["rack.errors"].close
-                       [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
+                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/close must not be called/)
@@ -471,25 +473,25 @@ describe Rack::Lint do
 
   it "notice HEAD errors" do
     Rack::Lint.new(lambda { |env|
-                     [200, {"Content-type" => "test/plain", "Content-length" => "3"}, []]
-                   }).call(env({"REQUEST_METHOD" => "HEAD"})).first.must_equal 200
+                     [200, { "Content-type" => "test/plain", "Content-length" => "3" }, []]
+                   }).call(env({ "REQUEST_METHOD" => "HEAD" })).first.must_equal 200
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, {"Content-type" => "test/plain", "Content-length" => "3"}, ["foo"]]
-                     }).call(env({"REQUEST_METHOD" => "HEAD"}))[2].each { }
+                       [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]]
+                     }).call(env({ "REQUEST_METHOD" => "HEAD" }))[2].each { }
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/body was given for HEAD/)
   end
 
   def assert_lint(*args)
-    hello_str = "hello world"
+    hello_str = "hello world".dup
     hello_str.force_encoding(Encoding::ASCII_8BIT)
 
     Rack::Lint.new(lambda { |env|
                      env["rack.input"].send(:read, *args)
-                     [201, {"Content-type" => "text/plain", "Content-length" => "0"}, []]
-                   }).call(env({"rack.input" => StringIO.new(hello_str)})).
+                     [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
+                   }).call(env({ "rack.input" => StringIO.new(hello_str) })).
       first.must_equal 201
   end
 
@@ -498,8 +500,8 @@ describe Rack::Lint do
     assert_lint 0
     assert_lint 1
     assert_lint nil
-    assert_lint nil, ''
-    assert_lint 1, ''
+    assert_lint nil, ''.dup
+    assert_lint 1, ''.dup
   end
 end
 
diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb
index fd4e70826f1ff4bd37936d91dbad3697ddd803f8..9f3b9a897d512b07fab2cd268790eaf76c75e6fd 100644
--- a/test/spec_lobster.rb
+++ b/test/spec_lobster.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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 c6f7c05ec70a8568964b90fc676c631510280162..cd9e1230d2d8117cad30b49a9757a3b1385339df 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/lock'
 require 'rack/mock'
@@ -44,7 +46,7 @@ describe Rack::Lock do
         def each; %w{ hi mom }.each { |x| yield x }; end
       }.new
 
-      app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
+      app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] })
       response = app.call(env)[2]
       list = []
       response.each { |x| list << x }
@@ -58,7 +60,7 @@ describe Rack::Lock do
       res = ['Hello World']
       def res.to_path ; "/tmp/hello.txt" ; end
 
-      app = Rack::Lock.new(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] }, lock)
+      app = Rack::Lock.new(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, res] }, lock)
       body = app.call(env)[2]
 
       body.must_respond_to :to_path
@@ -66,11 +68,11 @@ describe Rack::Lock do
     end
 
     it 'not delegate to_path if body does not implement it' do
-      env  = Rack::MockRequest.env_for("/")
+      env = Rack::MockRequest.env_for("/")
 
       res = ['Hello World']
 
-      app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, res] })
+      app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, res] })
       body = app.call(env)[2]
 
       body.wont_respond_to :to_path
@@ -85,7 +87,7 @@ describe Rack::Lock do
       def close; @close_called = true; end
     }.new
 
-    app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] })
+    app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] })
     app.call(env)
     response.close_called.must_equal false
     response.close
@@ -96,7 +98,7 @@ describe Rack::Lock do
     lock     = Lock.new
     env      = Rack::MockRequest.env_for("/")
     response = Object.new
-    app      = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, response] }, lock)
+    app      = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, response] }, lock)
     lock.synchronized.must_equal false
     response = app.call(env)[2]
     lock.synchronized.must_equal true
@@ -106,7 +108,7 @@ describe Rack::Lock do
 
   it "return value from app" do
     env  = Rack::MockRequest.env_for("/")
-    body = [200, {"Content-Type" => "text/plain"}, %w{ hi mom }]
+    body = [200, { "Content-Type" => "text/plain" }, %w{ hi mom }]
     app  = lock_app(lambda { |inner_env| body })
 
     res = app.call(env)
@@ -118,7 +120,7 @@ describe Rack::Lock do
   it "call synchronize on lock" do
     lock = Lock.new
     env = Rack::MockRequest.env_for("/")
-    app = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] }, lock)
+    app = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] }, lock)
     lock.synchronized.must_equal false
     app.call(env)
     lock.synchronized.must_equal true
@@ -143,7 +145,7 @@ describe Rack::Lock do
   it "set multithread flag to false" do
     app = lock_app(lambda { |env|
       env['rack.multithread'].must_equal false
-      [200, {"Content-Type" => "text/plain"}, %w{ a b c }]
+      [200, { "Content-Type" => "text/plain" }, %w{ a b c }]
     }, false)
     env = Rack::MockRequest.env_for("/")
     env['rack.multithread'].must_equal true
@@ -158,7 +160,7 @@ describe Rack::Lock do
         env['rack.multithread'].must_equal true
         super
       end
-    }.new(lambda { |env| [200, {"Content-Type" => "text/plain"}, %w{ a b c }] })
+    }.new(lambda { |env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] })
     Rack::Lint.new(app).call(Rack::MockRequest.env_for("/"))
   end
 
@@ -170,7 +172,7 @@ describe Rack::Lock do
       def unlock() @unlocked = true end
     end.new
     env = Rack::MockRequest.env_for("/")
-    app = lock_app(proc { [200, {"Content-Type" => "text/plain"}, []] }, lock)
+    app = lock_app(proc { [200, { "Content-Type" => "text/plain" }, []] }, lock)
     lambda { app.call(env) }.must_raise Exception
     lock.unlocked?.must_equal false
   end
@@ -180,7 +182,7 @@ describe Rack::Lock do
       attr_reader :env
       def initialize(env) @env = env end
     end
-    app = Rack::Lock.new lambda { |env| [200, {"Content-Type" => "text/plain"}, proxy.new(env)] }
+    app = Rack::Lock.new lambda { |env| [200, { "Content-Type" => "text/plain" }, proxy.new(env)] }
     response = app.call(Rack::MockRequest.env_for("/"))[2]
     response.env['rack.multithread'].must_equal false
   end
@@ -195,7 +197,7 @@ describe Rack::Lock do
 
   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]] })
+    app  = lock_app(lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, [inner_env.object_id.to_s]] })
 
     _, _, body = app.call(env)
 
diff --git a/test/spec_logger.rb b/test/spec_logger.rb
index ea503e1d066bf33bcc98ee2e943f02e1a4c4273c..f453b14df369f1ab82cd771af5a581df08b86d4f 100644
--- a/test/spec_logger.rb
+++ b/test/spec_logger.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/lint'
 require 'rack/logger'
@@ -11,7 +13,7 @@ describe Rack::Logger do
     log.info("Program started")
     log.warn("Nothing to do!")
 
-    [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]]
+    [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]]
   }
 
   it "conform to Rack::Lint" do
diff --git a/test/spec_media_type.rb b/test/spec_media_type.rb
index 1d9f0fc36326ef680595d5d301aa6277d1111690..7d52b4d48e6a1acfbcf3eda942ae3670be2b82cf 100644
--- a/test/spec_media_type.rb
+++ b/test/spec_media_type.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/media_type'
 
 describe Rack::MediaType do
@@ -23,7 +25,7 @@ describe Rack::MediaType do
       Rack::MediaType.type(@content_type).must_equal 'application/text'
     end
 
-    it  '#params is empty' do
+    it '#params is empty' do
       Rack::MediaType.params(@content_type).must_equal @empty_hash
     end
   end
diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb
index 1371ca9d465aaf64cd411d47c785ade0a05a839d..6b01f7c94600d4b858ecac5bed64bf7ab8e57bb0 100644
--- a/test/spec_method_override.rb
+++ b/test/spec_method_override.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/method_override'
 require 'rack/mock'
@@ -6,12 +8,12 @@ require 'rack/mock'
 describe Rack::MethodOverride do
   def app
     Rack::Lint.new(Rack::MethodOverride.new(lambda {|e|
-      [200, {"Content-Type" => "text/plain"}, []]
+      [200, { "Content-Type" => "text/plain" }, []]
     }))
   end
 
   it "not affect GET requests" do
-    env = Rack::MockRequest.env_for("/?_method=delete", :method => "GET")
+    env = Rack::MockRequest.env_for("/?_method=delete", method: "GET")
     app.call env
 
     env["REQUEST_METHOD"].must_equal "GET"
@@ -32,7 +34,7 @@ describe Rack::MethodOverride do
   end
 
   it "modify REQUEST_METHOD for POST requests when _method parameter is set" do
-    env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=put")
+    env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=put")
     app.call env
 
     env["REQUEST_METHOD"].must_equal "PUT"
@@ -49,14 +51,14 @@ describe Rack::MethodOverride do
   end
 
   it "not modify REQUEST_METHOD if the method is unknown" do
-    env = Rack::MockRequest.env_for("/", :method => "POST", :input => "_method=foo")
+    env = Rack::MockRequest.env_for("/", method: "POST", input: "_method=foo")
     app.call env
 
     env["REQUEST_METHOD"].must_equal "POST"
   end
 
   it "not modify REQUEST_METHOD when _method is nil" do
-    env = Rack::MockRequest.env_for("/", :method => "POST", :input => "foo=bar")
+    env = Rack::MockRequest.env_for("/", method: "POST", input: "foo=bar")
     app.call env
 
     env["REQUEST_METHOD"].must_equal "POST"
@@ -64,8 +66,8 @@ describe Rack::MethodOverride do
 
   it "store the original REQUEST_METHOD prior to overriding" do
     env = Rack::MockRequest.env_for("/",
-            :method => "POST",
-            :input  => "_method=options")
+            method: "POST",
+            input: "_method=options")
     app.call env
 
     env["rack.methodoverride.original_method"].must_equal "POST"
@@ -95,14 +97,14 @@ EOF
                       "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
+    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%)")
+    env = Rack::MockRequest.env_for("/", method: "POST", input: "(%bad-params%)")
     app.call env
 
     env["REQUEST_METHOD"].must_equal "POST"
diff --git a/test/spec_mime.rb b/test/spec_mime.rb
index 569233b491237589a77e6ce6e77d4c47e26769a2..8d1ca2566d120896138cde6c8275fc05657c20e4 100644
--- a/test/spec_mime.rb
+++ b/test/spec_mime.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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 e383f203fdd37b75682e7fa1c2262c204bdcae99..d7246d3f930bf02b3bb8dee78bb0424201e63ed7 100644
--- a/test/spec_mock.rb
+++ b/test/spec_mock.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'yaml'
 require 'rack/lint'
 require 'rack/mock'
@@ -14,9 +16,15 @@ app = Rack::Lint.new(lambda { |env|
   end
 
   body = req.head? ? "" : env.to_yaml
-  Rack::Response.new(body,
-                     req.GET["status"] || 200,
-                     "Content-Type" => "text/yaml").finish
+  response = Rack::Response.new(
+    body,
+    req.GET["status"] || 200,
+    "Content-Type" => "text/yaml"
+  )
+  response.set_cookie("session_test", { value: "session_test", domain: ".test.com", path: "/" })
+  response.set_cookie("secure_test", { value: "secure_test", domain: ".test.com",  path: "/", secure: true })
+  response.set_cookie("persistent_test", { value: "persistent_test", max_age: 15552000, path: "/" })
+  response.finish
 })
 
 describe Rack::MockRequest do
@@ -54,44 +62,53 @@ describe Rack::MockRequest do
   end
 
   it "allow GET/POST/PUT/DELETE/HEAD" do
-    res = Rack::MockRequest.new(app).get("", :input => "foo")
+    res = Rack::MockRequest.new(app).get("", input: "foo")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "GET"
 
-    res = Rack::MockRequest.new(app).post("", :input => "foo")
+    res = Rack::MockRequest.new(app).post("", input: "foo")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "POST"
 
-    res = Rack::MockRequest.new(app).put("", :input => "foo")
+    res = Rack::MockRequest.new(app).put("", input: "foo")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "PUT"
 
-    res = Rack::MockRequest.new(app).patch("", :input => "foo")
+    res = Rack::MockRequest.new(app).patch("", input: "foo")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "PATCH"
 
-    res = Rack::MockRequest.new(app).delete("", :input => "foo")
+    res = Rack::MockRequest.new(app).delete("", input: "foo")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "DELETE"
 
-    Rack::MockRequest.env_for("/", :method => "HEAD")["REQUEST_METHOD"]
-     .must_equal "HEAD"
+    Rack::MockRequest.env_for("/", method: "HEAD")["REQUEST_METHOD"]
+      .must_equal "HEAD"
 
-    Rack::MockRequest.env_for("/", :method => "OPTIONS")["REQUEST_METHOD"]
-     .must_equal "OPTIONS"
+    Rack::MockRequest.env_for("/", method: "OPTIONS")["REQUEST_METHOD"]
+      .must_equal "OPTIONS"
   end
 
   it "set content length" do
-    env = Rack::MockRequest.env_for("/", :input => "foo")
+    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
-    res = Rack::MockRequest.new(app).get("", :input => "foo")
+    res = Rack::MockRequest.new(app).get("", input: "foo")
     env = YAML.load(res.body)
     env["mock.postdata"].must_equal "foo"
 
-    res = Rack::MockRequest.new(app).post("", :input => StringIO.new("foo"))
+    res = Rack::MockRequest.new(app).post("", input: StringIO.new("foo"))
     env = YAML.load(res.body)
     env["mock.postdata"].must_equal "foo"
   end
@@ -146,7 +163,7 @@ describe Rack::MockRequest do
   end
 
   it "accept params and build query string for GET requests" do
-    res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => {:foo => {:bar => "1"}})
+    res = Rack::MockRequest.new(app).get("/foo?baz=2", params: { foo: { bar: "1" } })
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "GET"
     env["QUERY_STRING"].must_include "baz=2"
@@ -156,7 +173,7 @@ describe Rack::MockRequest do
   end
 
   it "accept raw input in params for GET requests" do
-    res = Rack::MockRequest.new(app).get("/foo?baz=2", :params => "foo[bar]=1")
+    res = Rack::MockRequest.new(app).get("/foo?baz=2", params: "foo[bar]=1")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "GET"
     env["QUERY_STRING"].must_include "baz=2"
@@ -166,7 +183,7 @@ describe Rack::MockRequest do
   end
 
   it "accept params and build url encoded params for POST requests" do
-    res = Rack::MockRequest.new(app).post("/foo", :params => {:foo => {:bar => "1"}})
+    res = Rack::MockRequest.new(app).post("/foo", params: { foo: { bar: "1" } })
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "POST"
     env["QUERY_STRING"].must_equal ""
@@ -176,7 +193,7 @@ describe Rack::MockRequest do
   end
 
   it "accept raw input in params for POST requests" do
-    res = Rack::MockRequest.new(app).post("/foo", :params => "foo[bar]=1")
+    res = Rack::MockRequest.new(app).post("/foo", params: "foo[bar]=1")
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "POST"
     env["QUERY_STRING"].must_equal ""
@@ -187,7 +204,7 @@ describe Rack::MockRequest do
 
   it "accept params and build multipart encoded params for POST requests" do
     files = Rack::Multipart::UploadedFile.new(File.join(File.dirname(__FILE__), "multipart", "file1.txt"))
-    res = Rack::MockRequest.new(app).post("/foo", :params => { "submit-name" => "Larry", "files" => files })
+    res = Rack::MockRequest.new(app).post("/foo", params: { "submit-name" => "Larry", "files" => files })
     env = YAML.load(res.body)
     env["REQUEST_METHOD"].must_equal "POST"
     env["QUERY_STRING"].must_equal ""
@@ -199,16 +216,16 @@ describe Rack::MockRequest do
 
   it "behave valid according to the Rack spec" do
     url = "https://bla.example.org:9292/meh/foo?bar"
-    Rack::MockRequest.new(app).get(url, :lint => true).
+    Rack::MockRequest.new(app).get(url, lint: true).
       must_be_kind_of Rack::MockResponse
   end
 
   it "call close on the original body object" do
     called = false
     body   = Rack::BodyProxy.new(['hi']) { called = true }
-    capp   = proc { |e| [200, {'Content-Type' => 'text/plain'}, body] }
+    capp   = proc { |e| [200, { 'Content-Type' => 'text/plain' }, body] }
     called.must_equal false
-    Rack::MockRequest.new(capp).get('/', :lint => true)
+    Rack::MockRequest.new(capp).get('/', lint: true)
     called.must_equal true
   end
 
@@ -216,13 +233,13 @@ describe Rack::MockRequest 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
+      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
@@ -248,7 +265,7 @@ describe Rack::MockResponse do
     res = Rack::MockRequest.new(app).get("/?status=307")
     res.must_be :redirect?
 
-    res = Rack::MockRequest.new(app).get("/?status=201", :lint => true)
+    res = Rack::MockRequest.new(app).get("/?status=201", lint: true)
     res.must_be :empty?
   end
 
@@ -263,6 +280,42 @@ describe Rack::MockResponse do
     res.location.must_be_nil
   end
 
+  it "provide access to session cookies" do
+    res = Rack::MockRequest.new(app).get("")
+    session_cookie = res.cookie("session_test")
+    session_cookie.value[0].must_equal "session_test"
+    session_cookie.domain.must_equal ".test.com"
+    session_cookie.path.must_equal "/"
+    session_cookie.secure.must_equal false
+    session_cookie.expires.must_be_nil
+  end
+
+  it "provide access to persistent cookies" do
+    res = Rack::MockRequest.new(app).get("")
+    persistent_cookie = res.cookie("persistent_test")
+    persistent_cookie.value[0].must_equal "persistent_test"
+    persistent_cookie.domain.must_be_nil
+    persistent_cookie.path.must_equal "/"
+    persistent_cookie.secure.must_equal false
+    persistent_cookie.expires.wont_be_nil
+    persistent_cookie.expires.must_be :<, (Time.now + 15552000)
+  end
+
+  it "provide access to secure cookies" do
+    res = Rack::MockRequest.new(app).get("")
+    secure_cookie = res.cookie("secure_test")
+    secure_cookie.value[0].must_equal "secure_test"
+    secure_cookie.domain.must_equal ".test.com"
+    secure_cookie.path.must_equal "/"
+    secure_cookie.secure.must_equal true
+    secure_cookie.expires.must_be_nil
+  end
+
+  it "return nil if a non existent cookie is requested" do
+    res = Rack::MockRequest.new(app).get("")
+    res.cookie("i_dont_exist").must_be_nil
+  end
+
   it "provide access to the HTTP body" do
     res = Rack::MockRequest.new(app).get("")
     res.body.must_match(/rack/)
@@ -270,7 +323,7 @@ describe Rack::MockResponse do
   end
 
   it "provide access to the Rack errors" do
-    res = Rack::MockRequest.new(app).get("/?error=foo", :lint => true)
+    res = Rack::MockRequest.new(app).get("/?error=foo", lint: true)
     res.must_be :ok?
     res.errors.wont_be :empty?
     res.errors.must_include "foo"
@@ -286,7 +339,7 @@ describe Rack::MockResponse do
 
   it "optionally make Rack errors fatal" do
     lambda {
-      Rack::MockRequest.new(app).get("/?error=foo", :fatal => true)
+      Rack::MockRequest.new(app).get("/?error=foo", fatal: true)
     }.must_raise Rack::MockRequest::FatalWarning
   end
 end
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 2f957d92ab510a0b87ffdce7d38c2d62d1c63bc9..dbf6fba4d2a29fdcee9d263ede719b2be2e60f58 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -1,11 +1,12 @@
-# coding: utf-8
+# frozen_string_literal: true
 
-require 'minitest/autorun'
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/multipart'
 require 'rack/multipart/parser'
 require 'rack/utils'
 require 'rack/mock'
+require 'timeout'
 
 describe Rack::Multipart do
   def multipart_fixture(name, boundary = "AaB03x")
@@ -107,11 +108,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
@@ -136,7 +132,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,
     }
 
@@ -151,6 +147,31 @@ describe Rack::Multipart do
     wr.close
   end
 
+  # see https://github.com/rack/rack/pull/1309
+  it "parse strange multipart pdf" do
+    boundary = '---------------------------932620571087722842402766118'
+
+    data = StringIO.new
+    data.write("--#{boundary}")
+    data.write("\r\n")
+    data.write('Content-Disposition: form-data; name="a"; filename="a.pdf"')
+    data.write("\r\n")
+    data.write("Content-Type:application/pdf\r\n")
+    data.write("\r\n")
+    data.write("-" * (1024 * 1024))
+    data.write("\r\n")
+    data.write("--#{boundary}--\r\n")
+
+    fixture = {
+      "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}",
+      "CONTENT_LENGTH" => data.length.to_s,
+      :input => data,
+    }
+
+    env = Rack::MockRequest.env_for '/', fixture
+    Timeout::timeout(10) { Rack::Multipart.parse_multipart(env) }
+  end
+
   it 'raises an EOF error on content-length mistmatch' do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:empty))
     env['rack.input'] = StringIO.new
@@ -177,7 +198,7 @@ describe Rack::Multipart do
     c = Class.new(Rack::QueryParser::Params) do
       def initialize(*)
         super
-        @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)}
+        @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
     query_parser = Rack::QueryParser.new c, 65536, 100
@@ -206,7 +227,7 @@ describe Rack::Multipart do
 
   it "parse multipart upload file using custom tempfile class" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
-    my_tempfile = ""
+    my_tempfile = "".dup
     env['rack.multipart.tempfile_factory'] = lambda { |filename, content_type| my_tempfile }
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:tempfile].object_id.must_equal my_tempfile.object_id
@@ -311,6 +332,12 @@ describe Rack::Multipart do
     params["files"][:filename].must_equal "flowers.exe\u0000.jpg"
   end
 
+  it "is robust separating Content-Disposition fields" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:robust_field_separation))
+    params = Rack::Multipart.parse_multipart(env)
+    params["text"].must_equal "contents"
+  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)
@@ -368,6 +395,19 @@ describe Rack::Multipart do
     params["files"][:tempfile].read.must_equal "contents"
   end
 
+  it "parse filename with plus character" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_plus))
+    params = Rack::Multipart.parse_multipart(env)
+    params["files"][:type].must_equal "application/octet-stream"
+    params["files"][:filename].must_equal "foo+bar"
+    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+      "name=\"files\"; " +
+      "filename=\"foo+bar\"\r\n" +
+      "Content-Type: application/octet-stream\r\n"
+    params["files"][:name].must_equal "files"
+    params["files"][:tempfile].read.must_equal "contents"
+  end
+
   it "parse filename with percent escaped quotes" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_with_percent_escaped_quotes))
     params = Rack::Multipart.parse_multipart(env)
@@ -482,7 +522,7 @@ Content-Type: image/jpeg\r
 
   it "builds nested multipart body" do
     files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"))
-    data  = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => files}])
+    data  = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => files }])
 
     options = {
       "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
@@ -562,7 +602,7 @@ Content-Type: image/jpeg\r
   end
 
   it "return nil if no UploadedFiles were used" do
-    data = Rack::Multipart.build_multipart("people" => [{"submit-name" => "Larry", "files" => "contents"}])
+    data = Rack::Multipart.build_multipart("people" => [{ "submit-name" => "Larry", "files" => "contents" }])
     data.must_be_nil
   end
 
@@ -589,7 +629,7 @@ EOF
     env = Rack::MockRequest.env_for("/", options)
     params = Rack::Multipart.parse_multipart(env)
 
-    params.must_equal "description"=>"Very very blue"
+    params.must_equal "description" => "Very very blue"
   end
 
   it "parse multipart upload with no content-length header" do
@@ -688,7 +728,7 @@ true\r
   it "fallback to content-type for name" do
     rack_logo = File.read(multipart_file("rack-logo.png"))
 
-    data = <<-EOF
+    data = <<-EOF.dup
 --AaB03x\r
 Content-Type: text/plain\r
 \r
@@ -707,7 +747,7 @@ Content-Type: image/png\r
     options = {
       "CONTENT_TYPE" => "multipart/related; boundary=AaB03x",
       "CONTENT_LENGTH" => data.bytesize.to_s,
-      :input => StringIO.new(data)
+      :input => StringIO.new(data.dup)
     }
     env = Rack::MockRequest.env_for("/", options)
     params = Rack::Multipart.parse_multipart(env)
diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb
index 3002d97fc7f84cec6e80aefc2e7e364ebaa529a8..1037c9fa369b716d951f32581d34182e8504fc2e 100644
--- a/test/spec_null_logger.rb
+++ b/test/spec_null_logger.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/mock'
 require 'rack/null_logger'
@@ -7,14 +9,14 @@ describe Rack::NullLogger do
   it "act as a noop logger" do
     app = lambda { |env|
       env['rack.logger'].warn "b00m"
-      [200, {'Content-Type' => 'text/plain'}, ["Hello, World!"]]
+      [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]]
     }
 
     logger = Rack::Lint.new(Rack::NullLogger.new(app))
 
     res = logger.call(Rack::MockRequest.env_for)
     res[0..1].must_equal [
-      200, {'Content-Type' => 'text/plain'}
+      200, { 'Content-Type' => 'text/plain' }
     ]
     res[2].to_enum.to_a.must_equal ["Hello, World!"]
   end
diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb
index 4a60e6004b1a2371911965b67a06c82dea8bc02a..e77d966d5d611c192af0c35e6164964ae536d4de 100644
--- a/test/spec_recursive.rb
+++ b/test/spec_recursive.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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 cfaedbcfe6cdc9aff9377dca2248c62fc826b2ae..583a367e35fa2013b3b56e7e2401bbbec574e48c 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'cgi'
 require 'rack/request'
@@ -53,7 +55,7 @@ class RackRequestTest < Minitest::Spec
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     req.set_header 'foo', 'bar'
     hash = {}
-    req.each_header do |k,v|
+    req.each_header do |k, v|
       hash[k] = v
     end
     assert_equal 'bar', hash['foo']
@@ -230,7 +232,7 @@ class RackRequestTest < Minitest::Spec
     c = Class.new(Rack::QueryParser::Params) do
       def initialize(*)
         super
-        @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)}
+        @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
     parser = Rack::QueryParser.new(c, 65536, 100)
@@ -272,7 +274,7 @@ class RackRequestTest < Minitest::Spec
 
     old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3
     begin
-      exp = {"foo"=>{"bar"=>{"baz"=>{"qux"=>"1"}}}}
+      exp = { "foo" => { "bar" => { "baz" => { "qux" => "1" } } } }
       make_request(nested_query).GET.must_equal exp
       lambda { make_request(plain_query).GET  }.must_raise RangeError
     ensure
@@ -298,7 +300,7 @@ class RackRequestTest < Minitest::Spec
     c = Class.new(Rack::QueryParser::Params) do
       def initialize(*)
         super
-        @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)}
+        @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
     parser = Rack::QueryParser.new(c, 65536, 100)
@@ -727,7 +729,7 @@ class RackRequestTest < Minitest::Spec
   end
 
   it "provide setters" do
-    req = make_request(e=Rack::MockRequest.env_for(""))
+    req = make_request(e = Rack::MockRequest.env_for(""))
     req.script_name.must_equal ""
     req.script_name = "/foo"
     req.script_name.must_equal "/foo"
@@ -958,7 +960,7 @@ EOF
 --AaB03x\r
 content-disposition: form-data; name="huge"; filename="huge"\r
 \r
-#{"x"*32768}\r
+#{"x" * 32768}\r
 --AaB03x\r
 content-disposition: form-data; name="mean"; filename="mean"\r
 \r
@@ -1077,7 +1079,7 @@ EOF
 content-disposition: form-data; name="fileupload"; filename="junk.a"\r
 content-type: application/octet-stream\r
 \r
-#{[0x36,0xCF,0x0A,0xF8].pack('c*')}\r
+#{[0x36, 0xCF, 0x0A, 0xF8].pack('c*')}\r
 --AaB03x--\r
 EOF
 
@@ -1097,7 +1099,7 @@ EOF
     rack_input.rewind
 
     req = make_request Rack::MockRequest.env_for("/",
-                      "rack.request.form_hash" => {'foo' => 'bar'},
+                      "rack.request.form_hash" => { 'foo' => 'bar' },
                       "rack.request.form_input" => rack_input,
                       :input => rack_input)
 
@@ -1108,10 +1110,10 @@ EOF
     app = lambda { |env|
       content = make_request(env).POST["file"].inspect
       size = content.bytesize
-      [200, {"Content-Type" => "text/html", "Content-Length" => size.to_s}, [content]]
+      [200, { "Content-Type" => "text/html", "Content-Length" => size.to_s }, [content]]
     }
 
-    input = <<EOF
+    input = <<EOF.dup
 --AaB03x\r
 content-disposition: form-data; name="reply"\r
 \r
@@ -1221,6 +1223,20 @@ EOF
     res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '127.0.0.1, 3.4.5.6'
     res.body.must_equal '3.4.5.6'
 
+    # IPv6 format with optional port: "[2001:db8:cafe::17]:47011"
+    res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '[2001:db8:cafe::17]:47011'
+    res.body.must_equal '2001:db8:cafe::17'
+
+    res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, [2001:db8:cafe::17]:47011'
+    res.body.must_equal '2001:db8:cafe::17'
+
+    # IPv4 format with optional port: "192.0.2.43:47011"
+    res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '192.0.2.43:47011'
+    res.body.must_equal '192.0.2.43'
+
+    res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '1.2.3.4, 192.0.2.43:47011'
+    res.body.must_equal '192.0.2.43'
+
     res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => 'unknown,192.168.0.1'
     res.body.must_equal 'unknown'
 
@@ -1286,28 +1302,45 @@ EOF
     res.body.must_equal '2.2.2.3'
   end
 
-  it "regard local addresses as proxies" do
+  it "preserves ip for trusted proxy chain" do
+    mock = Rack::MockRequest.new(Rack::Lint.new(ip_app))
+    res = mock.get '/',
+      'HTTP_X_FORWARDED_FOR' => '192.168.0.11, 192.168.0.7',
+      'HTTP_CLIENT_IP' => '127.0.0.1'
+    res.body.must_equal '192.168.0.11'
+
+  end
+
+  it "uses a custom trusted proxy filter" do
+    old_ip = Rack::Request.ip_filter
+    Rack::Request.ip_filter = lambda { |ip| ip == 'foo' }
+    req = make_request(Rack::MockRequest.env_for("/"))
+    assert req.trusted_proxy?('foo')
+    Rack::Request.ip_filter = old_ip
+  end
+
+  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
@@ -1317,7 +1350,7 @@ EOF
 
   class MyRequest < Rack::Request
     def params
-      {:foo => "bar"}
+      { foo: "bar" }
     end
   end
 
@@ -1330,7 +1363,7 @@ EOF
 
     req2 = MyRequest.new(env)
     req2.GET.must_equal "foo" => "bar"
-    req2.params.must_equal :foo => "bar"
+    req2.params.must_equal foo: "bar"
   end
 
   it "allow parent request to be instantiated after subclass request" do
@@ -1338,7 +1371,7 @@ EOF
 
     req1 = MyRequest.new(env)
     req1.GET.must_equal "foo" => "bar"
-    req1.params.must_equal :foo => "bar"
+    req1.params.must_equal foo: "bar"
 
     req2 = make_request(env)
     req2.GET.must_equal "foo" => "bar"
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 987199de50526a555904f53638f98a2b3038564a..c0736c73d7240a094cb1ce40e9e5b993d735d441 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/response'
 require 'stringio'
@@ -9,7 +11,7 @@ describe Rack::Response do
     cc = 'foo'
     response.cache_control = cc
     assert_equal cc, response.cache_control
-    assert_equal cc, response.to_a[2]['Cache-Control']
+    assert_equal cc, response.to_a[1]['Cache-Control']
   end
 
   it 'has an etag method' do
@@ -17,7 +19,7 @@ describe Rack::Response do
     etag = 'foo'
     response.etag = etag
     assert_equal etag, response.etag
-    assert_equal etag, response.to_a[2]['ETag']
+    assert_equal etag, response.to_a[1]['ETag']
   end
 
   it "have sensible default values" do
@@ -60,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"
@@ -78,103 +90,121 @@ describe Rack::Response do
 
   it "can set cookies with the same name for multiple domains" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"}
-    response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
+    response.set_cookie "foo", { value: "bar", domain: "sample.example.com" }
+    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")
   end
 
   it "formats the Cookie expiration date accordingly to RFC 6265" do
     response = Rack::Response.new
 
-    response.set_cookie "foo", {:value => "bar", :expires => Time.now+10}
+    response.set_cookie "foo", { value: "bar", expires: Time.now + 10 }
     response["Set-Cookie"].must_match(
       /expires=..., \d\d ... \d\d\d\d \d\d:\d\d:\d\d .../)
   end
 
   it "can set secure cookies" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :secure => true}
+    response.set_cookie "foo", { value: "bar", secure: true }
     response["Set-Cookie"].must_equal "foo=bar; secure"
   end
 
   it "can set http only cookies" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :httponly => true}
+    response.set_cookie "foo", { value: "bar", httponly: true }
     response["Set-Cookie"].must_equal "foo=bar; HttpOnly"
   end
 
   it "can set http only cookies with :http_only" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :http_only => true}
+    response.set_cookie "foo", { value: "bar", http_only: true }
     response["Set-Cookie"].must_equal "foo=bar; HttpOnly"
   end
 
   it "can set prefers :httponly for http only cookie setting when :httponly and :http_only provided" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :httponly => false, :http_only => true}
+    response.set_cookie "foo", { value: "bar", httponly: false, http_only: true }
     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}
+    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 "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 "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 "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 "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 "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 "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"}
+      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 "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 "foo", { value: "bar", same_site: non_truthy }
       response["Set-Cookie"].must_equal "foo=bar"
     end
   end
@@ -186,32 +216,32 @@ 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
 
   it "can delete cookies with the same name from multiple domains" do
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :domain => "sample.example.com"}
-    response.set_cookie "foo", {:value => "bar", :domain => ".example.com"}
+    response.set_cookie "foo", { value: "bar", domain: "sample.example.com" }
+    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.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.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 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 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
     response = Rack::Response.new
-    response.set_cookie "foo", {:value => "bar", :path => "/"}
-    response.set_cookie "foo", {:value => "bar", :path => "/path"}
+    response.set_cookie "foo", { value: "bar", path: "/" }
+    response.set_cookie "foo", { value: "bar", path: "/path" }
     response["Set-Cookie"].must_equal ["foo=bar; path=/",
                                          "foo=bar; path=/path"].join("\n")
 
-    response.delete_cookie "foo", :path => "/path"
+    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
@@ -231,12 +261,12 @@ describe Rack::Response do
   it "has a useful constructor" do
     r = Rack::Response.new("foo")
     status, header, body = r.finish
-    str = ""; body.each { |part| str << part }
+    str = "".dup; body.each { |part| str << part }
     str.must_equal "foo"
 
     r = Rack::Response.new(["foo", "bar"])
     status, header, body = r.finish
-    str = ""; body.each { |part| str << part }
+    str = "".dup; body.each { |part| str << part }
     str.must_equal "foobar"
 
     object_with_each = Object.new
@@ -247,7 +277,7 @@ describe Rack::Response do
     r = Rack::Response.new(object_with_each)
     r.write "foo"
     status, header, body = r.finish
-    str = ""; body.each { |part| str << part }
+    str = "".dup; body.each { |part| str << part }
     str.must_equal "foobarfoo"
 
     r = Rack::Response.new([], 500)
@@ -263,7 +293,7 @@ describe Rack::Response do
       res.write "foo"
     }
     status, _, body = r.finish
-    str = ""; body.each { |part| str << part }
+    str = "".dup; body.each { |part| str << part }
     str.must_equal "foo"
     status.must_equal 404
   end
@@ -271,15 +301,15 @@ describe Rack::Response do
   it "doesn't return invalid responses" do
     r = Rack::Response.new(["foo", "bar"], 204)
     _, header, body = r.finish
-    str = ""; body.each { |part| str << part }
+    str = "".dup; body.each { |part| str << part }
     str.must_be :empty?
     header["Content-Type"].must_be_nil
     header['Content-Length'].must_be_nil
 
     lambda {
-      Rack::Response.new(Object.new)
-    }.must_raise(TypeError).
-      message.must_match(/stringable or iterable required/)
+      Rack::Response.new(Object.new).each{}
+    }.must_raise(NoMethodError).
+      message.must_match(/undefined method .each. for/)
   end
 
   it "knows if it's empty" do
@@ -403,6 +433,28 @@ describe Rack::Response do
     res.headers["Content-Length"].must_equal "8"
   end
 
+  it "does not wrap body" do
+    body = Object.new
+    res = Rack::Response.new(body)
+
+    # It was passed through unchanged:
+    res.finish.last.must_equal body
+  end
+
+  it "does wraps body when using #write" do
+    body = ["Foo"]
+    res = Rack::Response.new(body)
+
+    # Write something using the response object:
+    res.write("Bar")
+
+    # The original body was not modified:
+    body.must_equal ["Foo"]
+
+    # But a new buffered body was created:
+    res.finish.last.must_equal ["Foo", "Bar"]
+  end
+
   it "calls close on #body" do
     res = Rack::Response.new
     res.body = StringIO.new
@@ -421,12 +473,6 @@ describe Rack::Response do
     res.body.must_be :closed?
     b.wont_equal res.body
 
-    res.body = StringIO.new
-    res.status = 205
-    _, _, b = res.finish
-    res.body.wont_be :closed?
-    b.wont_equal res.body
-
     res.body = StringIO.new
     res.status = 304
     _, _, b = res.finish
@@ -434,10 +480,20 @@ describe Rack::Response do
     b.wont_equal res.body
   end
 
-  it "wraps the body from #to_ary to prevent infinite loops" do
+  it "doesn't call close on #body when 205" do
     res = Rack::Response.new
-    res.finish.last.wont_respond_to(:to_ary)
-    lambda { res.finish.last.to_ary }.must_raise NoMethodError
+
+    res.body = StringIO.new
+    res.status = 205
+    _, _, b = res.finish
+    res.body.wont_be :closed?
+  end
+
+  it "flatten doesn't cause infinite loop" do
+    # https://github.com/rack/rack/issues/419
+    res = Rack::Response.new("Hello World")
+
+    res.finish.flatten.must_be_kind_of(Array)
   end
 end
 
diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb
index 5adfba1de652d4e720bcdbc634479de1f6de8379..6bb5f5cf884600795efc945c551f65b6ae2a6616 100644
--- a/test/spec_rewindable_input.rb
+++ b/test/spec_rewindable_input.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'stringio'
 require 'rack/rewindable_input'
 
@@ -26,14 +28,14 @@ module RewindableTest
   end
 
   it "be able to handle to read(length, buffer)" do
-    buffer = ""
+    buffer = "".dup
     result = @rio.read(1, buffer)
     result.must_equal "h"
     result.object_id.must_equal buffer.object_id
   end
 
   it "be able to handle to read(nil, buffer)" do
-    buffer = ""
+    buffer = "".dup
     result = @rio.read(nil, buffer)
     result.must_equal "hello world"
     result.object_id.must_equal buffer.object_id
@@ -95,7 +97,7 @@ end
 describe Rack::RewindableInput do
   describe "given an IO object that is already rewindable" do
     def setup
-      @io = StringIO.new("hello world")
+      @io = StringIO.new("hello world".dup)
       super
     end
 
@@ -104,7 +106,7 @@ describe Rack::RewindableInput do
 
   describe "given an IO object that is not rewindable" do
     def setup
-      @io = StringIO.new("hello world")
+      @io = StringIO.new("hello world".dup)
       @io.instance_eval do
         undef :rewind
       end
@@ -116,7 +118,7 @@ describe Rack::RewindableInput do
 
   describe "given an IO object whose rewind method raises Errno::ESPIPE" do
     def setup
-      @io = StringIO.new("hello world")
+      @io = StringIO.new("hello world".dup)
       def @io.rewind
         raise Errno::ESPIPE, "You can't rewind this!"
       end
diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb
index f7f52ad9afd85b81a5ca477bb36260507d9bba60..10e561dec1ef6c049bc72c574eb39f2f67b2c457 100644
--- a/test/spec_runtime.rb
+++ b/test/spec_runtime.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/lint'
 require 'rack/mock'
 require 'rack/runtime'
@@ -13,25 +15,25 @@ describe Rack::Runtime do
   end
 
   it "sets X-Runtime is none is set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] }
     response = runtime_app(app).call(request)
     response[1]['X-Runtime'].must_match(/[\d\.]+/)
   end
 
   it "doesn't set the X-Runtime if it is already set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', "X-Runtime" => "foobar"}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', "X-Runtime" => "foobar" }, "Hello, World!"] }
     response = runtime_app(app).call(request)
     response[1]['X-Runtime'].must_equal "foobar"
   end
 
   it "allow a suffix to be set" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] }
     response = runtime_app(app, "Test").call(request)
     response[1]['X-Runtime-Test'].must_match(/[\d\.]+/)
   end
 
   it "allow multiple timers to be set" do
-    app = lambda { |env| sleep 0.1; [200, {'Content-Type' => 'text/plain'}, "Hello, World!"] }
+    app = lambda { |env| sleep 0.1; [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] }
     runtime = runtime_app(app, "App")
 
     # wrap many times to guarantee a measurable difference
diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb
index 1868985750e36dd07f2ef31069884774dd0306af..cbed8db3c1c812c8fcfcc4edff1009dd705b93a7 100644
--- a/test/spec_sendfile.rb
+++ b/test/spec_sendfile.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'fileutils'
 require 'rack/lint'
 require 'rack/sendfile'
@@ -6,22 +8,22 @@ 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
 
-  def simple_app(body=sendfile_body)
-    lambda { |env| [200, {'Content-Type' => 'text/plain'}, body] }
+  def simple_app(body = sendfile_body)
+    lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
   end
 
   def sendfile_app(body, mappings = [])
     Rack::Lint.new Rack::Sendfile.new(simple_app(body), nil, mappings)
   end
 
-  def request(headers={}, body=sendfile_body, mappings=[])
+  def request(headers = {}, body = sendfile_body, mappings = [])
     yield Rack::MockRequest.new(sendfile_app(body, mappings)).get('/', headers)
   end
 
@@ -72,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?
@@ -82,7 +97,7 @@ describe Rack::Sendfile do
   end
 
   it 'does nothing when body does not respond to #to_path' do
-    request({'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile'}, ['Not a file...']) do |response|
+    request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' }, ['Not a file...']) do |response|
       response.body.must_equal 'Not a file...'
       response.headers.wont_include 'X-Sendfile'
     end
@@ -104,14 +119,49 @@ describe Rack::Sendfile do
         ["#{dir2}/", '/wibble/']
       ]
 
-      request({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, first_body, mappings) do |response|
+      request({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, first_body, mappings) 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({ 'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect' }, second_body, mappings) 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
+
+  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({'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect'}, second_body, mappings) do |response|
+      request(headers, second_body) do |response|
         response.must_be :ok?
         response.body.must_be :empty?
         response.headers['Content-Length'].must_equal '0'
diff --git a/test/spec_server.rb b/test/spec_server.rb
index 4864a87a4c6de2ddc02d249a3376d76d9238651a..8aecc554cea8cd1652844ee259799c05142e1860 100644
--- a/test/spec_server.rb
+++ b/test/spec_server.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack'
 require 'rack/server'
 require 'tempfile'
@@ -15,7 +17,7 @@ describe Rack::Server do
   before { SPEC_ARGV[0..-1] = [] }
 
   def app
-    lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }
+    lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['success']] }
   end
 
   def with_stderr
@@ -26,12 +28,12 @@ describe Rack::Server do
   end
 
   it "overrides :config if :app is passed in" do
-    server = Rack::Server.new(:app => "FOO")
+    server = Rack::Server.new(app: "FOO")
     server.app.must_equal "FOO"
   end
 
   it "prefer to use :builder when it is passed in" do
-    server = Rack::Server.new(:builder => "run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }")
+    server = Rack::Server.new(builder: "run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['success']] }")
     server.app.class.must_equal Proc
     Rack::MockRequest.new(server.app).get("/").body.to_s.must_equal 'success'
   end
@@ -39,13 +41,13 @@ describe Rack::Server do
   it "allow subclasses to override middleware" do
     server = Class.new(Rack::Server).class_eval { def middleware; Hash.new [] end; self }
     server.middleware['deployment'].wont_equal []
-    server.new(:app => 'foo').middleware['deployment'].must_equal []
+    server.new(app: 'foo').middleware['deployment'].must_equal []
   end
 
   it "allow subclasses to override default middleware" do
     server = Class.new(Rack::Server).instance_eval { def default_middleware_by_environment; Hash.new [] end; self }
     server.middleware['deployment'].must_equal []
-    server.new(:app => 'foo').middleware['deployment'].must_equal []
+    server.new(app: 'foo').middleware['deployment'].must_equal []
   end
 
   it "only provide default middleware for development and deployment environments" do
@@ -53,29 +55,29 @@ describe Rack::Server do
   end
 
   it "always return an empty array for unknown environments" do
-    server = Rack::Server.new(:app => 'foo')
+    server = Rack::Server.new(app: 'foo')
     server.middleware['production'].must_equal []
   end
 
   it "not include Rack::Lint in deployment environment" do
-    server = Rack::Server.new(:app => 'foo')
+    server = Rack::Server.new(app: 'foo')
     server.middleware['deployment'].flatten.wont_include Rack::Lint
   end
 
   it "not include Rack::ShowExceptions in deployment environment" do
-    server = Rack::Server.new(:app => 'foo')
+    server = Rack::Server.new(app: 'foo')
     server.middleware['deployment'].flatten.wont_include Rack::ShowExceptions
   end
 
   it "include Rack::TempfileReaper in deployment environment" do
-    server = Rack::Server.new(:app => 'foo')
+    server = Rack::Server.new(app: 'foo')
     server.middleware['deployment'].flatten.must_include Rack::TempfileReaper
   end
 
   it "support CGI" do
     begin
       o, ENV["REQUEST_METHOD"] = ENV["REQUEST_METHOD"], 'foo'
-      server = Rack::Server.new(:app => 'foo')
+      server = Rack::Server.new(app: 'foo')
       server.server.name =~ /CGI/
       Rack::Server.logging_middleware.call(server).must_be_nil
     ensure
@@ -84,7 +86,7 @@ describe Rack::Server do
   end
 
   it "be quiet if said so" do
-    server = Rack::Server.new(:app => "FOO", :quiet => true)
+    server = Rack::Server.new(app: "FOO", quiet: true)
     Rack::Server.logging_middleware.call(server).must_be_nil
   end
 
@@ -118,19 +120,23 @@ describe Rack::Server do
     pidfile = Tempfile.open('pidfile') { |f| break f }
     FileUtils.rm pidfile.path
     server = Rack::Server.new(
-      :app         => app,
-      :environment => 'none',
-      :pid         => pidfile.path,
-      :Port        => TCPServer.open('127.0.0.1', 0){|s| s.addr[1] },
-      :Host        => '127.0.0.1',
-      :Logger      => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
-      :AccessLog   => [],
-      :daemonize   => false,
-      :server      => 'webrick'
+      app: app,
+      environment: 'none',
+      pid: pidfile.path,
+      Port: TCPServer.open('127.0.0.1', 0){|s| s.addr[1] },
+      Host: '127.0.0.1',
+      Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
+      AccessLog: [],
+      daemonize: false,
+      server: 'webrick'
     )
     t = Thread.new { server.start { |s| Thread.current[:server] = s } }
     t.join(0.01) until t[:server] && t[:server].status != :Stop
-    body = open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read }
+    body = if URI.respond_to?(:open)
+             URI.open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read }
+           else
+             open("http://127.0.0.1:#{server.options[:Port]}/") { |f| f.read }
+           end
     body.must_equal 'success'
 
     Process.kill(:INT, $$)
@@ -140,34 +146,36 @@ describe Rack::Server do
 
   it "check pid file presence and running process" do
     pidfile = Tempfile.open('pidfile') { |f| f.write($$); break f }.path
-    server = Rack::Server.new(:pid => pidfile)
+    server = Rack::Server.new(pid: pidfile)
     server.send(:pidfile_process_status).must_equal :running
   end
 
   it "check pid file presence and dead process" do
     dead_pid = `echo $$`.to_i
     pidfile = Tempfile.open('pidfile') { |f| f.write(dead_pid); break f }.path
-    server = Rack::Server.new(:pid => pidfile)
+    server = Rack::Server.new(pid: pidfile)
     server.send(:pidfile_process_status).must_equal :dead
   end
 
   it "check pid file presence and exited process" do
     pidfile = Tempfile.open('pidfile') { |f| break f }.path
     ::File.delete(pidfile)
-    server = Rack::Server.new(:pid => pidfile)
+    server = Rack::Server.new(pid: pidfile)
     server.send(:pidfile_process_status).must_equal :exited
   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 = Rack::Server.new(pid: pidfile)
     server.send(:pidfile_process_status).must_equal :not_owned
   end
 
   it "not write pid file when it is created after check" do
     pidfile = Tempfile.open('pidfile') { |f| break f }.path
     ::File.delete(pidfile)
-    server = Rack::Server.new(:pid => pidfile)
+    server = Rack::Server.new(pid: pidfile)
     ::File.open(pidfile, 'w') { |f| f.write(1) }
     with_stderr do |err|
       lambda { server.send(:write_pid) }.must_raise SystemExit
@@ -180,7 +188,7 @@ describe Rack::Server do
 
   it "inform the user about existing pidfiles with running processes" do
     pidfile = Tempfile.open('pidfile') { |f| f.write(1); break f }.path
-    server = Rack::Server.new(:pid => pidfile)
+    server = Rack::Server.new(pid: pidfile)
     with_stderr do |err|
       lambda { server.start }.must_raise SystemExit
       err.rewind
diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb
index a6568f19842b87f61e87ea857db7671f5b2e71aa..3591a3dea145f09e15465f149130040f51b99c71 100644
--- a/test/spec_session_abstract_id.rb
+++ b/test/spec_session_abstract_id.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 ### WARNING: there be hax in this file.
 
 require 'rack/session/abstract/id'
@@ -24,7 +26,7 @@ describe Rack::Session::Abstract::ID do
         'fake_hex'
       end
     end
-    id = Rack::Session::Abstract::ID.new nil, :secure_random => secure_random.new
+    id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new
     id.send(:generate_sid).must_equal 'fake_hex'
   end
 
diff --git a/test/spec_session_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb
index 76b34a01ec99786e4609b137559f083889b65044..5d0d10ce4f23ef8a9ec087788ab140ad6a514f96 100644
--- a/test/spec_session_abstract_session_hash.rb
+++ b/test/spec_session_abstract_session_hash.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/session/abstract/id'
 
 describe Rack::Session::Abstract::SessionHash do
@@ -8,7 +10,7 @@ describe Rack::Session::Abstract::SessionHash do
     super
     store = Class.new do
       def load_session(req)
-        ["id", {foo: :bar, baz: :qux}]
+        ["id", { foo: :bar, baz: :qux }]
       end
       def session_exists?(req)
         true
@@ -42,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 9201a72954500b4cbf151cb846cc5a838ba1074b..57850239e55696c754d6087c3bdef668e1017b88 100644
--- a/test/spec_session_cookie.rb
+++ b/test/spec_session_cookie.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/session/cookie'
 require 'rack/lint'
 require 'rack/mock'
@@ -45,7 +47,7 @@ describe Rack::Session::Cookie do
     Rack::Response.new("Nothing").to_a
   end
 
-  def response_for(options={})
+  def response_for(options = {})
     request_options = options.fetch(:request, {})
     cookie = if options[:cookie].is_a?(Rack::Response)
       options[:cookie]["Set-Cookie"]
@@ -74,26 +76,32 @@ describe Rack::Session::Cookie do
     it 'uses base64 to encode' do
       coder = Rack::Session::Cookie::Base64.new
       str   = 'fuuuuu'
-      coder.encode(str).must_equal [str].pack('m')
+      coder.encode(str).must_equal [str].pack('m0')
     end
 
     it 'uses base64 to decode' do
       coder = Rack::Session::Cookie::Base64.new
-      str   = ['fuuuuu'].pack('m')
-      coder.decode(str).must_equal str.unpack('m').first
+      str   = ['fuuuuu'].pack('m0')
+      coder.decode(str).must_equal str.unpack('m0').first
+    end
+
+    it 'handles non-strict base64 encoding' do
+      coder = Rack::Session::Cookie::Base64.new
+      str   = ['A' * 256].pack('m')
+      coder.decode(str).must_equal 'A' * 256
     end
 
     describe 'Marshal' do
       it 'marshals and base64 encodes' do
         coder = Rack::Session::Cookie::Base64::Marshal.new
         str   = 'fuuuuu'
-        coder.encode(str).must_equal [::Marshal.dump(str)].pack('m')
+        coder.encode(str).must_equal [::Marshal.dump(str)].pack('m0')
       end
 
       it 'marshals and base64 decodes' do
         coder = Rack::Session::Cookie::Base64::Marshal.new
-        str   = [::Marshal.dump('fuuuuu')].pack('m')
-        coder.decode(str).must_equal ::Marshal.load(str.unpack('m').first)
+        str   = [::Marshal.dump('fuuuuu')].pack('m0')
+        coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first)
       end
 
       it 'rescues failures on decode' do
@@ -106,13 +114,13 @@ describe Rack::Session::Cookie 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')
+        coder.encode(obj).must_equal [::JSON.dump(obj)].pack('m0')
       end
 
       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)
+        str   = [::JSON.dump(%w[fuuuuu])].pack('m0')
+        coder.decode(str).must_equal ::JSON.parse(str.unpack('m0').first)
       end
 
       it 'rescues failures on decode' do
@@ -126,14 +134,14 @@ describe Rack::Session::Cookie do
         coder = Rack::Session::Cookie::Base64::ZipJSON.new
         obj   = %w[fuuuuu]
         json = JSON.dump(obj)
-        coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m')
+        coder.encode(obj).must_equal [Zlib::Deflate.deflate(json)].pack('m0')
       end
 
       it 'base64 decodes, inflates, and decodes json' do
         coder = Rack::Session::Cookie::Base64::ZipJSON.new
         obj   = %w[fuuuuu]
         json  = JSON.dump(obj)
-        b64   = [Zlib::Deflate.deflate(json)].pack('m')
+        b64   = [Zlib::Deflate.deflate(json)].pack('m0')
         coder.decode(b64).must_equal obj
       end
 
@@ -148,22 +156,22 @@ describe Rack::Session::Cookie do
     Rack::Session::Cookie.new(incrementor)
     @warnings.first.must_match(/no secret/i)
     @warnings.clear
-    Rack::Session::Cookie.new(incrementor, :secret => 'abc')
+    Rack::Session::Cookie.new(incrementor, secret: 'abc')
     @warnings.must_be :empty?
   end
 
   it "doesn't warn if coder is configured to handle encoding" do
     Rack::Session::Cookie.new(
       incrementor,
-      :coder => Object.new,
-      :let_coder_handle_secure_encoding => true)
+      coder: Object.new,
+      let_coder_handle_secure_encoding: true)
     @warnings.must_be :empty?
   end
 
   it "still warns if coder is not set" do
     Rack::Session::Cookie.new(
       incrementor,
-      :let_coder_handle_secure_encoding => true)
+      let_coder_handle_secure_encoding: true)
     @warnings.first.must_match(/no secret/i)
   end
 
@@ -178,7 +186,7 @@ describe Rack::Session::Cookie do
       def encode(str); @calls << :encode; str; end
       def decode(str); @calls << :decode; str; end
     }.new
-    response = response_for(:app => [incrementor, { :coder => identity }])
+    response = response_for(app: [incrementor, { coder: identity }])
 
     response["Set-Cookie"].must_include "rack.session="
     response.body.must_equal '{"counter"=>1}'
@@ -186,47 +194,47 @@ describe Rack::Session::Cookie do
   end
 
   it "creates a new cookie" do
-    response = response_for(:app => incrementor)
+    response = response_for(app: incrementor)
     response["Set-Cookie"].must_include "rack.session="
     response.body.must_equal '{"counter"=>1}'
   end
 
   it "loads from a cookie" do
-    response = response_for(:app => incrementor)
+    response = response_for(app: incrementor)
 
-    response = response_for(:app => incrementor, :cookie => response)
+    response = response_for(app: incrementor, cookie: response)
     response.body.must_equal '{"counter"=>2}'
 
-    response = response_for(:app => incrementor, :cookie => response)
+    response = response_for(app: incrementor, cookie: response)
     response.body.must_equal '{"counter"=>3}'
   end
 
   it "renew session id" do
-    response = response_for(:app => incrementor)
+    response = response_for(app: incrementor)
     cookie   = response['Set-Cookie']
-    response = response_for(:app => only_session_id, :cookie => cookie)
+    response = response_for(app: only_session_id, cookie: cookie)
     cookie   = response['Set-Cookie'] if response['Set-Cookie']
 
     response.body.wont_equal ""
     old_session_id = response.body
 
-    response = response_for(:app => renewer, :cookie => cookie)
+    response = response_for(app: renewer, cookie: cookie)
     cookie   = response['Set-Cookie'] if response['Set-Cookie']
-    response = response_for(:app => only_session_id, :cookie => cookie)
+    response = response_for(app: only_session_id, cookie: cookie)
 
     response.body.wont_equal ""
     response.body.wont_equal old_session_id
   end
 
   it "destroys session" do
-    response = response_for(:app => incrementor)
-    response = response_for(:app => only_session_id, :cookie => response)
+    response = response_for(app: incrementor)
+    response = response_for(app: only_session_id, cookie: response)
 
     response.body.wont_equal ""
     old_session_id = response.body
 
-    response = response_for(:app => destroy_session, :cookie => response)
-    response = response_for(:app => only_session_id, :cookie => response)
+    response = response_for(app: destroy_session, cookie: response)
+    response = response_for(app: only_session_id, cookie: response)
 
     response.body.wont_equal ""
     response.body.wont_equal old_session_id
@@ -234,104 +242,104 @@ describe Rack::Session::Cookie do
 
   it "survives broken cookies" do
     response = response_for(
-      :app => incrementor,
-      :cookie => "rack.session=blarghfasel"
+      app: incrementor,
+      cookie: "rack.session=blarghfasel"
     )
     response.body.must_equal '{"counter"=>1}'
 
     response = response_for(
-      :app => [incrementor, { :secret => "test" }],
-      :cookie => "rack.session="
+      app: [incrementor, { secret: "test" }],
+      cookie: "rack.session="
     )
     response.body.must_equal '{"counter"=>1}'
   end
 
   it "barks on too big cookies" do
     lambda{
-      response_for(:app => bigcookie, :request => { :fatal => true })
+      response_for(app: bigcookie, request: { fatal: true })
     }.must_raise Rack::MockRequest::FatalWarning
   end
 
   it "loads from a cookie with integrity hash" do
-    app = [incrementor, { :secret => "test" }]
+    app = [incrementor, { secret: "test" }]
 
-    response = response_for(:app => app)
-    response = response_for(:app => app, :cookie => response)
+    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 = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>3}'
 
-    app = [incrementor, { :secret => "other" }]
+    app = [incrementor, { secret: "other" }]
 
-    response = response_for(:app => app, :cookie => response)
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>1}'
   end
 
   it "loads from a cookie with accept-only integrity hash for graceful key rotation" do
-    response = response_for(:app => [incrementor, { :secret => "test" }])
+    response = response_for(app: [incrementor, { secret: "test" }])
 
-    app = [incrementor, { :secret => "test2", :old_secret => "test" }]
-    response = response_for(:app => app, :cookie => response)
+    app = [incrementor, { secret: "test2", old_secret: "test" }]
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>2}'
 
-    app = [incrementor, { :secret => "test3", :old_secret => "test2" }]
-    response = response_for(:app => app, :cookie => response)
+    app = [incrementor, { secret: "test3", old_secret: "test2" }]
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>3}'
   end
 
   it "ignores tampered with session cookies" do
-    app = [incrementor, { :secret => "test" }]
-    response = response_for(:app => app)
+    app = [incrementor, { secret: "test" }]
+    response = response_for(app: app)
     response.body.must_equal '{"counter"=>1}'
 
-    response = response_for(:app => app, :cookie => response)
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>2}'
 
     _, digest = response["Set-Cookie"].split("--")
     tampered_with_cookie = "hackerman-was-here" + "--" + digest
 
-    response = response_for(:app => app, :cookie => tampered_with_cookie)
+    response = response_for(app: app, cookie: tampered_with_cookie)
     response.body.must_equal '{"counter"=>1}'
   end
 
   it "supports either of secret or old_secret" do
-    app = [incrementor, { :secret => "test" }]
-    response = response_for(:app => app)
+    app = [incrementor, { secret: "test" }]
+    response = response_for(app: app)
     response.body.must_equal '{"counter"=>1}'
 
-    response = response_for(:app => app, :cookie => response)
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>2}'
 
-    app = [incrementor, { :old_secret => "test" }]
-    response = response_for(:app => app)
+    app = [incrementor, { old_secret: "test" }]
+    response = response_for(app: app)
     response.body.must_equal '{"counter"=>1}'
 
-    response = response_for(:app => app, :cookie => response)
+    response = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>2}'
   end
 
   it "supports custom digest class" do
-    app = [incrementor, { :secret => "test", hmac: OpenSSL::Digest::SHA256 }]
+    app = [incrementor, { secret: "test", hmac: OpenSSL::Digest::SHA256 }]
 
-    response = response_for(:app => app)
-    response = response_for(:app => app, :cookie => response)
+    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 = response_for(app: app, cookie: response)
     response.body.must_equal '{"counter"=>3}'
 
-    app = [incrementor, { :secret => "other" }]
+    app = [incrementor, { secret: "other" }]
 
-    response = response_for(:app => app, :cookie => response)
+    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)
+    response = response_for(app: incrementor)
 
     lint = Rack::Lint.new(session_id)
-    response = response_for(:app => lint, :cookie => response)
+    response = response_for(app: lint, cookie: response)
     response.body.wont_be :nil?
   end
 
@@ -346,75 +354,75 @@ describe Rack::Session::Cookie do
       end
     end
 
-    response = response_for(:app => incrementor)
+    response = response_for(app: incrementor)
 
     inspector = TestEnvInspector.new(session_id)
-    response = response_for(:app => inspector, :cookie => response)
+    response = response_for(app: inspector, cookie: response)
     response.body.wont_be :nil?
   end
 
   it "returns the session id in the session hash" do
-    response = response_for(:app => incrementor)
+    response = response_for(app: incrementor)
     response.body.must_equal '{"counter"=>1}'
 
-    response = response_for(:app => session_id, :cookie => response)
+    response = response_for(app: session_id, cookie: response)
     response.body.must_match(/"session_id"=>/)
     response.body.must_match(/"counter"=>1/)
   end
 
   it "does not return a cookie if set to secure but not using ssl" do
-    app = [incrementor, { :secure => true }]
+    app = [incrementor, { secure: true }]
 
-    response = response_for(:app => app)
+    response = response_for(app: app)
     response["Set-Cookie"].must_be_nil
 
-    response = response_for(:app => app, :request => { "HTTPS" => "on" })
+    response = response_for(app: app, request: { "HTTPS" => "on" })
     response["Set-Cookie"].wont_be :nil?
     response["Set-Cookie"].must_match(/secure/)
   end
 
   it "does not return a cookie if cookie was not read/written" do
-    response = response_for(:app => nothing)
+    response = response_for(app: nothing)
     response["Set-Cookie"].must_be_nil
   end
 
   it "does not return a cookie if cookie was not written (only read)" do
-    response = response_for(:app => session_id)
+    response = response_for(app: session_id)
     response["Set-Cookie"].must_be_nil
   end
 
   it "returns even if not read/written if :expire_after is set" do
-    app = [nothing, { :expire_after => 3600 }]
-    request = { "rack.session" => { "not" => "empty" }}
-    response = response_for(:app => app, :request => request)
+    app = [nothing, { expire_after: 3600 }]
+    request = { "rack.session" => { "not" => "empty" } }
+    response = response_for(app: app, request: request)
     response["Set-Cookie"].wont_be :nil?
   end
 
   it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
-    app = [nothing, { :expire_after => 3600 }]
-    response = response_for(:app => app)
+    app = [nothing, { expire_after: 3600 }]
+    response = response_for(app: app)
     response["Set-Cookie"].must_be_nil
   end
 
   it "exposes :secret in env['rack.session.option']" do
-    response = response_for(:app => [session_option[:secret], { :secret => "foo" }])
+    response = response_for(app: [session_option[:secret], { secret: "foo" }])
     response.body.must_equal '"foo"'
   end
 
   it "exposes :coder in env['rack.session.option']" do
-    response = response_for(:app => session_option[:coder])
+    response = response_for(app: session_option[:coder])
     response.body.must_match(/Base64::Marshal/)
   end
 
   it "allows passing in a hash with session data from middleware in front" do
-    request = { 'rack.session' => { :foo => 'bar' }}
-    response = response_for(:app => session_id, :request => request)
+    request = { 'rack.session' => { foo: 'bar' } }
+    response = response_for(app: session_id, request: request)
     response.body.must_match(/foo/)
   end
 
   it "allows modifying session data with session data from middleware in front" do
-    request = { 'rack.session' => { :foo => 'bar' }}
-    response = response_for(:app => incrementor, :request => request)
+    request = { 'rack.session' => { foo: 'bar' } }
+    response = response_for(app: incrementor, request: request)
     response.body.must_match(/counter/)
     response.body.must_match(/foo/)
   end
@@ -423,7 +431,7 @@ describe Rack::Session::Cookie do
     @counter = 0
     app = lambda do |env|
       env["rack.session"]["message"] ||= ""
-      env["rack.session"]["message"] << "#{(@counter += 1).to_s}--"
+      env["rack.session"]["message"] += "#{(@counter += 1).to_s}--"
       hash = env["rack.session"].dup
       hash.delete("session_id")
       Rack::Response.new(hash["message"]).to_a
@@ -433,10 +441,44 @@ describe Rack::Session::Cookie do
       def encode(hash); hash.inspect end
       def decode(str); eval(str) if str; end
     }.new
-    _app = [ app, { :secret => "test", :coder => unsafe_coder } ]
-    response = response_for(:app => _app)
+    _app = [ app, { secret: "test", coder: unsafe_coder } ]
+    response = response_for(app: _app)
     response.body.must_equal "1--"
-    response = response_for(:app => _app, :cookie => response)
+    response = response_for(app: _app, cookie: response)
     response.body.must_equal "1--2--"
   end
+
+  it 'allows for non-strict encoded cookie' do
+    long_session_app = lambda do |env|
+      env['rack.session']['value'] = 'A' * 256
+      env['rack.session']['counter'] = 1
+      hash = env["rack.session"].dup
+      hash.delete("session_id")
+      Rack::Response.new(hash.inspect).to_a
+    end
+
+    non_strict_coder = Class.new {
+      def encode(str)
+        [Marshal.dump(str)].pack('m')
+      end
+
+      def decode(str)
+        return unless str
+
+        Marshal.load(str.unpack('m').first)
+      end
+    }.new
+
+    non_strict_response = response_for(app: [
+      long_session_app, { coder: non_strict_coder }
+    ])
+
+    response = response_for(app: [
+      incrementor
+    ], cookie: non_strict_response)
+
+    response.body.must_match %Q["value"=>"#{'A' * 256}"]
+    response.body.must_match '"counter"=>2'
+    response.body.must_match(/\A{[^}]+}\z/)
+  end
 end
diff --git a/test/spec_session_memcache.rb b/test/spec_session_memcache.rb
deleted file mode 100644
index 93a03d120569e2d7987368ca4d99b493316af178..0000000000000000000000000000000000000000
--- a/test/spec_session_memcache.rb
+++ /dev/null
@@ -1,320 +0,0 @@
-require 'minitest/autorun'
-begin
-  require 'rack/session/memcache'
-  require 'rack/lint'
-  require 'rack/mock'
-  require 'thread'
-
-  describe Rack::Session::Memcache do
-    session_key = Rack::Session::Memcache::DEFAULT_OPTIONS[:key]
-    session_match = /#{session_key}=([0-9a-fA-F]+);/
-    incrementor = lambda do |env|
-      env["rack.session"]["counter"] ||= 0
-      env["rack.session"]["counter"] += 1
-      Rack::Response.new(env["rack.session"].inspect).to_a
-    end
-    drop_session = Rack::Lint.new(proc do |env|
-      env['rack.session.options'][:drop] = true
-      incrementor.call(env)
-    end)
-    renew_session = Rack::Lint.new(proc do |env|
-      env['rack.session.options'][:renew] = true
-      incrementor.call(env)
-    end)
-    defer_session = Rack::Lint.new(proc do |env|
-      env['rack.session.options'][:defer] = true
-      incrementor.call(env)
-    end)
-    skip_session = Rack::Lint.new(proc do |env|
-      env['rack.session.options'][:skip] = true
-      incrementor.call(env)
-    end)
-    incrementor = Rack::Lint.new(incrementor)
-
-    # test memcache connection
-    Rack::Session::Memcache.new(incrementor)
-
-    it "faults on no connection" do
-      lambda {
-        Rack::Session::Memcache.new(incrementor, :memcache_server => 'nosuchserver')
-      }.must_raise(RuntimeError).message.must_equal 'No memcache servers'
-    end
-
-    it "connects to existing server" do
-      test_pool = MemCache.new(incrementor, :namespace => 'test:rack:session')
-      test_pool.namespace.must_equal 'test:rack:session'
-    end
-
-    it "passes options to MemCache" do
-      pool = Rack::Session::Memcache.new(incrementor, :namespace => 'test:rack:session')
-      pool.pool.namespace.must_equal 'test:rack:session'
-    end
-
-    it "creates a new cookie" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      res = Rack::MockRequest.new(pool).get("/")
-      res["Set-Cookie"].must_include "#{session_key}="
-      res.body.must_equal '{"counter"=>1}'
-    end
-
-    it "determines session from a cookie" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-      res = req.get("/")
-      cookie = res["Set-Cookie"]
-      req.get("/", "HTTP_COOKIE" => cookie).
-        body.must_equal '{"counter"=>2}'
-      req.get("/", "HTTP_COOKIE" => cookie).
-        body.must_equal '{"counter"=>3}'
-    end
-
-    it "determines session only from a cookie by default" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-      res = req.get("/")
-      sid = res["Set-Cookie"][session_match, 1]
-      req.get("/?rack.session=#{sid}").
-        body.must_equal '{"counter"=>1}'
-      req.get("/?rack.session=#{sid}").
-        body.must_equal '{"counter"=>1}'
-    end
-
-    it "determines session from params" do
-      pool = Rack::Session::Memcache.new(incrementor, :cookie_only => false)
-      req = Rack::MockRequest.new(pool)
-      res = req.get("/")
-      sid = res["Set-Cookie"][session_match, 1]
-      req.get("/?rack.session=#{sid}").
-        body.must_equal '{"counter"=>2}'
-      req.get("/?rack.session=#{sid}").
-        body.must_equal '{"counter"=>3}'
-    end
-
-    it "survives nonexistant cookies" do
-      bad_cookie = "rack.session=blarghfasel"
-      pool = Rack::Session::Memcache.new(incrementor)
-      res = Rack::MockRequest.new(pool).
-        get("/", "HTTP_COOKIE" => bad_cookie)
-      res.body.must_equal '{"counter"=>1}'
-      cookie = res["Set-Cookie"][session_match]
-      cookie.wont_match(/#{bad_cookie}/)
-    end
-
-    it "maintains freshness" do
-      pool = Rack::Session::Memcache.new(incrementor, :expire_after => 3)
-      res = Rack::MockRequest.new(pool).get('/')
-      res.body.must_include '"counter"=>1'
-      cookie = res["Set-Cookie"]
-      res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
-      res["Set-Cookie"].must_equal cookie
-      res.body.must_include '"counter"=>2'
-      puts 'Sleeping to expire session' if $DEBUG
-      sleep 4
-      res = Rack::MockRequest.new(pool).get('/', "HTTP_COOKIE" => cookie)
-      res["Set-Cookie"].wont_equal cookie
-      res.body.must_include '"counter"=>1'
-    end
-
-    it "does not send the same session id if it did not change" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-
-      res0 = req.get("/")
-      cookie = res0["Set-Cookie"][session_match]
-      res0.body.must_equal '{"counter"=>1}'
-
-      res1 = req.get("/", "HTTP_COOKIE" => cookie)
-      res1["Set-Cookie"].must_be_nil
-      res1.body.must_equal '{"counter"=>2}'
-
-      res2 = req.get("/", "HTTP_COOKIE" => cookie)
-      res2["Set-Cookie"].must_be_nil
-      res2.body.must_equal '{"counter"=>3}'
-    end
-
-    it "deletes cookies with :drop option" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-      drop = Rack::Utils::Context.new(pool, drop_session)
-      dreq = Rack::MockRequest.new(drop)
-
-      res1 = req.get("/")
-      session = (cookie = res1["Set-Cookie"])[session_match]
-      res1.body.must_equal '{"counter"=>1}'
-
-      res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
-      res2["Set-Cookie"].must_be_nil
-      res2.body.must_equal '{"counter"=>2}'
-
-      res3 = req.get("/", "HTTP_COOKIE" => cookie)
-      res3["Set-Cookie"][session_match].wont_equal session
-      res3.body.must_equal '{"counter"=>1}'
-    end
-
-    it "provides new session id with :renew option" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-      renew = Rack::Utils::Context.new(pool, renew_session)
-      rreq = Rack::MockRequest.new(renew)
-
-      res1 = req.get("/")
-      session = (cookie = res1["Set-Cookie"])[session_match]
-      res1.body.must_equal '{"counter"=>1}'
-
-      res2 = rreq.get("/", "HTTP_COOKIE" => cookie)
-      new_cookie = res2["Set-Cookie"]
-      new_session = new_cookie[session_match]
-      new_session.wont_equal session
-      res2.body.must_equal '{"counter"=>2}'
-
-      res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
-      res3.body.must_equal '{"counter"=>3}'
-
-      # Old cookie was deleted
-      res4 = req.get("/", "HTTP_COOKIE" => cookie)
-      res4.body.must_equal '{"counter"=>1}'
-    end
-
-    it "omits cookie with :defer option but still updates the state" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      count = Rack::Utils::Context.new(pool, incrementor)
-      defer = Rack::Utils::Context.new(pool, defer_session)
-      dreq = Rack::MockRequest.new(defer)
-      creq = Rack::MockRequest.new(count)
-
-      res0 = dreq.get("/")
-      res0["Set-Cookie"].must_be_nil
-      res0.body.must_equal '{"counter"=>1}'
-
-      res0 = creq.get("/")
-      res1 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"])
-      res1.body.must_equal '{"counter"=>2}'
-      res2 = dreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"])
-      res2.body.must_equal '{"counter"=>3}'
-    end
-
-    it "omits cookie and state update with :skip option" do
-      pool = Rack::Session::Memcache.new(incrementor)
-      count = Rack::Utils::Context.new(pool, incrementor)
-      skip = Rack::Utils::Context.new(pool, skip_session)
-      sreq = Rack::MockRequest.new(skip)
-      creq = Rack::MockRequest.new(count)
-
-      res0 = sreq.get("/")
-      res0["Set-Cookie"].must_be_nil
-      res0.body.must_equal '{"counter"=>1}'
-
-      res0 = creq.get("/")
-      res1 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"])
-      res1.body.must_equal '{"counter"=>2}'
-      res2 = sreq.get("/", "HTTP_COOKIE" => res0["Set-Cookie"])
-      res2.body.must_equal '{"counter"=>2}'
-    end
-
-    it "updates deep hashes correctly" do
-      hash_check = proc do |env|
-        session = env['rack.session']
-        unless session.include? 'test'
-          session.update :a => :b, :c => { :d => :e },
-            :f => { :g => { :h => :i} }, 'test' => true
-        else
-          session[:f][:g][:h] = :j
-        end
-        [200, {}, [session.inspect]]
-      end
-      pool = Rack::Session::Memcache.new(hash_check)
-      req = Rack::MockRequest.new(pool)
-
-      res0 = req.get("/")
-      session_id = (cookie = res0["Set-Cookie"])[session_match, 1]
-      ses0 = pool.pool.get(session_id, true)
-
-      req.get("/", "HTTP_COOKIE" => cookie)
-      ses1 = pool.pool.get(session_id, true)
-
-      ses1.wont_equal ses0
-    end
-
-    # anyone know how to do this better?
-    it "cleanly merges sessions when multithreaded" do
-      skip unless $DEBUG
-
-      warn 'Running multithread test for Session::Memcache'
-      pool = Rack::Session::Memcache.new(incrementor)
-      req = Rack::MockRequest.new(pool)
-
-      res = req.get('/')
-      res.body.must_equal '{"counter"=>1}'
-      cookie = res["Set-Cookie"]
-      session_id = cookie[session_match, 1]
-
-      delta_incrementor = lambda do |env|
-        # emulate disconjoinment of threading
-        env['rack.session'] = env['rack.session'].dup
-        Thread.stop
-        env['rack.session'][(Time.now.usec*rand).to_i] = true
-        incrementor.call(env)
-      end
-      tses = Rack::Utils::Context.new pool, delta_incrementor
-      treq = Rack::MockRequest.new(tses)
-      tnum = rand(7).to_i+5
-      r = Array.new(tnum) do
-        Thread.new(treq) do |run|
-          run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
-        end
-      end.reverse.map{|t| t.run.join.value }
-      r.each do |request|
-        request['Set-Cookie'].must_equal cookie
-        request.body.must_include '"counter"=>2'
-      end
-
-      session = pool.pool.get(session_id)
-      session.size.must_equal tnum+1 # counter
-      session['counter'].must_equal 2 # meeeh
-
-      tnum = rand(7).to_i+5
-      r = Array.new(tnum) do
-        app = Rack::Utils::Context.new pool, time_delta
-        req = Rack::MockRequest.new app
-        Thread.new(req) do |run|
-          run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
-        end
-      end.reverse.map{|t| t.run.join.value }
-      r.each do |request|
-        request['Set-Cookie'].must_equal cookie
-        request.body.must_include '"counter"=>3'
-      end
-
-      session = pool.pool.get(session_id)
-      session.size.must_equal tnum+1
-      session['counter'].must_equal 3
-
-      drop_counter = proc do |env|
-        env['rack.session'].delete 'counter'
-        env['rack.session']['foo'] = 'bar'
-        [200, {'Content-Type'=>'text/plain'}, env['rack.session'].inspect]
-      end
-      tses = Rack::Utils::Context.new pool, drop_counter
-      treq = Rack::MockRequest.new(tses)
-      tnum = rand(7).to_i+5
-      r = Array.new(tnum) do
-        Thread.new(treq) do |run|
-          run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
-        end
-      end.reverse.map{|t| t.run.join.value }
-      r.each do |request|
-        request['Set-Cookie'].must_equal cookie
-        request.body.must_include '"foo"=>"bar"'
-      end
-
-      session = pool.pool.get(session_id)
-      session.size.must_equal r.size+1
-      session['counter'].must_be_nil?
-      session['foo'].must_equal 'bar'
-    end
-  end
-rescue RuntimeError
-  $stderr.puts "Skipping Rack::Session::Memcache tests. Start memcached and try again."
-rescue LoadError
-  $stderr.puts "Skipping Rack::Session::Memcache tests (Memcache is required). `gem install memcache-client` and try again."
-end
diff --git a/test/spec_session_persisted_secure_secure_session_hash.rb b/test/spec_session_persisted_secure_secure_session_hash.rb
new file mode 100644
index 0000000000000000000000000000000000000000..21cbf8e6e5f544482b11d2a494545446a9050c31
--- /dev/null
+++ b/test/spec_session_persisted_secure_secure_session_hash.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
+require 'rack/session/abstract/id'
+
+describe Rack::Session::Abstract::PersistedSecure::SecureSessionHash do
+  attr_reader :hash
+
+  def setup
+    super
+    @store = Class.new do
+      def load_session(req)
+        [Rack::Session::SessionId.new("id"), { foo: :bar, baz: :qux }]
+      end
+      def session_exists?(req)
+        true
+      end
+    end
+    @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.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 "#[]" do
+    it "returns value for a matching key" do
+      assert_equal :bar, hash[:foo]
+    end
+
+    it "returns value for a 'session_id' key" do
+      assert_equal "id", hash['session_id']
+    end
+
+    it "returns nil value for missing 'session_id' key" do
+      store = @store.new
+      def store.load_session(req)
+        [nil, {}]
+      end
+      @hash = Rack::Session::Abstract::PersistedSecure::SecureSessionHash.new(store, nil)
+      assert_nil hash['session_id']
+    end
+  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
+
+  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_pool.rb b/test/spec_session_pool.rb
index 2d061691548636d151ce5ba72e8f87529f8f0565..dd1b6573f74e1553312b5dcfe51cf4af8985cd43 100644
--- a/test/spec_session_pool.rb
+++ b/test/spec_session_pool.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'thread'
 require 'rack/lint'
 require 'rack/mock'
@@ -6,7 +8,7 @@ require 'rack/session/pool'
 
 describe Rack::Session::Pool do
   session_key = Rack::Session::Pool::DEFAULT_OPTIONS[:key]
-  session_match = /#{session_key}=[0-9a-fA-F]+;/
+  session_match = /#{session_key}=([0-9a-fA-F]+);/
 
   incrementor = lambda do |env|
     env["rack.session"]["counter"] ||= 0
@@ -14,7 +16,7 @@ describe Rack::Session::Pool do
     Rack::Response.new(env["rack.session"].inspect).to_a
   end
 
-  session_id = Rack::Lint.new(lambda do |env|
+  get_session_id = Rack::Lint.new(lambda do |env|
     Rack::Response.new(env["rack.session"].inspect).to_a
   end)
 
@@ -143,6 +145,43 @@ describe Rack::Session::Pool do
     pool.pool.size.must_equal 1
   end
 
+  it "can read the session with the legacy id" do
+    pool = Rack::Session::Pool.new(incrementor)
+    req = Rack::MockRequest.new(pool)
+
+    res0 = req.get("/")
+    cookie = res0["Set-Cookie"]
+    session_id = Rack::Session::SessionId.new cookie[session_match, 1]
+    ses0 = pool.pool[session_id.private_id]
+    pool.pool[session_id.public_id] = ses0
+    pool.pool.delete(session_id.private_id)
+
+    res1 = req.get("/", "HTTP_COOKIE" => cookie)
+    res1["Set-Cookie"].must_be_nil
+    res1.body.must_equal '{"counter"=>2}'
+    pool.pool[session_id.private_id].wont_be_nil
+  end
+
+  it "drops the session in the legacy id as well" do
+    pool = Rack::Session::Pool.new(incrementor)
+    req = Rack::MockRequest.new(pool)
+    drop = Rack::Utils::Context.new(pool, drop_session)
+    dreq = Rack::MockRequest.new(drop)
+
+    res0 = req.get("/")
+    cookie = res0["Set-Cookie"]
+    session_id = Rack::Session::SessionId.new cookie[session_match, 1]
+    ses0 = pool.pool[session_id.private_id]
+    pool.pool[session_id.public_id] = ses0
+    pool.pool.delete(session_id.private_id)
+
+    res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
+    res2["Set-Cookie"].must_be_nil
+    res2.body.must_equal '{"counter"=>2}'
+    pool.pool[session_id.private_id].must_be_nil
+    pool.pool[session_id.public_id].must_be_nil
+  end
+
   # anyone know how to do this better?
   it "should merge sessions when multithreaded" do
     unless $DEBUG
@@ -157,18 +196,18 @@ describe Rack::Session::Pool do
     res = req.get('/')
     res.body.must_equal '{"counter"=>1}'
     cookie = res["Set-Cookie"]
-    sess_id = cookie[/#{pool.key}=([^,;]+)/,1]
+    sess_id = cookie[/#{pool.key}=([^,;]+)/, 1]
 
     delta_incrementor = lambda do |env|
       # emulate disconjoinment of threading
       env['rack.session'] = env['rack.session'].dup
       Thread.stop
-      env['rack.session'][(Time.now.usec*rand).to_i] = true
+      env['rack.session'][(Time.now.usec * rand).to_i] = true
       incrementor.call(env)
     end
     tses = Rack::Utils::Context.new pool, delta_incrementor
     treq = Rack::MockRequest.new(tses)
-    tnum = rand(7).to_i+5
+    tnum = rand(7).to_i + 5
     r = Array.new(tnum) do
       Thread.new(treq) do |run|
         run.get('/', "HTTP_COOKIE" => cookie, 'rack.multithread' => true)
@@ -180,7 +219,7 @@ describe Rack::Session::Pool do
     end
 
     session = pool.pool[sess_id]
-    session.size.must_equal tnum+1 # counter
+    session.size.must_equal tnum + 1 # counter
     session['counter'].must_equal 2 # meeeh
   end
 
@@ -191,19 +230,19 @@ describe Rack::Session::Pool do
   end
 
   it "does not return a cookie if cookie was not written (only read)" do
-    app = Rack::Session::Pool.new(session_id)
+    app = Rack::Session::Pool.new(get_session_id)
     res = Rack::MockRequest.new(app).get("/")
     res["Set-Cookie"].must_be_nil
   end
 
   it "returns even if not read/written if :expire_after is set" do
-    app = Rack::Session::Pool.new(nothing, :expire_after => 3600)
-    res = Rack::MockRequest.new(app).get("/", 'rack.session' => {'not' => 'empty'})
+    app = Rack::Session::Pool.new(nothing, expire_after: 3600)
+    res = Rack::MockRequest.new(app).get("/", 'rack.session' => { 'not' => 'empty' })
     res["Set-Cookie"].wont_be :nil?
   end
 
   it "returns no cookie if no data was written and no session was created previously, even if :expire_after is set" do
-    app = Rack::Session::Pool.new(nothing, :expire_after => 3600)
+    app = Rack::Session::Pool.new(nothing, expire_after: 3600)
     res = Rack::MockRequest.new(app).get("/")
     res["Set-Cookie"].must_be_nil
   end
diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb
index 61e5d92d692175a7d45a2d4c8d57fb1469932d56..a4ade121d5c844c0ada17534361b9caeefaf6c51 100644
--- a/test/spec_show_exceptions.rb
+++ b/test/spec_show_exceptions.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/show_exceptions'
 require 'rack/lint'
 require 'rack/mock'
@@ -25,6 +27,24 @@ describe Rack::ShowExceptions do
     assert_match(res, /ShowExceptions/)
   end
 
+  it "works with binary data in the Rack environment" do
+    res = nil
+
+    # "\xCC" is not a valid UTF-8 string
+    req = Rack::MockRequest.new(
+      show_exceptions(
+        lambda{|env| env['foo'] = "\xCC"; raise RuntimeError }
+    ))
+
+    res = req.get("/", "HTTP_ACCEPT" => "text/html")
+
+    res.must_be :server_error?
+    res.status.must_equal 500
+
+    assert_match(res, /RuntimeError/)
+    assert_match(res, /ShowExceptions/)
+  end
+
   it "responds with HTML only to requests accepting HTML" do
     res = nil
 
@@ -35,11 +55,11 @@ describe Rack::ShowExceptions do
 
     [
       # Serve text/html when the client accepts text/html
-      ["text/html", ["/", {"HTTP_ACCEPT" => "text/html"}]],
-      ["text/html", ["/", {"HTTP_ACCEPT" => "*/*"}]],
+      ["text/html", ["/", { "HTTP_ACCEPT" => "text/html" }]],
+      ["text/html", ["/", { "HTTP_ACCEPT" => "*/*" }]],
       # Serve text/plain when the client does not accept text/html
       ["text/plain", ["/"]],
-      ["text/plain", ["/", {"HTTP_ACCEPT" => "application/json"}]]
+      ["text/plain", ["/", { "HTTP_ACCEPT" => "application/json" }]]
     ].each do |exmime, rargs|
       res = req.get(*rargs)
 
@@ -78,6 +98,28 @@ describe Rack::ShowExceptions do
     assert_match(res, /unknown location/)
   end
 
+  it "allows subclasses to override template" do
+    c = Class.new(Rack::ShowExceptions) do
+      TEMPLATE = ERB.new("foo")
+
+      def template
+        TEMPLATE
+      end
+    end
+
+    app = lambda { |env| raise RuntimeError, "", [] }
+
+    req = Rack::MockRequest.new(
+      Rack::Lint.new c.new(app)
+    )
+
+    res = req.get("/", "HTTP_ACCEPT" => "text/html")
+
+    res.must_be :server_error?
+    res.status.must_equal 500
+    res.body.must_equal "foo"
+  end
+
   it "knows to prefer plaintext for non-html" do
     # We don't need an app for this
     exc = Rack::ShowExceptions.new(nil)
diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb
index d32dc7cb9594884fdd59f225a35d5b4546463a71..ca23134e0598ed424b5d68e8a3fa808755342b57 100644
--- a/test/spec_show_status.rb
+++ b/test/spec_show_status.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/show_status'
 require 'rack/lint'
 require 'rack/mock'
@@ -12,10 +14,10 @@ describe Rack::ShowStatus do
   it "provide a default status message" do
     req = Rack::MockRequest.new(
       show_status(lambda{|env|
-        [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+        [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
     }))
 
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
     res.must_be :not_found?
     res.wont_be_empty
 
@@ -29,10 +31,10 @@ describe Rack::ShowStatus do
       show_status(
         lambda{|env|
           env["rack.showstatus.detail"] = "gone too meta."
-          [404, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+          [404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
     }))
 
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
     res.must_be :not_found?
     res.wont_be_empty
 
@@ -48,10 +50,10 @@ describe Rack::ShowStatus do
       show_status(
         lambda{|env|
           env["rack.showstatus.detail"] = detail
-          [500, {"Content-Type" => "text/plain", "Content-Length" => "0"}, []]
+          [500, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
     }))
 
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
     res.wont_be_empty
 
     res["Content-Type"].must_equal "text/html"
@@ -64,21 +66,21 @@ describe Rack::ShowStatus do
     req = Rack::MockRequest.new(
       show_status(
         lambda{|env|
-          [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
+          [404, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["foo!"]]
     }))
 
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
     res.must_be :not_found?
 
     res.body.must_equal "foo!"
   end
 
   it "pass on original headers" do
-    headers = {"WWW-Authenticate" => "Basic blah"}
+    headers = { "WWW-Authenticate" => "Basic blah" }
 
     req = Rack::MockRequest.new(
       show_status(lambda{|env| [401, headers, []] }))
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
 
     res["WWW-Authenticate"].must_equal "Basic blah"
   end
@@ -88,10 +90,10 @@ describe Rack::ShowStatus do
       show_status(
         lambda{|env|
           env["rack.showstatus.detail"] = "gone too meta."
-          [404, {"Content-Type" => "text/plain", "Content-Length" => "4"}, ["foo!"]]
+          [404, { "Content-Type" => "text/plain", "Content-Length" => "4" }, ["foo!"]]
     }))
 
-    res = req.get("/", :lint => true)
+    res = req.get("/", lint: true)
     res.must_be :not_found?
     res.wont_be_empty
 
diff --git a/test/spec_static.rb b/test/spec_static.rb
index 634f8acf763e8e9e026f8079193311d91b25a401..d33e8edcbf143ec3a8e4316683f756e63d935579 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/static'
 require 'rack/lint'
 require 'rack/mock'
@@ -7,7 +9,7 @@ require 'stringio'
 
 class DummyApp
   def call(env)
-    [200, {"Content-Type" => "text/plain"}, ["Hello World"]]
+    [200, { "Content-Type" => "text/plain" }, ["Hello World"]]
   end
 end
 
@@ -18,15 +20,17 @@ describe Rack::Static do
 
   root = File.expand_path(File.dirname(__FILE__))
 
-  OPTIONS = {:urls => ["/cgi"], :root => root}
-  STATIC_OPTIONS = {:urls => [""], :root => "#{root}/static", :index => 'index.html'}
-  HASH_OPTIONS = {:urls => {"/cgi/sekret" => 'cgi/test'}, :root => root}
-  HASH_ROOT_OPTIONS = {:urls => {"/" => "static/foo.html"}, :root => root}
-  GZIP_OPTIONS = {:urls => ["/cgi"], :root => root, :gzip=>true}
+  OPTIONS = { urls: ["/cgi"], root: root }
+  STATIC_OPTIONS = { urls: [""], root: "#{root}/static", index: 'index.html' }
+  STATIC_URLS_OPTIONS = { urls: ["/static"], root: "#{root}", index: 'index.html' }
+  HASH_OPTIONS = { urls: { "/cgi/sekret" => 'cgi/test' }, root: root }
+  HASH_ROOT_OPTIONS = { urls: { "/" => "static/foo.html" }, root: root }
+  GZIP_OPTIONS = { urls: ["/cgi"], root: root, gzip: true }
 
   before do
   @request = Rack::MockRequest.new(static(DummyApp.new, OPTIONS))
   @static_request = Rack::MockRequest.new(static(DummyApp.new, STATIC_OPTIONS))
+  @static_urls_request = Rack::MockRequest.new(static(DummyApp.new, STATIC_URLS_OPTIONS))
   @hash_request = Rack::MockRequest.new(static(DummyApp.new, HASH_OPTIONS))
   @hash_root_request = Rack::MockRequest.new(static(DummyApp.new, HASH_ROOT_OPTIONS))
   @gzip_request = Rack::MockRequest.new(static(DummyApp.new, GZIP_OPTIONS))
@@ -63,6 +67,16 @@ describe Rack::Static do
     res.body.must_match(/another index!/)
   end
 
+  it "does not call index file when requesting folder with unknown prefix" do
+    res = @static_urls_request.get("/static/another/")
+    res.must_be :ok?
+    res.body.must_match(/index!/)
+
+    res = @static_urls_request.get("/something/else/")
+    res.must_be :ok?
+    res.body.must_equal "Hello World"
+  end
+
   it "doesn't call index file if :index option was omitted" do
     res = @request.get("/")
     res.body.must_equal "Hello World"
@@ -87,7 +101,7 @@ describe Rack::Static do
   end
 
   it "serves gzipped files if client accepts gzip encoding and gzip files are present" do
-    res = @gzip_request.get("/cgi/test", 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip')
+    res = @gzip_request.get("/cgi/test", 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip')
     res.must_be :ok?
     res.headers['Content-Encoding'].must_equal 'gzip'
     res.headers['Content-Type'].must_equal 'text/plain'
@@ -95,7 +109,7 @@ describe Rack::Static do
   end
 
   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 = @gzip_request.get("/cgi/rackup_stub.rb", 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip')
     res.must_be :ok?
     res.headers['Content-Encoding'].must_be_nil
     res.headers['Content-Type'].must_equal 'text/x-script.ruby'
@@ -111,21 +125,21 @@ describe Rack::Static do
   end
 
   it "supports serving fixed cache-control (legacy option)" do
-    opts = OPTIONS.merge(:cache_control => 'public')
+    opts = OPTIONS.merge(cache_control: 'public')
     request = Rack::MockRequest.new(static(DummyApp.new, opts))
     res = request.get("/cgi/test")
     res.must_be :ok?
     res.headers['Cache-Control'].must_equal 'public'
   end
 
-  HEADER_OPTIONS = {:urls => ["/cgi"], :root => root, :header_rules => [
-    [:all, {'Cache-Control' => 'public, max-age=100'}],
-    [:fonts, {'Cache-Control' => 'public, max-age=200'}],
-    [%w(png jpg), {'Cache-Control' => 'public, max-age=300'}],
-    ['/cgi/assets/folder/', {'Cache-Control' => 'public, max-age=400'}],
-    ['cgi/assets/javascripts', {'Cache-Control' => 'public, max-age=500'}],
-    [/\.(css|erb)\z/, {'Cache-Control' => 'public, max-age=600'}]
-  ]}
+  HEADER_OPTIONS = { urls: ["/cgi"], root: root, header_rules: [
+    [:all, { 'Cache-Control' => 'public, max-age=100' }],
+    [:fonts, { 'Cache-Control' => 'public, max-age=200' }],
+    [%w(png jpg), { 'Cache-Control' => 'public, max-age=300' }],
+    ['/cgi/assets/folder/', { 'Cache-Control' => 'public, max-age=400' }],
+    ['cgi/assets/javascripts', { 'Cache-Control' => 'public, max-age=500' }],
+    [/\.(css|erb)\z/, { 'Cache-Control' => 'public, max-age=600' }]
+  ] }
 
   it "supports header rule :all" do
     # Headers for all files via :all shortcut
@@ -170,9 +184,9 @@ describe Rack::Static do
 
   it "prioritizes header rules over fixed cache-control setting (legacy option)" do
     opts = OPTIONS.merge(
-      :cache_control => 'public, max-age=24',
-      :header_rules => [
-        [:all, {'Cache-Control' => 'public, max-age=42'}]
+      cache_control: 'public, max-age=24',
+      header_rules: [
+        [:all, { 'Cache-Control' => 'public, max-age=42' }]
       ])
 
     request = Rack::MockRequest.new(static(DummyApp.new, opts))
@@ -181,4 +195,14 @@ describe Rack::Static do
     res.headers['Cache-Control'].must_equal 'public, max-age=42'
   end
 
+  it "expands the root path upon the middleware initialization" do
+    relative_path = STATIC_OPTIONS[:root].sub("#{Dir.pwd}/", '')
+    opts = { urls: [""], root: relative_path, index: 'index.html' }
+    request = Rack::MockRequest.new(static(DummyApp.new, opts))
+    Dir.chdir '..' do
+      res = request.get("")
+      res.must_be :ok?
+      res.body.must_match(/index!/)
+    end
+  end
 end
diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb
index b7c6256390127e998a3080426e6c09cd255682c9..0e7de841503714ee113e0b0f6f496cfbcb7fa433 100644
--- a/test/spec_tempfile_reaper.rb
+++ b/test/spec_tempfile_reaper.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+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 85b225ed922e83142cc27d7e6d67d248e21a65a9..0729c3f3c39f7d29975efff20fabd984f69241ac 100644
--- a/test/spec_thin.rb
+++ b/test/spec_thin.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 begin
 require 'rack/handler/thin'
 require File.expand_path('../testrequest', __FILE__)
@@ -13,7 +15,7 @@ describe Rack::Handler::Thin do
     Thin::Logging.silent = true
 
     @thread = Thread.new do
-      Rack::Handler::Thin.run(@app, :Host => @host='127.0.0.1', :Port => @port=9204, :tag => "tag") do |server|
+      Rack::Handler::Thin.run(@app, Host: @host = '127.0.0.1', Port: @port = 9204, tag: "tag") do |server|
         @server = server
       end
     end
@@ -44,7 +46,7 @@ describe Rack::Handler::Thin do
 
   it "have rack headers" do
     GET("/")
-    response["rack.version"].must_equal [1,0]
+    response["rack.version"].must_equal [1, 0]
     response["rack.multithread"].must_equal false
     response["rack.multiprocess"].must_equal false
     response["rack.run_once"].must_equal false
@@ -66,7 +68,7 @@ describe Rack::Handler::Thin do
   end
 
   it "have CGI headers on POST" do
-    POST("/", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+    POST("/", { "rack-form-data" => "23" }, { 'X-test-header' => '42' })
     status.must_equal 200
     response["REQUEST_METHOD"].must_equal "POST"
     response["REQUEST_PATH"].must_equal "/"
@@ -76,7 +78,7 @@ describe Rack::Handler::Thin do
   end
 
   it "support HTTP auth" do
-    GET("/test", {:user => "ruth", :passwd => "secret"})
+    GET("/test", { user: "ruth", passwd: "secret" })
     response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ="
   end
 
diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb
index 9d655c22040221fb22f588d77149bb1f8854b2c2..9ce382987f56266bb28e2a85f924d37c6a04d5ee 100644
--- a/test/spec_urlmap.rb
+++ b/test/spec_urlmap.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/urlmap'
 require 'rack/mock'
 
@@ -117,6 +119,14 @@ describe Rack::URLMap do
     res.must_be :ok?
     res["X-Position"].must_equal "default.org"
 
+    res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "any-host.org")
+    res.must_be :ok?
+    res["X-Position"].must_equal "default.org"
+
+    res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "any-host.org", "HTTP_X_FORWARDED_HOST" => "any-host.org")
+    res.must_be :ok?
+    res["X-Position"].must_equal "default.org"
+
     res = Rack::MockRequest.new(map).get("/",
                                          "HTTP_HOST" => "example.org:9292",
                                          "SERVER_PORT" => "9292")
@@ -127,7 +137,7 @@ describe Rack::URLMap do
   it "be nestable" do
     map = Rack::Lint.new(Rack::URLMap.new("/foo" =>
       Rack::URLMap.new("/bar" =>
-        Rack::URLMap.new("/quux" =>  lambda { |env|
+        Rack::URLMap.new("/quux" => lambda { |env|
                            [200,
                             { "Content-Type" => "text/plain",
                               "X-Position" => "/foo/bar/quux",
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 143ad30a68eaa62a7c2349e727149da6a927ac15..5fd9224113847a1b4b784cb7d6ab84c5bb36eb75 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -1,5 +1,6 @@
-# -*- encoding: utf-8 -*-
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/utils'
 require 'rack/mock'
 require 'timeout'
@@ -72,7 +73,7 @@ describe Rack::Utils do
   end
 
   it "escape path spaces with %20" do
-    Rack::Utils.escape_path("foo bar").must_equal  "foo%20bar"
+    Rack::Utils.escape_path("foo bar").must_equal "foo%20bar"
   end
 
   it "unescape correctly" do
@@ -181,38 +182,38 @@ describe Rack::Utils do
       must_equal "foo" => ["bar"], "baz" => ["1", "2", "3"]
 
     Rack::Utils.parse_nested_query("x[y][z]=1").
-      must_equal "x" => {"y" => {"z" => "1"}}
+      must_equal "x" => { "y" => { "z" => "1" } }
     Rack::Utils.parse_nested_query("x[y][z][]=1").
-      must_equal "x" => {"y" => {"z" => ["1"]}}
+      must_equal "x" => { "y" => { "z" => ["1"] } }
     Rack::Utils.parse_nested_query("x[y][z]=1&x[y][z]=2").
-      must_equal "x" => {"y" => {"z" => "2"}}
+      must_equal "x" => { "y" => { "z" => "2" } }
     Rack::Utils.parse_nested_query("x[y][z][]=1&x[y][z][]=2").
-      must_equal "x" => {"y" => {"z" => ["1", "2"]}}
+      must_equal "x" => { "y" => { "z" => ["1", "2"] } }
 
     Rack::Utils.parse_nested_query("x[y][][z]=1").
-      must_equal "x" => {"y" => [{"z" => "1"}]}
+      must_equal "x" => { "y" => [{ "z" => "1" }] }
     Rack::Utils.parse_nested_query("x[y][][z][]=1").
-      must_equal "x" => {"y" => [{"z" => ["1"]}]}
+      must_equal "x" => { "y" => [{ "z" => ["1"] }] }
     Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=2").
-      must_equal "x" => {"y" => [{"z" => "1", "w" => "2"}]}
+      must_equal "x" => { "y" => [{ "z" => "1", "w" => "2" }] }
 
     Rack::Utils.parse_nested_query("x[y][][v][w]=1").
-      must_equal "x" => {"y" => [{"v" => {"w" => "1"}}]}
+      must_equal "x" => { "y" => [{ "v" => { "w" => "1" } }] }
     Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][v][w]=2").
-      must_equal "x" => {"y" => [{"z" => "1", "v" => {"w" => "2"}}]}
+      must_equal "x" => { "y" => [{ "z" => "1", "v" => { "w" => "2" } }] }
 
     Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][z]=2").
-      must_equal "x" => {"y" => [{"z" => "1"}, {"z" => "2"}]}
+      must_equal "x" => { "y" => [{ "z" => "1" }, { "z" => "2" }] }
     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"}]}
+      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"}}]
+      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"}}]
+      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"}}] }
+      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).
@@ -233,13 +234,13 @@ describe Rack::Utils do
 
   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"}]}]
+      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"}},
+        { "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
 
@@ -249,7 +250,7 @@ describe Rack::Utils do
       param_parser_class = Class.new(Rack::QueryParser::Params) do
         def initialize(*)
           super
-          @params = Hash.new{|h,k| h[k.to_s] if k.is_a?(Symbol)}
+          @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
         end
       end
       Rack::Utils.default_query_parser = Rack::QueryParser.new(param_parser_class, 65536, 100)
@@ -320,7 +321,7 @@ 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"]]}).
+    Rack::Utils.build_nested_query({ "foo" => ["1", ["2"]] }).
       must_equal 'foo[]=1&foo[][]=2'
 
     lambda { Rack::Utils.build_nested_query("foo=bar") }.
@@ -329,24 +330,24 @@ describe Rack::Utils do
   end
 
   it 'performs the inverse function of #parse_nested_query' do
-    [{"foo" => nil, "bar" => ""},
-      {"foo" => "bar", "baz" => ""},
-      {"foo" => ["1", "2"]},
-      {"foo" => "bar", "baz" => ["1", "2", "3"]},
-      {"foo" => ["bar"], "baz" => ["1", "2", "3"]},
-      {"foo" => ["1", "2"]},
-      {"foo" => "bar", "baz" => ["1", "2", "3"]},
-      {"x" => {"y" => {"z" => "1"}}},
-      {"x" => {"y" => {"z" => ["1"]}}},
-      {"x" => {"y" => {"z" => ["1", "2"]}}},
-      {"x" => {"y" => [{"z" => "1"}]}},
-      {"x" => {"y" => [{"z" => ["1"]}]}},
-      {"x" => {"y" => [{"z" => "1", "w" => "2"}]}},
-      {"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"}]}},
-      {"foo" => ["1", ["2"]]},
+    [{ "foo" => nil, "bar" => "" },
+      { "foo" => "bar", "baz" => "" },
+      { "foo" => ["1", "2"] },
+      { "foo" => "bar", "baz" => ["1", "2", "3"] },
+      { "foo" => ["bar"], "baz" => ["1", "2", "3"] },
+      { "foo" => ["1", "2"] },
+      { "foo" => "bar", "baz" => ["1", "2", "3"] },
+      { "x" => { "y" => { "z" => "1" } } },
+      { "x" => { "y" => { "z" => ["1"] } } },
+      { "x" => { "y" => { "z" => ["1", "2"] } } },
+      { "x" => { "y" => [{ "z" => "1" }] } },
+      { "x" => { "y" => [{ "z" => ["1"] }] } },
+      { "x" => { "y" => [{ "z" => "1", "w" => "2" }] } },
+      { "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" }] } },
+      { "foo" => ["1", ["2"]] },
     ].each { |params|
       qs = Rack::Utils.build_nested_query(params)
       Rack::Utils.parse_nested_query(qs).must_equal params
@@ -461,6 +462,12 @@ describe Rack::Utils do
     Rack::Utils.status_code(:ok).must_equal 200
   end
 
+  it "raise an error for an invalid symbol" do
+    assert_raises(ArgumentError, "Unrecognized status code :foobar") do
+      Rack::Utils.status_code(:foobar)
+    end
+  end
+
   it "return rfc2822 format from rfc2822 helper" do
     Rack::Utils.rfc2822(Time.at(0).gmtime).must_equal "Thu, 01 Jan 1970 00:00:00 -0000"
   end
@@ -492,19 +499,23 @@ 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"})
+    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" => "%"})
+    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"})
+    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"})
+    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"})
+    Rack::Utils.parse_cookies(env).must_equal({ "foo" => "bar" })
+
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "%66oo=baz;foo=bar")
+    cookies = Rack::Utils.parse_cookies(env)
+    cookies.must_equal({ "%66oo" => "baz", "foo" => "bar" })
   end
 
   it "adds new cookies to nil header" do
@@ -538,48 +549,48 @@ end
 
 describe Rack::Utils, "byte_range" do
   it "ignore missing or syntactically invalid byte ranges" do
-    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
+    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_be_nil
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=456-455"},500).must_be_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
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-456"},500).must_equal [(123..456)]
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-"},500).must_equal [(123..499)]
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-100"},500).must_equal [(400..499)]
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-0"},500).must_equal [(0..0)]
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=499-499"},500).must_equal [(499..499)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-" }, 500).must_equal [(123..499)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-100" }, 500).must_equal [(400..499)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0" }, 500).must_equal [(0..0)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=499-499" }, 500).must_equal [(499..499)]
   end
 
   it "parse several byte ranges" do
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-600,601-999"},1000).must_equal [(500..600),(601..999)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-600,601-999" }, 1000).must_equal [(500..600), (601..999)]
   end
 
   it "truncate byte ranges" do
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-999"},500).must_equal [(123..499)]
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=600-999"},500).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-999"},500).must_equal [(0..499)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-999" }, 500).must_equal [(123..499)]
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=600-999" }, 500).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-999" }, 500).must_equal [(0..499)]
   end
 
   it "ignore unsatisfiable byte ranges" do
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-501"},500).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=500-"},500).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=999-"},500).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-0"},500).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-501" }, 500).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=500-" }, 500).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=999-" }, 500).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-0" }, 500).must_equal []
   end
 
   it "handle byte ranges of empty files" do
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=123-456"},0).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-"},0).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-100"},0).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=0-0"},0).must_equal []
-    Rack::Utils.byte_ranges({"HTTP_RANGE" => "bytes=-0"},0).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 0).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-" }, 0).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-100" }, 0).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=0-0" }, 0).must_equal []
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=-0" }, 0).must_equal []
   end
 end
 
@@ -612,7 +623,7 @@ describe Rack::Utils::HeaderHash do
   it "merge case-insensitively" do
     h = Rack::Utils::HeaderHash.new("ETag" => 'HELLO', "content-length" => '123')
     merged = h.merge("Etag" => 'WORLD', 'Content-Length' => '321', "Foo" => 'BAR')
-    merged.must_equal "Etag"=>'WORLD', "Content-Length"=>'321', "Foo"=>'BAR'
+    merged.must_equal "Etag" => 'WORLD', "Content-Length" => '321', "Foo" => 'BAR'
   end
 
   it "overwrite case insensitively and assume the new key's case" do
@@ -635,7 +646,7 @@ describe Rack::Utils::HeaderHash do
 
   it "replace hashes correctly" do
     h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
-    j = {"foo" => "bar"}
+    j = { "foo" => "bar" }
     h.replace(j)
     h["foo"].must_equal "bar"
   end
@@ -664,16 +675,16 @@ 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
 
   it "convert Array values to Strings when responding to #each" do
     h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
-    h.each do |k,v|
+    h.each do |k, v|
       k.must_equal "foo"
       v.must_equal "bar\nbaz"
     end
@@ -690,14 +701,14 @@ end
 describe Rack::Utils::Context do
   class ContextTest
     attr_reader :app
-    def initialize app; @app=app; end
+    def initialize app; @app = app; end
     def call env; context env; end
-    def context env, app=@app; app.call(env); end
+    def context env, app = @app; app.call(env); end
   end
-  test_target1 = proc{|e| e.to_s+' world' }
-  test_target2 = proc{|e| e.to_i+2 }
+  test_target1 = proc{|e| e.to_s + ' world' }
+  test_target2 = proc{|e| e.to_i + 2 }
   test_target3 = proc{|e| nil }
-  test_target4 = proc{|e| [200,{'Content-Type'=>'text/plain', 'Content-Length'=>'0'},['']] }
+  test_target4 = proc{|e| [200, { 'Content-Type' => 'text/plain', 'Content-Length' => '0' }, ['']] }
   test_app = ContextTest.new test_target4
 
   it "set context correctly" do
diff --git a/test/spec_version.rb b/test/spec_version.rb
index 6ab0a74ca2db406c12a44cb40fe2f026e827dc46..d4191aa41724bd0e5fae767718a0855b4818bd25 100644
--- a/test/spec_version.rb
+++ b/test/spec_version.rb
@@ -1,5 +1,6 @@
-# -*- encoding: utf-8 -*-
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack'
 
 describe Rack do
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
index 55899f84bb8cfe1498dbe71f12f1f8ce3c8e85fd..0d0aa8f7fabe81b4b6ecaa66875bdeba59ff29d9 100644
--- a/test/spec_webrick.rb
+++ b/test/spec_webrick.rb
@@ -1,4 +1,6 @@
-require 'minitest/autorun'
+# frozen_string_literal: true
+
+require 'minitest/global_expectations/autorun'
 require 'rack/mock'
 require 'thread'
 require File.expand_path('../testrequest', __FILE__)
@@ -9,10 +11,10 @@ describe Rack::Handler::WEBrick do
   include TestRequest::Helpers
 
   before do
-  @server = WEBrick::HTTPServer.new(:Host => @host='127.0.0.1',
-                                    :Port => @port=9202,
-                                    :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
-                                    :AccessLog => [])
+  @server = WEBrick::HTTPServer.new(Host: @host = '127.0.0.1',
+                                    Port: @port = 9202,
+                                    Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
+                                    AccessLog: [])
   @server.mount "/test", Rack::Handler::WEBrick,
     Rack::Lint.new(TestRequest.new)
   @thread = Thread.new { @server.start }
@@ -49,7 +51,7 @@ describe Rack::Handler::WEBrick do
 
   it "have rack headers" do
     GET("/test")
-    response["rack.version"].must_equal [1,3]
+    response["rack.version"].must_equal [1, 3]
     response["rack.multithread"].must_equal true
     assert_equal false, response["rack.multiprocess"]
     assert_equal false, response["rack.run_once"]
@@ -80,7 +82,7 @@ describe Rack::Handler::WEBrick do
   end
 
   it "have CGI headers on POST" do
-    POST("/test", {"rack-form-data" => "23"}, {'X-test-header' => '42'})
+    POST("/test", { "rack-form-data" => "23" }, { 'X-test-header' => '42' })
     status.must_equal 200
     response["REQUEST_METHOD"].must_equal "POST"
     response["SCRIPT_NAME"].must_equal "/test"
@@ -92,7 +94,7 @@ describe Rack::Handler::WEBrick do
   end
 
   it "support HTTP auth" do
-    GET("/test", {:user => "ruth", :passwd => "secret"})
+    GET("/test", { user: "ruth", passwd: "secret" })
     response["HTTP_AUTHORIZATION"].must_equal "Basic cnV0aDpzZWNyZXQ="
   end
 
@@ -124,17 +126,27 @@ describe Rack::Handler::WEBrick do
     t = Thread.new do
       Rack::Handler::WEBrick.run(lambda {},
                                  {
-                                   :Host => '127.0.0.1',
-                                   :Port => 9210,
-                                   :Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
-                                   :AccessLog => []}) { |server|
-        block_ran = true
+                                   Host: '127.0.0.1',
+                                   Port: 9210,
+                                   Logger: WEBrick::Log.new(nil, WEBrick::BasicLog::WARN),
+                                   AccessLog: [] }) { |server|
         assert_kind_of WEBrick::HTTPServer, server
         queue.push(server)
       }
     end
 
     server = queue.pop
+
+    # The server may not yet have started: wait for it
+    seconds = 10
+    wait_time = 0.1
+    until server.status == :Running || seconds <= 0
+      seconds -= wait_time
+      sleep wait_time
+    end
+
+    raise "Server never reached status 'Running'" unless server.status == :Running
+
     server.shutdown
     t.join
   end
@@ -185,7 +197,7 @@ describe Rack::Handler::WEBrick do
     Rack::Lint.new(lambda{ |req|
       [
         200,
-        {"Transfer-Encoding" => "chunked"},
+        { "Transfer-Encoding" => "chunked" },
         ["7\r\nchunked\r\n0\r\n\r\n"]
       ]
     })
@@ -199,8 +211,8 @@ describe Rack::Handler::WEBrick do
   end
 
   after do
-  @status_thread.join
-  @server.shutdown
-  @thread.join
+    @status_thread.join
+    @server.shutdown
+    @thread.join
   end
 end
diff --git a/test/testrequest.rb b/test/testrequest.rb
index cacd23d5079b09cb6b8ebae8450ee05df451af8f..aabe7fa6b342ad068fdec7b7cec94271df1ebe00 100644
--- a/test/testrequest.rb
+++ b/test/testrequest.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'yaml'
 require 'net/http'
 require 'rack/lint'
@@ -10,10 +12,10 @@ class TestRequest
     env["test.postdata"] = env["rack.input"].read
     minienv = env.dup
     # This may in the future want to replace with a dummy value instead.
-    minienv.delete_if { |k,v| NOSERIALIZE.any? { |c| v.kind_of?(c) } }
+    minienv.delete_if { |k, v| NOSERIALIZE.any? { |c| v.kind_of?(c) } }
     body = minienv.to_yaml
     size = body.bytesize
-    [status, {"Content-Type" => "text/yaml", "Content-Length" => size.to_s}, [body]]
+    [status, { "Content-Type" => "text/yaml", "Content-Length" => size.to_s }, [body]]
   end
 
   module Helpers
@@ -30,7 +32,7 @@ class TestRequest
       "#{ROOT}/bin/rackup"
     end
 
-    def GET(path, header={})
+    def GET(path, header = {})
       Net::HTTP.start(@host, @port) { |http|
         user = header.delete(:user)
         passwd = header.delete(:passwd)
@@ -48,7 +50,7 @@ class TestRequest
       }
     end
 
-    def POST(path, formdata={}, header={})
+    def POST(path, formdata = {}, header = {})
       Net::HTTP.start(@host, @port) { |http|
         user = header.delete(:user)
         passwd = header.delete(:passwd)
@@ -67,7 +69,7 @@ end
 
 class StreamingRequest
   def self.call(env)
-    [200, {"Content-Type" => "text/plain"}, new]
+    [200, { "Content-Type" => "text/plain" }, new]
   end
 
   def each
diff --git a/test/unregistered_handler/rack/handler/unregistered.rb b/test/unregistered_handler/rack/handler/unregistered.rb
index 3ca5a72c5d17df3bd2780f7a615dd886a0026d95..e98468cc6669fa05d1106ec6d5e5b09229e3ea74 100644
--- a/test/unregistered_handler/rack/handler/unregistered.rb
+++ b/test/unregistered_handler/rack/handler/unregistered.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Handler
     # this class doesn't do anything, we're just seeing if we get it.
diff --git a/test/unregistered_handler/rack/handler/unregistered_long_one.rb b/test/unregistered_handler/rack/handler/unregistered_long_one.rb
index 2c2fae1709f2da529bd600aacd7263c70c13d3af..87c6c25431f90fa5a5128c14fcf178682be1509f 100644
--- a/test/unregistered_handler/rack/handler/unregistered_long_one.rb
+++ b/test/unregistered_handler/rack/handler/unregistered_long_one.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module Rack
   module Handler
     # this class doesn't do anything, we're just seeing if we get it.