diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000000000000000000000000000000000..ee510310dfdab43538ef3d38d1257198f13274b3
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,8 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5ace4600a1f26e6892982f3e2f069ebfab108d87
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,6 @@
+version: 2
+updates:
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/depsreview.yaml b/.github/workflows/depsreview.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a25de591ba3c64f2a64205d89265dc3bb0e0244f
--- /dev/null
+++ b/.github/workflows/depsreview.yaml
@@ -0,0 +1,14 @@
+name: 'Dependency Review'
+on: [pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  dependency-review:
+    runs-on: ubuntu-latest
+    steps:
+      - name: 'Checkout Repository'
+        uses: actions/checkout@v3
+      - name: 'Dependency Review'
+        uses: actions/dependency-review-action@v2
diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml
deleted file mode 100644
index 0757016be374eed62379ca5819970b0ff73ecc98..0000000000000000000000000000000000000000
--- a/.github/workflows/development.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Development
-
-on: [push, pull_request]
-
-jobs:
-  test:
-    strategy:
-      fail-fast: false
-      matrix:
-        os: [ubuntu-20.04]
-        ruby: [2.3, 2.4, 2.5, 2.6, 2.7, '3.0', 3.1, 3.2]
-    runs-on: ${{matrix.os}}
-    steps:
-    - uses: actions/checkout@v2
-
-    - uses: ruby/setup-ruby@v1
-      with:
-        ruby-version: ${{matrix.ruby}}
-
-    - uses: actions/cache@v1
-      with:
-        path: vendor/bundle
-        key: bundle-use-ruby-${{matrix.os}}-${{matrix.ruby}}-${{hashFiles('**/Gemfile')}}
-        restore-keys: |
-          bundle-use-ruby-${{matrix.os}}-${{matrix.ruby}}-
-
-    - name: Installing packages
-      run: sudo apt-get install libfcgi-dev libmemcached-dev
-
-    - name: Bundle install...
-      run: |
-        bundle config path vendor/bundle
-        bundle install
-
-    - run: bundle exec rake
diff --git a/.github/workflows/test-external.yaml b/.github/workflows/test-external.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..d55882cea2e1078ac1e2283dde8db72d3c1691f1
--- /dev/null
+++ b/.github/workflows/test-external.yaml
@@ -0,0 +1,28 @@
+name: Test External
+
+on: [push, pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  test:
+    strategy:
+      fail-fast: false
+      matrix:
+        os: [ubuntu-latest]
+        ruby: ['2.7', '3.0', '3.1']
+
+    runs-on: ${{matrix.os}}
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - uses: ruby/setup-ruby-pkgs@v1
+      with:
+        ruby-version: ${{matrix.ruby}}
+        bundler-cache: true
+        apt-get: _update_ libfcgi-dev libmemcached-dev
+        brew: fcgi libmemcached
+
+    - run: bundle exec bake test:external
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..697b3cd275a2fe618a5ad6d7b73b2afef724ab65
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,38 @@
+name: Test
+
+on: [push, pull_request]
+
+permissions:
+  contents: read
+
+jobs:
+  test:
+    strategy:
+      fail-fast: false
+      matrix:
+        os:
+          - ubuntu-latest
+        ruby:
+          - '2.4'
+          - '2.5'
+          - '2.6'
+          - '2.7'
+          - '3.0'
+          - '3.1'
+          - '3.2'
+          - jruby
+          - truffleruby-head
+        include:
+          - os: macos-latest
+            ruby: '3.1'
+    runs-on: ${{matrix.os}}
+
+    steps:
+    - uses: actions/checkout@v3
+
+    - uses: ruby/setup-ruby@v1
+      with:
+        ruby-version: ${{matrix.ruby}}
+        bundler-cache: true
+
+    - run: bundle exec rake
diff --git a/.rubocop.yml b/.rubocop.yml
index ca9867670d647b8b73c5cb9ab1c7c86941948ab1..7188939dc382c008007b7fe1966f8f575c10a0bd 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,8 @@
+require:
+  - rubocop-packaging
+
 AllCops:
-  TargetRubyVersion: 2.3
+  TargetRubyVersion: 2.4
   DisabledByDefault: true
   Exclude:
     - '**/vendor/**/*'
@@ -50,8 +53,11 @@ Layout/SpaceBeforeFirstArg:
 Layout/SpaceInsideHashLiteralBraces:
   Enabled: true
 
-Layout/Tab:
+Layout/IndentationStyle:
   Enabled: true
 
 Layout/TrailingWhitespace:
   Enabled: true
+
+Lint/DeprecatedOpenSSLConstant:
+  Enabled: true
diff --git a/.yardopts b/.yardopts
index f4d6aebae4cc1d7b85159bfcdf95421c287f55bc..06b7cada7a8d7b665714d1c3544eff601b351b10 100644
--- a/.yardopts
+++ b/.yardopts
@@ -1,2 +1,2 @@
 -
-SPEC
+SPEC.rdoc
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85cb1fc2a65504b9ccaab168e05d3939182e0c46..09a7aa440a608bd99f3c425e59d0e9a4a7c6c8e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,33 +2,148 @@
 
 All notable changes to this project will be documented in this file. For info on how to format all future additions to this file please reference [Keep A Changelog](https://keepachangelog.com/en/1.0.0/).
 
-## [2.2.6.4] - 2023-03-13
+## [3.0.7] - 2023-03-16
+
+- Make query parameters without `=` have `nil` values. ([#2059](https://github.com/rack/rack/pull/2059), [@jeremyevans])
+
+## [3.0.6.1] - 2023-03-13
 
 - [CVE-2023-27539] Avoid ReDoS in header parsing
 
-## [2.2.6.3] - 2023-03-02
+## [3.0.6] - 2023-03-13
 
-- [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts
+- Add `QueryParser#missing_value` for handling missing values + tests. ([#2052](https://github.com/rack/rack/pull/2052), [@ioquatix])
 
-## [2.2.6.2] - 2022-01-17
+## [3.0.5] - 2023-03-13
 
-- [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges
+- Split form/query parsing into two steps. ([#2038](https://github.com/rack/rack/pull/2038), [@matthewd](https://github.com/matthewd))
+
+## [3.0.4.1] - 2023-03-02
+
+- [CVE-2023-27530] Introduce multipart_total_part_limit to limit total parts
 
-## [2.2.6.1] - 2022-01-17
+## [3.0.4.1] - 2023-01-17
 
 - [CVE-2022-44571] Fix ReDoS vulnerability in multipart parser
+- [CVE-2022-44570] Fix ReDoS in Rack::Utils.get_byte_ranges
 - [CVE-2022-44572] Forbid control characters in attributes (also ReDoS)
 
-## [2.2.6] - 2022-01-17
+## [3.0.4] - 2023-01-17
 
-- Extend `Rack::MethodOverride` to handle `QueryParser::ParamsTooDeepError` error. ([#2011](https://github.com/rack/rack/pull/2011), [@byroot](https://github.com/byroot))
+- `Rack::Request#POST` should consistently raise errors. Cache errors that occur when invoking `Rack::Request#POST` so they can be raised again later. ([#2010](https://github.com/rack/rack/pull/2010), [@ioquatix])
+- Fix `Rack::Lint` error message for `HTTP_CONTENT_TYPE` and `HTTP_CONTENT_LENGTH`. ([#2007](https://github.com/rack/rack/pull/2007), [@byroot](https://github.com/byroot))
+- Extend `Rack::MethodOverride` to handle `QueryParser::ParamsTooDeepError` error. ([#2006](https://github.com/rack/rack/pull/2006), [@byroot](https://github.com/byroot))
 
-## [2.2.5] - 2022-12-27
+## [3.0.3] - 2022-12-27
 
 ### Fixed
 
 - `Rack::URLMap` uses non-deprecated form of `Regexp.new`. ([#1998](https://github.com/rack/rack/pull/1998), [@weizheheng](https://github.com/weizheheng))
 
+## [3.0.2] - 2022-12-05
+
+### Fixed
+
+- `Utils.build_nested_query` URL-encodes nested field names including the square brackets.
+- Allow `Rack::Response` to pass through streaming bodies. ([#1993](https://github.com/rack/rack/pull/1993), [@ioquatix])
+
+## [3.0.1] - 2022-11-18
+
+### Fixed
+
+- `MethodOverride` does not look for an override if a request does not include form/parseable data.
+- `Rack::Lint::Wrapper` correctly handles `respond_to?` with `to_ary`, `each`, `call` and `to_path`, forwarding to the body. ([#1981](https://github.com/rack/rack/pull/1981), [@ioquatix])
+
+## [3.0.0] - 2022-09-06
+
+- No changes
+
+## [3.0.0.rc1] - 2022-09-04
+
+### SPEC Changes
+
+- Stream argument must implement `<<` https://github.com/rack/rack/pull/1959
+- `close` may be called on `rack.input` https://github.com/rack/rack/pull/1956
+- `rack.response_finished` may be used for executing code after the response has been finished https://github.com/rack/rack/pull/1952
+
+## [3.0.0.beta1] - 2022-08-08
+
+### Security
+
+- Do not use semicolon as GET parameter separator. ([#1733](https://github.com/rack/rack/pull/1733), [@jeremyevans])
+
+### SPEC Changes
+
+- Response array must now be non-frozen.
+- Response `status` must now be an integer greater than or equal to 100.
+- Response `headers` must now be an unfrozen hash.
+- Response header keys can no longer include uppercase characters.
+- Response header values can be an `Array` to handle multiple values (and no longer supports `\n` encoded headers).
+- Response body can now respond to `#call` (streaming body) instead of `#each` (enumerable body), for the equivalent of response hijacking in previous versions.
+- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`.
+- `rack.input` is no longer required to be rewindable.
+- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys.
+- `SERVER_PROTOCOL` is now a required environment key, matching the HTTP protocol used in the request.
+- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
+- `rack.hijack_io` has been removed completely.
+- `rack.response_finished` is an optional environment key which contains an array of callable objects that must accept `#call(env, status, headers, error)` and are invoked after the response is finished (either successfully or unsuccessfully).
+- It is okay to call `#close` on `rack.input` to indicate that you no longer need or care about the input.
+- The stream argument supplied to the streaming body and hijack must support `#<<` for writing output.
+
+### Removed
+
+- Remove `rack.multithread`/`rack.multiprocess`/`rack.run_once`. These variables generally come too late to be useful. ([#1720](https://github.com/rack/rack/pull/1720), [@ioquatix], [@jeremyevans]))
+- Remove deprecated Rack::Request::SCHEME_WHITELIST. ([@jeremyevans])
+- Remove internal cookie deletion using pattern matching, there are very few practical cases where it would be useful and browsers handle it correctly without us doing anything special. ([#1844](https://github.com/rack/rack/pull/1844), [@ioquatix])
+- Remove `rack.version` as it comes too late to be useful. ([#1938](https://github.com/rack/rack/pull/1938), [@ioquatix])
+- Extract `rackup` command, `Rack::Server`, `Rack::Handler` and related code into a separate gem. ([#1937](https://github.com/rack/rack/pull/1937), [@ioquatix])
+
+### Added
+
+- `Rack::Headers` added to support lower-case header keys. ([@jeremyevans])
+- `Rack::Utils#set_cookie_header` now supports `escape_key: false` to avoid key escaping.  ([@jeremyevans])
+- `Rack::RewindableInput` supports size. ([@ahorek](https://github.com/ahorek))
+- `Rack::RewindableInput::Middleware` added for making `rack.input` rewindable. ([@jeremyevans])
+- The RFC 7239 Forwarded header is now supported and considered by default when looking for information on forwarding, falling back to the X-Forwarded-* headers. `Rack::Request.forwarded_priority` accessor has been added for configuring the priority of which header to check.  ([#1423](https://github.com/rack/rack/issues/1423), [@jeremyevans])
+- Allow response headers to contain array of values. ([#1598](https://github.com/rack/rack/issues/1598), [@ioquatix])
+- Support callable body for explicit streaming support and clarify streaming response body behaviour. ([#1745](https://github.com/rack/rack/pull/1745), [@ioquatix], [#1748](https://github.com/rack/rack/pull/1748), [@wjordan])
+- Allow `Rack::Builder#run` to take a block instead of an argument. ([#1942](https://github.com/rack/rack/pull/1942), [@ioquatix])
+- Add `rack.response_finished` to `Rack::Lint`. ([#1802](https://github.com/rack/rack/pull/1802), [@BlakeWilliams], [#1952](https://github.com/rack/rack/pull/1952), [@ioquatix])
+- The stream argument must implement `#<<`. ([#1959](https://github.com/rack/rack/pull/1959), [@ioquatix])
+
+### Changed
+
+- BREAKING CHANGE: Require `status` to be an Integer. ([#1662](https://github.com/rack/rack/pull/1662), [@olleolleolle](https://github.com/olleolleolle))
+- BREAKING CHANGE: Query parsing now treats parameters without `=` as having the empty string value instead of nil value, to conform to the URL spec. ([#1696](https://github.com/rack/rack/issues/1696), [@jeremyevans])
+- Relax validations around `Rack::Request#host` and `Rack::Request#hostname`. ([#1606](https://github.com/rack/rack/issues/1606), [@pvande](https://github.com/pvande))
+- Removed antiquated handlers: FCGI, LSWS, SCGI, Thin. ([#1658](https://github.com/rack/rack/pull/1658), [@ioquatix])
+- Removed options from `Rack::Builder.parse_file` and `Rack::Builder.load_file`. ([#1663](https://github.com/rack/rack/pull/1663), [@ioquatix])
+- `Rack::HTTP_VERSION` has been removed and the `HTTP_VERSION` env setting is no longer set in the CGI and Webrick handlers. ([#970](https://github.com/rack/rack/issues/970), [@jeremyevans])
+- `Rack::Request#[]` and `#[]=` now warn even in non-verbose mode. ([#1277](https://github.com/rack/rack/issues/1277), [@jeremyevans])
+- Decrease default allowed parameter recursion level from 100 to 32. ([#1640](https://github.com/rack/rack/issues/1640), [@jeremyevans])
+- Attempting to parse a multipart response with an empty body now raises Rack::Multipart::EmptyContentError. ([#1603](https://github.com/rack/rack/issues/1603), [@jeremyevans])
+- `Rack::Utils.secure_compare` uses OpenSSL's faster implementation if available. ([#1711](https://github.com/rack/rack/pull/1711), [@bdewater](https://github.com/bdewater))
+- `Rack::Request#POST` now caches an empty hash if input content type is not parseable. ([#749](https://github.com/rack/rack/pull/749), [@jeremyevans])
+- BREAKING CHANGE: Updated `trusted_proxy?` to match full 127.0.0.0/8 network. ([#1781](https://github.com/rack/rack/pull/1781), [@snbloch](https://github.com/snbloch))
+- Explicitly deprecate `Rack::File` which was an alias for `Rack::Files`. ([#1811](https://github.com/rack/rack/pull/1720), [@ioquatix]).
+- Moved `Rack::Session` into [separate gem](https://github.com/rack/rack-session). ([#1805](https://github.com/rack/rack/pull/1805), [@ioquatix])
+- `rackup -D` option to daemonizes no longer changes the working directory to the root. ([#1813](https://github.com/rack/rack/pull/1813), [@jeremyevans])
+- The `x-forwarded-proto` header is now considered before the `x-forwarded-scheme` header for determining the forwarded protocol. `Rack::Request.x_forwarded_proto_priority` accessor has been added for configuring the priority of which header to check.  ([#1809](https://github.com/rack/rack/issues/1809), [@jeremyevans])
+- `Rack::Request.forwarded_authority` (and methods that call it, such as `host`) now returns the last authority in the forwarded header, instead of the first, as earlier forwarded authorities can be forged by clients. This restores the Rack 2.1 behavior. ([#1829](https://github.com/rack/rack/issues/1809), [@jeremyevans])
+- Use lower case cookie attributes when creating cookies, and fold cookie attributes to lower case when reading cookies (specifically impacting `secure` and `httponly` attributes). ([#1849](https://github.com/rack/rack/pull/1849), [@ioquatix])
+- The response array must now be mutable (non-frozen) so middleware can modify it without allocating a new Array,therefore reducing object allocations. ([#1887](https://github.com/rack/rack/pull/1887), [#1927](https://github.com/rack/rack/pull/1927), [@amatsuda], [@ioquatix])
+- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional. `rack.hijack_io` is no longer required/specified. ([#1939](https://github.com/rack/rack/pull/1939), [@ioquatix])
+- Allow calling close on `rack.input`. ([#1956](https://github.com/rack/rack/pull/1956), [@ioquatix])
+
+### Fixed
+
+- Make Rack::MockResponse handle non-hash headers. ([#1629](https://github.com/rack/rack/issues/1629), [@jeremyevans])
+- TempfileReaper now deletes temp files if application raises an exception. ([#1679](https://github.com/rack/rack/issues/1679), [@jeremyevans])
+- Handle cookies with values that end in '=' ([#1645](https://github.com/rack/rack/pull/1645), [@lukaso](https://github.com/lukaso))
+- Make `Rack::NullLogger` respond to `#fatal!` [@jeremyevans])
+- Fix multipart filename generation for filenames that contain spaces. Encode spaces as "%20" instead of "+" which will be decoded properly by the multipart parser. ([#1736](https://github.com/rack/rack/pull/1645), [@muirdm](https://github.com/muirdm))
+- `Rack::Request#scheme` returns `ws` or `wss` when one of the `X-Forwarded-Scheme` / `X-Forwarded-Proto` headers is set to `ws` or `wss`, respectively. ([#1730](https://github.com/rack/rack/issues/1730), [@erwanst](https://github.com/erwanst))
+
 ## [2.2.4] - 2022-06-30
 
 - Better support for lower case headers in `Rack::ETag` middleware. ([#1919](https://github.com/rack/rack/pull/1919), [@ioquatix](https://github.com/ioquatix))
@@ -36,23 +151,21 @@ All notable changes to this project will be documented in this file. For info on
 
 ## [2.2.3.1] - 2022-05-27
 
-### Security
-
 - [CVE-2022-30123] Fix shell escaping issue in Common Logger
 - [CVE-2022-30122] Restrict parsing of broken MIME attachments
 
-## [2.2.3] - 2020-02-11
+## [2.2.3] - 2020-06-15
 
 ### Security
 
-- [CVE-2020-8184] Only decode cookie values
+- [[CVE-2020-8184](https://nvd.nist.gov/vuln/detail/CVE-2020-8184)] Do not allow percent-encoded cookie name to override existing cookie names. BREAKING CHANGE: Accessing cookie names that require URL encoding with decoded name no longer works. ([@fletchto99](https://github.com/fletchto99))
 
 ## [2.2.2] - 2020-02-11
 
 ### Fixed
 
-- Fix incorrect `Rack::Request#host` value. ([#1591](https://github.com/rack/rack/pull/1591), [@ioquatix](https://github.com/ioquatix))
-- Revert `Rack::Handler::Thin` implementation. ([#1583](https://github.com/rack/rack/pull/1583), [@jeremyevans](https://github.com/jeremyevans))
+- Fix incorrect `Rack::Request#host` value. ([#1591](https://github.com/rack/rack/pull/1591), [@ioquatix])
+- Revert `Rack::Handler::Thin` implementation. ([#1583](https://github.com/rack/rack/pull/1583), [@jeremyevans])
 - Double assignment is still needed to prevent an "unused variable" warning. ([#1589](https://github.com/rack/rack/pull/1589), [@kamipo](https://github.com/kamipo))
 - Fix to handle same_site option for session pool. ([#1587](https://github.com/rack/rack/pull/1587), [@kamipo](https://github.com/kamipo))
 
@@ -60,105 +173,111 @@ All notable changes to this project will be documented in this file. For info on
 
 ### Fixed
 
-- Rework `Rack::Request#ip` to handle empty `forwarded_for`. ([#1577](https://github.com/rack/rack/pull/1577), [@ioquatix](https://github.com/ioquatix))
+- Rework `Rack::Request#ip` to handle empty `forwarded_for`. ([#1577](https://github.com/rack/rack/pull/1577), [@ioquatix])
 
 ## [2.2.0] - 2020-02-08
 
 ### SPEC Changes
 
-- `rack.session` request environment entry must respond to `to_hash` and return unfrozen Hash. ([@jeremyevans](https://github.com/jeremyevans))
-- Request environment cannot be frozen. ([@jeremyevans](https://github.com/jeremyevans))
-- CGI values in the request environment with non-ASCII characters must use ASCII-8BIT encoding. ([@jeremyevans](https://github.com/jeremyevans))
-- Improve SPEC/lint relating to SERVER_NAME, SERVER_PORT and HTTP_HOST. ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix](https://github.com/ioquatix))
+- `rack.session` request environment entry must respond to `to_hash` and return unfrozen Hash. ([@jeremyevans])
+- Request environment cannot be frozen. ([@jeremyevans])
+- CGI values in the request environment with non-ASCII characters must use ASCII-8BIT encoding. ([@jeremyevans])
+- Improve SPEC/lint relating to SERVER_NAME, SERVER_PORT and HTTP_HOST. ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix])
 
 ### Added
 
-- `rackup` supports multiple `-r` options and will require all arguments. ([@jeremyevans](https://github.com/jeremyevans))
+- `rackup` supports multiple `-r` options and will require all arguments. ([@jeremyevans])
 - `Server` supports an array of paths to require for the `:require` option. ([@khotta](https://github.com/khotta))
 - `Files` supports multipart range requests. ([@fatkodima](https://github.com/fatkodima))
-- `Multipart::UploadedFile` supports an IO-like object instead of using the filesystem, using `:filename` and `:io` options. ([@jeremyevans](https://github.com/jeremyevans))
-- `Multipart::UploadedFile` supports keyword arguments `:path`, `:content_type`, and `:binary` in addition to positional arguments. ([@jeremyevans](https://github.com/jeremyevans))
-- `Static` supports a `:cascade` option for calling the app if there is no matching file. ([@jeremyevans](https://github.com/jeremyevans))
-- `Session::Abstract::SessionHash#dig`. ([@jeremyevans](https://github.com/jeremyevans))
-- `Response.[]` and `MockResponse.[]` for creating instances using status, headers, and body. ([@ioquatix](https://github.com/ioquatix))
-- Convenient cache and content type methods for `Rack::Response`. ([#1555](https://github.com/rack/rack/pull/1555), [@ioquatix](https://github.com/ioquatix))
+- `Multipart::UploadedFile` supports an IO-like object instead of using the filesystem, using `:filename` and `:io` options. ([@jeremyevans])
+- `Multipart::UploadedFile` supports keyword arguments `:path`, `:content_type`, and `:binary` in addition to positional arguments. ([@jeremyevans])
+- `Static` supports a `:cascade` option for calling the app if there is no matching file. ([@jeremyevans])
+- `Session::Abstract::SessionHash#dig`. ([@jeremyevans])
+- `Response.[]` and `MockResponse.[]` for creating instances using status, headers, and body. ([@ioquatix])
+- Convenient cache and content type methods for `Rack::Response`. ([#1555](https://github.com/rack/rack/pull/1555), [@ioquatix])
 
 ### Changed
 
-- `Request#params` no longer rescues EOFError. ([@jeremyevans](https://github.com/jeremyevans))
-- `Directory` uses a streaming approach, significantly improving time to first byte for large directories. ([@jeremyevans](https://github.com/jeremyevans))
-- `Directory` no longer includes a Parent directory link in the root directory index. ([@jeremyevans](https://github.com/jeremyevans))
-- `QueryParser#parse_nested_query` uses original backtrace when reraising exception with new class. ([@jeremyevans](https://github.com/jeremyevans))
-- `ConditionalGet` follows RFC 7232 precedence if both If-None-Match and If-Modified-Since headers are provided. ([@jeremyevans](https://github.com/jeremyevans))
+- `Request#params` no longer rescues EOFError. ([@jeremyevans])
+- `Directory` uses a streaming approach, significantly improving time to first byte for large directories. ([@jeremyevans])
+- `Directory` no longer includes a Parent directory link in the root directory index. ([@jeremyevans])
+- `QueryParser#parse_nested_query` uses original backtrace when reraising exception with new class. ([@jeremyevans])
+- `ConditionalGet` follows RFC 7232 precedence if both If-None-Match and If-Modified-Since headers are provided. ([@jeremyevans])
 - `.ru` files supports the `frozen-string-literal` magic comment. ([@eregon](https://github.com/eregon))
-- Rely on autoload to load constants instead of requiring internal files, make sure to require 'rack' and not just 'rack/...'. ([@jeremyevans](https://github.com/jeremyevans))
-- `Etag` will continue sending ETag even if the response should not be cached. ([@henm](https://github.com/henm))
+- Rely on autoload to load constants instead of requiring internal files, make sure to require 'rack' and not just 'rack/...'. ([@jeremyevans])
+- BREAKING CHANGE: `Etag` will continue sending ETag even if the response should not be cached. Streaming no longer works without a workaround, see [#1619](https://github.com/rack/rack/issues/1619#issuecomment-848460528). ([@henm](https://github.com/henm))
 - `Request#host_with_port` no longer includes a colon for a missing or empty port. ([@AlexWayfer](https://github.com/AlexWayfer))
-- All handlers uses keywords arguments instead of an options hash argument. ([@ioquatix](https://github.com/ioquatix))
-- `Files` handling of range requests no longer return a body that supports `to_path`, to ensure range requests are handled correctly. ([@jeremyevans](https://github.com/jeremyevans))
-- `Multipart::Generator` only includes `Content-Length` for files with paths, and `Content-Disposition` `filename` if the `UploadedFile` instance has one. ([@jeremyevans](https://github.com/jeremyevans))
-- `Request#ssl?` is true for the `wss` scheme (secure websockets). ([@jeremyevans](https://github.com/jeremyevans))
-- `Rack::HeaderHash` is memoized by default. ([#1549](https://github.com/rack/rack/pull/1549), [@ioquatix](https://github.com/ioquatix))
+- All handlers uses keywords arguments instead of an options hash argument. ([@ioquatix])
+- `Files` handling of range requests no longer return a body that supports `to_path`, to ensure range requests are handled correctly. ([@jeremyevans])
+- `Multipart::Generator` only includes `Content-Length` for files with paths, and `Content-Disposition` `filename` if the `UploadedFile` instance has one. ([@jeremyevans])
+- `Request#ssl?` is true for the `wss` scheme (secure websockets). ([@jeremyevans])
+- `Rack::HeaderHash` is memoized by default. ([#1549](https://github.com/rack/rack/pull/1549), [@ioquatix])
 - `Rack::Directory` allow directory traversal inside root directory. ([#1417](https://github.com/rack/rack/pull/1417), [@ThomasSevestre](https://github.com/ThomasSevestre))
-- Sort encodings by server preference. ([#1184](https://github.com/rack/rack/pull/1184), [@ioquatix](https://github.com/ioquatix), [@wjordan](https://github.com/wjordan))
-- Rework host/hostname/authority implementation in `Rack::Request`. `#host` and `#host_with_port` have been changed to correctly return IPv6 addresses formatted with square brackets, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix](https://github.com/ioquatix))
-- `Rack::Builder` parsing options on first `#\` line is deprecated. ([#1574](https://github.com/rack/rack/pull/1574), [@ioquatix](https://github.com/ioquatix))
+- Sort encodings by server preference. ([#1184](https://github.com/rack/rack/pull/1184), [@ioquatix], [@wjordan](https://github.com/wjordan))
+- Rework host/hostname/authority implementation in `Rack::Request`. `#host` and `#host_with_port` have been changed to correctly return IPv6 addresses formatted with square brackets, as defined by [RFC3986](https://tools.ietf.org/html/rfc3986#section-3.2.2). ([#1561](https://github.com/rack/rack/pull/1561), [@ioquatix])
+- `Rack::Builder` parsing options on first `#\` line is deprecated. ([#1574](https://github.com/rack/rack/pull/1574), [@ioquatix])
 
 ### Removed
 
-- `Directory#path` as it was not used and always returned nil. ([@jeremyevans](https://github.com/jeremyevans))
-- `BodyProxy#each` as it was only needed to work around a bug in Ruby <1.9.3. ([@jeremyevans](https://github.com/jeremyevans))
+- `Directory#path` as it was not used and always returned nil. ([@jeremyevans])
+- `BodyProxy#each` as it was only needed to work around a bug in Ruby <1.9.3. ([@jeremyevans])
 - `URLMap::INFINITY` and `URLMap::NEGATIVE_INFINITY`, in favor of `Float::INFINITY`. ([@ch1c0t](https://github.com/ch1c0t))
 - Deprecation of `Rack::File`. It will be deprecated again in rack 2.2 or 3.0. ([@rafaelfranca](https://github.com/rafaelfranca))
-- Support for Ruby 2.2 as it is well past EOL. ([@ioquatix](https://github.com/ioquatix))
-- Remove `Rack::Files#response_body` as the implementation was broken. ([#1153](https://github.com/rack/rack/pull/1153), [@ioquatix](https://github.com/ioquatix))
-- Remove `SERVER_ADDR` which was never part of the original SPEC. ([#1573](https://github.com/rack/rack/pull/1573), [@ioquatix](https://github.com/ioquatix))
+- Support for Ruby 2.2 as it is well past EOL. ([@ioquatix])
+- Remove `Rack::Files#response_body` as the implementation was broken. ([#1153](https://github.com/rack/rack/pull/1153), [@ioquatix])
+- Remove `SERVER_ADDR` which was never part of the original SPEC. ([#1573](https://github.com/rack/rack/pull/1573), [@ioquatix])
 
 ### Fixed
 
-- `Directory` correctly handles root paths containing glob metacharacters. ([@jeremyevans](https://github.com/jeremyevans))
-- `Cascade` uses a new response object for each call if initialized with no apps. ([@jeremyevans](https://github.com/jeremyevans))
-- `BodyProxy` correctly delegates keyword arguments to the body object on Ruby 2.7+. ([@jeremyevans](https://github.com/jeremyevans))
-- `BodyProxy#method` correctly handles methods delegated to the body object. ([@jeremyevans](https://github.com/jeremyevans))
+- `Directory` correctly handles root paths containing glob metacharacters. ([@jeremyevans])
+- `Cascade` uses a new response object for each call if initialized with no apps. ([@jeremyevans])
+- `BodyProxy` correctly delegates keyword arguments to the body object on Ruby 2.7+. ([@jeremyevans])
+- `BodyProxy#method` correctly handles methods delegated to the body object. ([@jeremyevans])
 - `Request#host` and `Request#host_with_port` handle IPv6 addresses correctly. ([@AlexWayfer](https://github.com/AlexWayfer))
-- `Lint` checks when response hijacking that `rack.hijack` is called with a valid object. ([@jeremyevans](https://github.com/jeremyevans))
-- `Response#write` correctly updates `Content-Length` if initialized with a body. ([@jeremyevans](https://github.com/jeremyevans))
+- `Lint` checks when response hijacking that `rack.hijack` is called with a valid object. ([@jeremyevans])
+- `Response#write` correctly updates `Content-Length` if initialized with a body. ([@jeremyevans])
 - `CommonLogger` includes `SCRIPT_NAME` when logging. ([@Erol](https://github.com/Erol))
-- `Utils.parse_nested_query` correctly handles empty queries, using an empty instance of the params class instead of a hash. ([@jeremyevans](https://github.com/jeremyevans))
+- `Utils.parse_nested_query` correctly handles empty queries, using an empty instance of the params class instead of a hash. ([@jeremyevans])
 - `Directory` correctly escapes paths in links. ([@yous](https://github.com/yous))
-- `Request#delete_cookie` and related `Utils` methods handle `:domain` and `:path` options in same call. ([@jeremyevans](https://github.com/jeremyevans))
-- `Request#delete_cookie` and related `Utils` methods do an exact match on `:domain` and `:path` options. ([@jeremyevans](https://github.com/jeremyevans))
+- `Request#delete_cookie` and related `Utils` methods handle `:domain` and `:path` options in same call. ([@jeremyevans])
+- `Request#delete_cookie` and related `Utils` methods do an exact match on `:domain` and `:path` options. ([@jeremyevans])
 - `Static` no longer adds headers when a gzipped file request has a 304 response. ([@chooh](https://github.com/chooh))
-- `ContentLength` sets `Content-Length` response header even for bodies not responding to `to_ary`. ([@jeremyevans](https://github.com/jeremyevans))
-- Thin handler supports options passed directly to `Thin::Controllers::Controller`. ([@jeremyevans](https://github.com/jeremyevans))
-- WEBrick handler no longer ignores `:BindAddress` option. ([@jeremyevans](https://github.com/jeremyevans))
-- `ShowExceptions` handles invalid POST data. ([@jeremyevans](https://github.com/jeremyevans))
-- Basic authentication requires a password, even if the password is empty. ([@jeremyevans](https://github.com/jeremyevans))
-- `Lint` checks response is array with 3 elements, per SPEC. ([@jeremyevans](https://github.com/jeremyevans))
+- `ContentLength` sets `Content-Length` response header even for bodies not responding to `to_ary`. ([@jeremyevans])
+- Thin handler supports options passed directly to `Thin::Controllers::Controller`. ([@jeremyevans])
+- WEBrick handler no longer ignores `:BindAddress` option. ([@jeremyevans])
+- `ShowExceptions` handles invalid POST data. ([@jeremyevans])
+- Basic authentication requires a password, even if the password is empty. ([@jeremyevans])
+- `Lint` checks response is array with 3 elements, per SPEC. ([@jeremyevans])
 - Support for using `:SSLEnable` option when using WEBrick handler. (Gregor Melhorn)
-- Close response body after buffering it when buffering. ([@ioquatix](https://github.com/ioquatix))
+- Close response body after buffering it when buffering. ([@ioquatix])
 - Only accept `;` as delimiter when parsing cookies. ([@mrageh](https://github.com/mrageh))
-- `Utils::HeaderHash#clear` clears the name mapping as well. ([@raxoft](https://github.com/raxoft)) 
-- Support for passing `nil` `Rack::Files.new`, which notably fixes Rails' current `ActiveStorage::FileServer` implementation. ([@ioquatix](https://github.com/ioquatix))
+- `Utils::HeaderHash#clear` clears the name mapping as well. ([@raxoft](https://github.com/raxoft))
+- Support for passing `nil` `Rack::Files.new`, which notably fixes Rails' current `ActiveStorage::FileServer` implementation. ([@ioquatix])
 
 ### Documentation
 
 - CHANGELOG updates. ([@aupajo](https://github.com/aupajo))
 - Added [CONTRIBUTING](CONTRIBUTING.md). ([@dblock](https://github.com/dblock))
 
+## [2.0.9] - 2020-02-08
+
+- Handle case where session id key is requested but missing ([@jeremyevans])
+- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans])
+- Add support for `SameSite=None` cookie value. ([@hennikul](https://github.com/hennikul))
+
 ## [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))
+- Skip deflating in Rack::Deflater if Content-Length is 0 ([@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))
+- Handle case where session id key is requested but missing ([@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))
-- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans](https://github.com/jeremyevans))
+- Remove `Rack::Chunked` from `Rack::Server` default middleware. ([#1475](https://github.com/rack/rack/pull/1475), [@ioquatix])
+- Restore support for code relying on `SessionId#to_s`. ([@jeremyevans])
 
 ## [2.1.0] - 2020-01-10
 
@@ -175,13 +294,13 @@ All notable changes to this project will be documented in this file. For info on
 - 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 `Builder#freeze_app` to freeze application and all middleware instances. ([@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))
+- Don't propagate nil values from middleware. ([@ioquatix])
+- Lazily initialize the response body and only buffer it if required. ([@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))
@@ -194,15 +313,15 @@ All notable changes to this project will be documented in this file. For info on
 - 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))
+- Add Falcon to the default handler fallbacks. ([@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))
+- Rename `Rack::File` -> `Rack::Files` and add deprecation notice. ([@postmodern](https://github.com/postmodern))
+- Prefer Base64 “strict encoding” for Base64 cookies. ([@ioquatix])
 
 ### Removed
 
-- Remove `to_ary` from Response ([@tenderlove](https://github.com/tenderlove))
+- BREAKING CHANGE: 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
@@ -283,7 +402,7 @@ All notable changes to this project will be documented in this file. For info on
 ### Added
 
 - Allow `Session::Abstract::SessionHash#fetch` to accept a block with a default value. ([@yannvanhalewyn](https://github.com/yannvanhalewyn))
-- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans](https://github.com/jeremyevans))
+- Add `Builder#freeze_app` to freeze application and all middleware. ([@jeremyevans])
 
 ### Changed
 
@@ -295,9 +414,9 @@ All notable changes to this project will be documented in this file. For info on
 ### Fixed
 
 - Handle `NULL` bytes in multipart filenames. ([@casperisfine](https://github.com/casperisfine))
-- Remove warnings due to miscapitalized global. ([@ioquatix](https://github.com/ioquatix))
+- Remove warnings due to miscapitalized global. ([@ioquatix])
 - Prevent exceptions caused by a race condition on multi-threaded servers. ([@sophiedeziel](https://github.com/sophiedeziel))
-- Add RDoc as an explicit depencency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan))
+- Add RDoc as an explicit dependency for `doc` group. ([@tonytonyjan](https://github.com/tonytonyjan))
 - Record errors originating from `Multipart::Parser` in the `MethodOverride` middleware instead of letting them bubble up. ([@carlzulauf](https://github.com/carlzulauf))
 - Remove remaining use of removed `Utils#bytesize` method from the `File` middleware. ([@brauliomartinezlm](https://github.com/brauliomartinezlm))
 
@@ -615,7 +734,7 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
   - 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 change to document rack.input binary mode in greater detail
   - SPEC define optional rack.logger specification
   - File servers support X-Cascade header
   - Imported Config middleware
@@ -733,3 +852,9 @@ Items below this line are from the previously maintained HISTORY.md and NEWS.md
   - Removed Rails adapter, was too alpha.
 
 ## [0.1] 2007-03-03
+
+[@ioquatix]: https://github.com/ioquatix "Samuel Williams"
+[@jeremyevans]: https://github.com/jeremyevans "Jeremy Evans"
+[@amatsuda]: https://github.com/amatsuda "Akira Matsuda"
+[@wjordan]: https://github.com/wjordan "Will Jordan"
+[@BlakeWilliams]: https://github.com/BlakeWilliams "Blake Williams"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 70a27468e5f8ad03c5e7c036cad139c06c2a70b8..bd5a51207583e9c176e841fa75fa3fcb25663613 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,29 +1,33 @@
-Contributing to Rack
-=====================
+# Contributing to Rack
 
-Rack is work of [hundreds of contributors](https://github.com/rack/rack/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rack/rack/pulls), [propose features and discuss issues](https://github.com/rack/rack/issues). When in doubt, post to the [rack-devel](http://groups.google.com/group/rack-devel) mailing list.
+Rack is work of [hundreds of
+contributors](https://github.com/rack/rack/graphs/contributors). You're
+encouraged to submit [pull requests](https://github.com/rack/rack/pulls) and
+[propose features and discuss issues](https://github.com/rack/rack/issues).
 
-#### Fork the Project
+## Fork the Project
 
-Fork the [project on Github](https://github.com/rack/rack) and check out your copy.
+Fork the [project on GitHub](https://github.com/rack/rack) and check out your
+copy.
 
 ```
-git clone https://github.com/contributor/rack.git
+git clone https://github.com/(your-github-username)/rack.git
 cd rack
 git remote add upstream https://github.com/rack/rack.git
 ```
 
-#### Create a Topic Branch
+## Create a Topic Branch
 
-Make sure your fork is up-to-date and create a topic branch for your feature or bug fix.
+Make sure your fork is up-to-date and create a topic branch for your feature or
+bug fix.
 
 ```
-git checkout master
-git pull upstream master
+git checkout main
+git pull upstream main
 git checkout -b my-feature-branch
 ```
 
-#### Bundle Install and Quick Test
+## Bundle Install and Quick Test
 
 Ensure that you can build the project and run quick tests.
 
@@ -32,7 +36,7 @@ bundle install --without extra
 bundle exec rake test
 ```
 
-#### Running All Tests
+## Running All Tests
 
 Install all dependencies.
 
@@ -46,39 +50,33 @@ Run all tests.
 rake test
 ```
 
-The test suite has no dependencies outside of the core Ruby installation and bacon.
+## Write Tests
 
-Some tests will be skipped if a dependency is not found.
+Try to write a test that reproduces the problem you're trying to fix or
+describes a feature that you want to build.
 
-To run the test suite completely, you need:
+We definitely appreciate pull requests that highlight or reproduce a problem,
+even without a fix.
 
-  * fcgi
-  * dalli
-  * thin
-
-To test Memcache sessions, you need memcached (will be run on port 11211) and dalli installed.
-
-#### Write Tests
-
-Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build.
-
-We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix.
-
-#### Write Code
+## Write Code
 
 Implement your feature or bug fix.
 
-Make sure that `bundle exec rake fulltest` completes without errors.
+Make sure that all tests pass:
+
+```
+bundle exec rake test
+```
 
-#### Write Documentation
+## Write Documentation
 
-Document any external behavior in the [README](README.rdoc).
+Document any external behavior in the [README](README.md).
 
-#### Update Changelog
+## Update Changelog
 
 Add a line to [CHANGELOG](CHANGELOG.md).
 
-#### Commit Changes
+## Commit Changes
 
 Make sure git knows your name and email address:
 
@@ -87,34 +85,37 @@ git config --global user.name "Your Name"
 git config --global user.email "contributor@example.com"
 ```
 
-Writing good commit logs is important. A commit log should describe what changed and why.
+Writing good commit logs is important. A commit log should describe what changed
+and why.
 
 ```
 git add ...
 git commit
 ```
 
-#### Push
+## Push
 
 ```
 git push origin my-feature-branch
 ```
 
-#### Make a Pull Request
+## Make a Pull Request
 
-Go to https://github.com/contributor/rack and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days.
+Go to your fork of rack on GitHub and select your feature branch. Click the
+'Pull Request' button and fill out the form. Pull requests are usually
+reviewed within a few days.
 
-#### Rebase
+## Rebase
 
-If you've been working on a change for a while, rebase with upstream/master.
+If you've been working on a change for a while, rebase with upstream/main.
 
 ```
 git fetch upstream
-git rebase upstream/master
+git rebase upstream/main
 git push origin my-feature-branch -f
 ```
 
-#### Make Required Changes
+## Make Required Changes
 
 Amend your previous commit and force push the changes.
 
@@ -123,14 +124,19 @@ git commit --amend
 git push origin my-feature-branch -f
 ```
 
-#### Check on Your Pull Request
+## Check on Your Pull Request
 
-Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above.
+Go back to your pull request after a few minutes and see whether it passed
+tests with GitHub Actions. Everything should look green, otherwise fix issues and
+amend your commit as described above.
 
-#### Be Patient
+## Be Patient
 
-It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there!
+It's likely that your change will not be merged and that the nitpicky
+maintainers will ask you to do more, or fix seemingly benign problems. Hang in
+there!
 
-#### Thank You
+## Thank You
 
-Please do know that we really appreciate and value your time and work. We love you, really.
+Please do know that we really appreciate and value your time and work. We love
+you, really.
diff --git a/Gemfile b/Gemfile
index b6ce15e4b96a12cbb8b0424361af42a2ff55b7eb..2b58d070a6e5d00fd90eeb17c9abf168f856d115 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,31 +4,18 @@ 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
-# available, 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", require: false
+gem "webrick"
 
-group :test do
-  gem "webrick" # gemified in Ruby 3.1+
-  gem "psych"
-end
-
-# 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
+group :maintenance, optional: true do
+  gem "rubocop", require: false
+  gem "rubocop-packaging", require: false
 end
 
 group :doc do
   gem 'rdoc'
 end
+
+group :test do
+  gem 'minitest'
+  gem 'bake-test-external', '~> 0.1.3'
+end
diff --git a/MIT-LICENSE b/MIT-LICENSE
index 703d118f9a293d561fd731038ffd994777c19ed2..fb33b7fee10e84cef0e3278583b19fc940f0daf6 100644
--- a/MIT-LICENSE
+++ b/MIT-LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (C) 2007-2019 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
+Copyright (C) 2007-2021 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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2d26ab44f1378eb004288b7e3b6a0c3dedf543f6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,309 @@
+# ![Rack](contrib/logo.webp)
+
+> **_NOTE:_** Rack v3.0.0 was recently released. Please check the [Upgrade
+> Guide](UPGRADE-GUIDE.md) for more details about migrating your existing
+> servers, middlewares and applications. For detailed information on specific
+> changes, check the [Change Log](CHANGELOG.md).
+
+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 bridge between web servers, web
+frameworks, and web application into a single method call.
+
+The exact details of this are described in the [Rack Specification], which all
+Rack applications should conform to.
+
+## Installation
+
+Add the rack gem to your application bundle, or follow the instructions provided
+by a [supported web framework](#supported-web-frameworks):
+
+```bash
+# Install it generally:
+$ gem install rack --pre
+
+# or, add it to your current application gemfile:
+$ bundle add rack --version 3.0.0
+```
+
+If you need features from `Rack::Session` or `bin/rackup` please add those gems separately.
+
+```bash
+$ gem install rack-session rackup
+```
+
+## Usage
+
+Create a file called `config.ru` with the following contents:
+
+```ruby
+run do |env|
+  [200, {}, ["Hello World"]]
+end
+```
+
+Run this using the rackup gem or another [supported web
+server](#supported-web-servers).
+
+```bash
+$ gem install rackup
+$ rackup
+$ curl http://localhost:9292
+Hello World
+```
+
+## Supported web servers
+
+Rack is supported by a wide range of servers, including:
+
+* [Agoo](https://github.com/ohler55/agoo)
+* [Falcon](https://github.com/socketry/falcon) **(Rack 3 Compatible)**
+* [Iodine](https://github.com/boazsegev/iodine)
+* [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/)
+* [Thin](https://github.com/macournoyer/thin)
+* [Unicorn](https://yhbt.net/unicorn/)
+* [uWSGI](https://uwsgi-docs.readthedocs.io/en/latest/)
+* [Lamby](https://lamby.custominktech.com) (for AWS Lambda)
+
+You will need to consult the server documentation to find out what features and
+limitations they may have. In general, any valid Rack app will run the same on
+all these servers, without changing anything.
+
+### Rackup
+
+Rack provides a separate gem, [rackup](https://github.com/rack/rackup) which is
+a generic interface for running a Rack application on supported servers, which
+include `WEBRick`, `Puma`, `Falcon` and others.
+
+## Supported web frameworks
+
+These frameworks and many others support the [Rack Specification]:
+
+* [Camping](https://github.com/camping/camping)
+* [Hanami](https://hanamirb.org/)
+* [Padrino](https://padrinorb.com/)
+* [Roda](https://github.com/jeremyevans/roda) **(Rack 3 Compatible)**
+* [Ruby on Rails](https://rubyonrails.org/)
+* [Sinatra](https://sinatrarb.com/)
+* [Utopia](https://github.com/socketry/utopia) **(Rack 3 Compatible)**
+* [WABuR](https://github.com/ohler55/wabur)
+
+### Older (possibly unsupported) web frameworks
+
+* [Ramaze](http://ramaze.net/)
+* [Rum](https://github.com/leahneukirchen/rum)
+
+## Available middleware shipped with Rack
+
+Between the server and the framework, Rack can be customized to your
+applications needs using middleware. Rack itself ships with the following
+middleware:
+
+* `Rack::CommonLogger` for creating Apache-style logfiles.
+* `Rack::ConditionalGet` for returning [Not
+  Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304)
+  responses when the response has not changed.
+* `Rack::Config` for modifying the environment before processing the request.
+* `Rack::ContentLength` for setting a `content-length` header based on body
+  size.
+* `Rack::ContentType` for setting a default `content-type` header for responses.
+* `Rack::Deflater` for compressing responses with gzip.
+* `Rack::ETag` for setting `etag` header on bodies that can be buffered.
+* `Rack::Events` for providing easy hooks when a request is received and when
+  the response is sent.
+* `Rack::Files` for serving static files.
+* `Rack::Head` for returning an empty body for HEAD requests.
+* `Rack::Lint` for checking conformance to the [Rack Specification].
+* `Rack::Lock` for serializing requests using a mutex.
+* `Rack::Logger` for setting a logger to handle logging errors.
+* `Rack::MethodOverride` for modifying the request method based on a submitted
+  parameter.
+* `Rack::Recursive` for including data from other paths in the application, and
+  for performing internal redirects.
+* `Rack::Reloader` for reloading files if they have been modified.
+* `Rack::Runtime` for including a response header with the time taken to process
+  the request.
+* `Rack::Sendfile` for working with web servers that can use optimized file
+  serving for file system paths.
+* `Rack::ShowException` for catching unhandled exceptions and presenting them in
+  a nice and helpful way with clickable backtrace.
+* `Rack::ShowStatus` for using nice error pages for empty client error
+  responses.
+* `Rack::Static` for more configurable serving of static files.
+* `Rack::TempfileReaper` for removing temporary files creating during a request.
+
+All these components use the same interface, which is described in detail in the
+[Rack Specification]. These optional components can be used in any way you wish.
+
+### Convenience interfaces
+
+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 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.
+* `Rack::Cascade` for trying additional Rack applications if an application
+  returns a not found or method not supported response.
+* `Rack::Directory` for serving files under a given directory, with directory
+  indexes.
+* `Rack::MediaType` for parsing content-type headers.
+* `Rack::Mime` for determining content-type based on file extension.
+* `Rack::RewindableInput` for making any IO object rewindable, using a temporary
+  file buffer.
+* `Rack::URLMap` to route to multiple applications inside the same process.
+
+## Configuration
+
+Rack exposes several configuration parameters to control various features of the
+implementation.
+
+### `param_depth_limit`
+
+```ruby
+Rack::Utils.param_depth_limit = 32 # default
+```
+
+The maximum amount of nesting allowed in parameters. For example, if set to 3,
+this query string would be allowed:
+
+```
+?a[b][c]=d
+```
+
+but this query string would not be allowed:
+
+```
+?a[b][c][d]=e
+```
+
+Limiting the depth prevents a possible stack overflow when parsing parameters.
+
+### `multipart_file_limit`
+
+```ruby
+Rack::Utils.multipart_file_limit = 128 # default
+```
+
+The maximum number of parts with a filename a request can contain. Accepting
+too many parts can lead to the server running out of file handles.
+
+The default is 128, which means that a single request can't upload more than 128
+files at once. Set to 0 for no limit.
+
+Can also be set via the `RACK_MULTIPART_FILE_LIMIT` environment variable.
+
+(This is also aliased as `multipart_part_limit` and `RACK_MULTIPART_PART_LIMIT` for compatibility)
+
+
+### `multipart_total_part_limit`
+
+The maximum total number of parts a request can contain of any type, including
+both file and non-file form fields.
+
+The default is 4096, which means that a single request can't contain more than
+4096 parts.
+
+Set to 0 for no limit.
+
+Can also be set via the `RACK_MULTIPART_TOTAL_PART_LIMIT` environment variable.
+
+
+## Changelog
+
+See [CHANGELOG.md](CHANGELOG.md).
+
+## Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for specific details about how to make a
+contribution to Rack.
+
+Please post bugs, suggestions and patches to [GitHub
+Issues](https://github.com/rack/rack/issues).
+
+Please check our [Security Policy](https://github.com/rack/rack/security/policy)
+for responsible disclosure and security bug reporting process. 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.
+
+## See Also
+
+### `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.
+
+* https://github.com/rack/rack-contrib
+
+### `rack-session`
+
+Provides convenient session management for Rack.
+
+* https://github.com/rack/rack-session
+
+## Thanks
+
+The Rack Core Team, consisting of
+
+* Aaron Patterson [tenderlove](https://github.com/tenderlove)
+* Samuel Williams [ioquatix](https://github.com/ioquatix)
+* Jeremy Evans [jeremyevans](https://github.com/jeremyevans)
+* Eileen Uchitelle [eileencodes](https://github.com/eileencodes)
+* Matthew Draper [matthewd](https://github.com/matthewd)
+* Rafael França [rafaelfranca](https://github.com/rafaelfranca)
+
+and the Rack Alumni
+
+* Ryan Tomayko [rtomayko](https://github.com/rtomayko)
+* Scytrin dai Kinthra [scytrin](https://github.com/scytrin)
+* 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)
+* Michael Fellinger [manveru](https://github.com/manveru)
+* Santiago Pastorino [spastorino](https://github.com/spastorino)
+* Konstantin Haase [rkh](https://github.com/rkh)
+
+would like to thank:
+
+* Adrian Madrid, for the LiteSpeed handler.
+* Christoffer Sawicki, for the first Rails adapter and `Rack::Deflater`.
+* Tim Fletcher, for the HTTP authentication code.
+* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes.
+* Armin Ronacher, for the logo and racktools.
+* Alex Beregszaszi, Alexander Kahn, Anil Wadghule, Aredridel, Ben Alpert, Dan
+  Kubb, Daniel Roethlisberger, Matt Todd, Tom Robinson, Phil Hagelberg, S. Brent
+  Faulkner, Bosko Milekic, Daniel Rodríguez Troitiño, Genki Takiuchi, Geoffrey
+  Grosenbach, Julien Sanchez, Kamal Fariz Mahyuddin, Masayoshi Takahashi,
+  Patrick Aljordm, Mig, Kazuhiro Nishiyama, Jon Bardin, Konstantin Haase, Larry
+  Siden, Matias Korhonen, Sam Ruby, Simon Chiang, Tim Connor, Timur Batyrshin,
+  and Zach Brock for bug fixing and other improvements.
+* Eric Wong, Hongli Lai, Jeremy Kemper for their continuous support and API
+  improvements.
+* Yehuda Katz and Carl Lerche for refactoring rackup.
+* Brian Candler, for `Rack::ContentType`.
+* Graham Batty, for improved handler loading.
+* Stephen Bannasch, for bug reports and documentation.
+* Gary Wright, for proposing a better `Rack::Response` interface.
+* Jonathan Buch, for improvements regarding `Rack::Response`.
+* Armin Röhrl, for tracking down bugs in the Cookie generator.
+* 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.
+* All bug reporters and patch contributors not mentioned above.
+
+## License
+
+Rack is released under the [MIT License](MIT-LICENSE).
+
+[Rack Specification]: SPEC.rdoc
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index cbb257239a3cb236a148034a2044a457ee7b0055..0000000000000000000000000000000000000000
--- a/README.rdoc
+++ /dev/null
@@ -1,320 +0,0 @@
-= \Rack, a modular Ruby webserver interface
-
-{<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]
-{<img src="http://inch-ci.org/github/rack/rack.svg?branch=master" alt="Inline docs" />}[http://inch-ci.org/github/rack/rack]
-
-\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.
-
-== Supported web servers
-
-The included *handlers* connect all kinds of web servers to \Rack:
-
-* WEBrick[https://github.com/ruby/webrick]
-* FCGI
-* CGI
-* SCGI
-* 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]
-* Iodine[https://github.com/boazsegev/iodine]
-* {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://yhbt.net/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 and many others support the \Rack API:
-
-* Camping[http://www.ruby-camping.com/]
-* Coset[http://leahneukirchen.org/repos/coset/]
-* Hanami[https://hanamirb.org/]
-* Padrino[http://padrinorb.com/]
-* 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]
-
-== Available middleware shipped with \Rack
-
-Between the server and the framework, \Rack can be customized to your
-applications needs using middleware. \Rack itself ships with the following
-middleware:
-
-* Rack::Chunked, for streaming responses using chunked encoding.
-* Rack::CommonLogger, for creating Apache-style logfiles.
-* Rack::ConditionalGet, for returning not modified responses when the response
-  has not changed.
-* Rack::Config, for modifying the environment before processing the request.
-* Rack::ContentLength, for setting Content-Length header based on body size.
-* Rack::ContentType, for setting default Content-Type header for responses.
-* Rack::Deflater, for compressing responses with gzip.
-* Rack::ETag, for setting ETag header on string bodies.
-* Rack::Events, for providing easy hooks when a request is received
-  and when the response is sent.
-* Rack::Files, for serving static files.
-* Rack::Head, for returning an empty body for HEAD requests.
-* Rack::Lint, for checking conformance to the \Rack API.
-* Rack::Lock, for serializing requests using a mutex.
-* Rack::Logger, for setting a logger to handle logging errors.
-* Rack::MethodOverride, for modifying the request method based on a submitted
-  parameter.
-* Rack::Recursive, for including data from other paths in the application,
-  and for performing internal redirects.
-* Rack::Reloader, for reloading files if they have been modified.
-* Rack::Runtime, for including a response header with the time taken to
-  process the request.
-* Rack::Sendfile, for working with web servers that can use optimized
-  file serving for file system paths.
-* Rack::ShowException, for catching unhandled exceptions and
-  presenting them in a nice and helpful way with clickable backtrace.
-* Rack::ShowStatus, for using nice error pages for empty client error
-  responses.
-* Rack::Static, for more configurable serving of static files.
-* Rack::TempfileReaper, for removing temporary files creating during a
-  request.
-
-All these components use the same interface, which is described in
-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
-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.
-* Rack::Cascade, for trying additional \Rack applications if an
-  application returns a not found or method not supported response.
-* Rack::Directory, for serving files under a given directory, with
-  directory indexes.
-* Rack::MediaType, for parsing Content-Type headers.
-* Rack::Mime, for determining Content-Type based on file extension.
-* Rack::RewindableInput, for making any IO object rewindable, using
-  a temporary file buffer.
-* Rack::URLMap, to route to multiple applications inside the same process.
-
-== 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.
-
-* https://github.com/rack/rack-contrib
-
-== rackup
-
-rackup is a useful tool for running \Rack applications, which uses the
-Rack::Builder DSL to configure middleware and build up applications
-easily.
-
-rackup automatically figures out the environment it is run in, and
-runs your application as FastCGI, CGI, or WEBrick---all from the
-same configuration.
-
-== Quick start
-
-Try the lobster!
-
-Either with the embedded WEBrick starter:
-
-    ruby -Ilib lib/rack/lobster.rb
-
-Or with rackup:
-
-    bin/rackup -Ilib example/lobster.ru
-
-By default, the lobster is found at http://localhost:9292.
-
-== Installing with RubyGems
-
-A Gem of \Rack is available at {rubygems.org}[https://rubygems.org/gems/rack]. You can install it with:
-
-    gem install rack
-
-== Usage
-
-You should require the library:
-
-    require 'rack'
-
-\Rack uses autoload to automatically load other files \Rack ships with on demand,
-so you should not need require paths under +rack+.  If you require paths under
-+rack+ without requiring +rack+ itself, things may not work correctly.
-
-== Configuration
-
-Several parameters can be modified on Rack::Utils to configure \Rack behaviour.
-
-e.g:
-
-    Rack::Utils.key_space_limit = 128
-
-=== key_space_limit
-
-The default number of bytes to allow all parameters keys in a given parameter hash to take up.
-Does not affect nested parameter hashes, so doesn't actually prevent an attacker from using
-more than this many bytes for parameter keys.
-
-Defaults to 65536 characters.
-
-=== param_depth_limit
-
-The maximum amount of nesting allowed in parameters.
-For example, if set to 3, this query string would be allowed:
-
-    ?a[b][c]=d
-
-but this query string would not be allowed:
-
-    ?a[b][c][d]=e
-
-Limiting the depth prevents a possible stack overflow when parsing parameters.
-
-Defaults to 100.
-
-=== multipart_file_limit
-
-The maximum number of parts with a filename a request can contain.
-Accepting too many part can lead to the server running out of file handles.
-
-The default is 128, which means that a single request can't upload more than 128 files at once.
-
-Set to 0 for no limit.
-
-Can also be set via the +RACK_MULTIPART_FILE_LIMIT+ environment variable.
-
-(This is also aliased as +multipart_part_limit+ and +RACK_MULTIPART_PART_LIMIT+ for compatibility)
-
-=== multipart_total_part_limit
-
-The maximum total number of parts a request can contain of any type, including
-both file and non-file form fields.
-
-The default is 4096, which means that a single request can't contain more than
-4096 parts.
-
-Set to 0 for no limit.
-
-Can also be set via the +RACK_MULTIPART_TOTAL_PART_LIMIT+ environment variable.
-
-== Changelog
-
-See {CHANGELOG.md}[https://github.com/rack/rack/blob/master/CHANGELOG.md].
-
-== Contributing
-
-See {CONTRIBUTING.md}[https://github.com/rack/rack/blob/master/CONTRIBUTING.md].
-
-== Contact
-
-Please post bugs, suggestions and patches to
-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/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/forum/#!forum/rack-devel>.
-
-Git repository (send Git patches to the mailing list):
-
-* https://github.com/rack/rack
-
-You are also welcome to join the #rack channel on irc.freenode.net.
-
-== Thanks
-
-The \Rack Core Team, consisting of
-
-* Aaron Patterson (tenderlove[https://github.com/tenderlove])
-* Samuel Williams (ioquatix[https://github.com/ioquatix])
-* Jeremy Evans (jeremyevans[https://github.com/jeremyevans])
-* Eileen Uchitelle (eileencodes[https://github.com/eileencodes])
-* Matthew Draper (matthewd[https://github.com/matthewd])
-* Rafael França (rafaelfranca[https://github.com/rafaelfranca])
-
-and the \Rack Alumni
-
-* Ryan Tomayko (rtomayko[https://github.com/rtomayko])
-* Scytrin dai Kinthra (scytrin[https://github.com/scytrin])
-* 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])
-* Michael Fellinger (manveru[https://github.com/manveru])
-* Santiago Pastorino (spastorino[https://github.com/spastorino])
-* Konstantin Haase (rkh[https://github.com/rkh])
-
-would like to thank:
-
-* Adrian Madrid, for the LiteSpeed handler.
-* Christoffer Sawicki, for the first Rails adapter and Rack::Deflater.
-* Tim Fletcher, for the HTTP authentication code.
-* Luc Heinrich for the Cookie sessions, the static file handler and bugfixes.
-* Armin Ronacher, for the logo and racktools.
-* Alex Beregszaszi, Alexander Kahn, Anil Wadghule, Aredridel, Ben
-  Alpert, Dan Kubb, Daniel Roethlisberger, Matt Todd, Tom Robinson,
-  Phil Hagelberg, S. Brent Faulkner, Bosko Milekic, Daniel Rodríguez
-  Troitiño, Genki Takiuchi, Geoffrey Grosenbach, Julien Sanchez, Kamal
-  Fariz Mahyuddin, Masayoshi Takahashi, Patrick Aljordm, Mig, Kazuhiro
-  Nishiyama, Jon Bardin, Konstantin Haase, Larry Siden, Matias
-  Korhonen, Sam Ruby, Simon Chiang, Tim Connor, Timur Batyrshin, and
-  Zach Brock for bug fixing and other improvements.
-* Eric Wong, Hongli Lai, Jeremy Kemper for their continuous support
-  and API improvements.
-* Yehuda Katz and Carl Lerche for refactoring rackup.
-* Brian Candler, for Rack::ContentType.
-* Graham Batty, for improved handler loading.
-* Stephen Bannasch, for bug reports and documentation.
-* Gary Wright, for proposing a better Rack::Response interface.
-* Jonathan Buch, for improvements regarding Rack::Response.
-* Armin Röhrl, for tracking down bugs in the Cookie generator.
-* 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.
-* All bug reporters and patch contributors not mentioned above.
-
-== Links
-
-\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>
-
-== License
-
-\Rack is released under the {MIT License}[https://opensource.org/licenses/MIT].
diff --git a/Rakefile b/Rakefile
index 237c3f26160f60d13c2b462c3e61db4c147e8fa5..7df2fa82d010d2c3148bee72dbdd2196693018fe 100644
--- a/Rakefile
+++ b/Rakefile
@@ -98,8 +98,24 @@ task "test_cov" do
   Rake::Task['test:regular'].invoke
 end
 
+desc "Run separate tests for each test file, to test directly requiring components"
+task "test:separate" do
+  fails = []
+  FileList["test/**/spec_*.rb"].each do |file|
+    puts "#{FileUtils::RUBY} -w #{file}"
+    fails << file unless system({'SEPARATE'=>'1'},  FileUtils::RUBY, '-w', file)
+  end
+  if fails.empty?
+    puts 'All test files passed'
+  else
+    puts "Failures in the following test files:"
+    puts fails
+    raise "At least one separate test failed"
+  end
+end
+
 desc "Run all the fast + platform agnostic tests"
-task test: %w[spec test:regular]
+task test: %w[spec test:regular test:separate]
 
 desc "Run all the tests we run on CI"
 task ci: :test
@@ -118,13 +134,3 @@ task rdoc: %w[changelog spec] do
               `git ls-files lib/\*\*/\*.rb`.strip.split)
   cp "contrib/rdoc.css", "doc/rdoc.css"
 end
-
-task pushdoc: :rdoc do
-  sh "rsync -avz doc/ rack.rubyforge.org:/var/www/gforge-projects/rack/doc/"
-end
-
-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"
-end
diff --git a/SECURITY_POLICY.md b/SECURITY.md
similarity index 93%
rename from SECURITY_POLICY.md
rename to SECURITY.md
index 3590fa4d508272dae33b028f8b3c81396bf82092..93e413c78f0bee9aefbefce4c134c65324241da3 100644
--- a/SECURITY_POLICY.md
+++ b/SECURITY.md
@@ -1,30 +1,30 @@
-# Rack maintenance
+# Security Policy
 
 ## Supported versions
 
 ### New features
 
-New features will only be added to the master branch and will not be made available in point releases.
+New features will only be added to the main 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.1.x
+* Current release series: 2.2.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.1.x
-* Next most recent release series: 2.0.x
+* Current release series: 2.2.x
+* Next most recent release series: 2.1.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.1.x
-* Next most recent release series: 2.0.x
+* Current release series: 2.2.x
+* Next most recent release series: 2.1.x
 * Last major release series: 1.6.x
 
 ### Unsupported Release Series
diff --git a/SPEC.rdoc b/SPEC.rdoc
index 277142376e1e24b1fc3f79accc41dcf815ae33a5..ddf474ae1d0aba06bf0d553d9494fdea6e38d0ea 100644
--- a/SPEC.rdoc
+++ b/SPEC.rdoc
@@ -1,23 +1,27 @@
-This specification aims to formalize the Rack protocol.  You
+This specification aims to formalize the Rack protocol. You
 can (and should) use Rack::Lint to enforce it.
 
 When you develop middleware, be sure to add a Lint before and
 after to catch all mistakes.
+
 = Rack applications
+
 A Rack application is a Ruby object (not a class) that
 responds to +call+.
 It takes exactly one argument, the *environment*
-and returns an Array of exactly three values:
+and returns a non-frozen Array of exactly three values:
 The *status*,
 the *headers*,
 and the *body*.
+
 == The Environment
+
 The environment must be an unfrozen instance of Hash that includes
-CGI-like headers.  The application is free to modify the
+CGI-like headers. The Rack application is free to modify the
 environment.
 
 The environment is required to include these variables
-(adopted from PEP333), except when they'd be empty, but see
+(adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see
 below.
 <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
                           "GET" or "POST". This cannot ever
@@ -42,17 +46,20 @@ below.
 <tt>QUERY_STRING</tt>:: The portion of the request URL that
                         follows the <tt>?</tt>, if any. May be
                         empty, but is always required!
-<tt>SERVER_NAME</tt>, <tt>SERVER_PORT</tt>::
-                       When combined with <tt>SCRIPT_NAME</tt> and
+<tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
                        <tt>PATH_INFO</tt>, these variables can be
                        used to complete the URL. Note, however,
                        that <tt>HTTP_HOST</tt>, if present,
                        should be used in preference to
                        <tt>SERVER_NAME</tt> for reconstructing
                        the request URL.
-                       <tt>SERVER_NAME</tt> and <tt>SERVER_PORT</tt>
-                       can never be empty strings, and so
-                       are always required.
+                       <tt>SERVER_NAME</tt> can never be an empty
+                       string, and so is always required.
+<tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
+                       server is running on. Should be specified if
+                       the server is running on a non-standard port.
+<tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used
+                           for the request.
 <tt>HTTP_</tt> Variables:: Variables corresponding to the
                            client-supplied HTTP request
                            headers (i.e., variables whose
@@ -66,40 +73,19 @@ below.
                            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
-                        See Rack::VERSION, that corresponds to
-                        the version of this SPEC.
 <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
                            request URL.
 <tt>rack.input</tt>:: See below, the input stream.
 <tt>rack.errors</tt>:: See below, the error stream.
-<tt>rack.multithread</tt>:: true if the application object may be
-                            simultaneously invoked by another thread
-                            in the same process, false otherwise.
-<tt>rack.multiprocess</tt>:: true if an equivalent application object
-                             may be simultaneously invoked by another
-                             process, false otherwise.
-<tt>rack.run_once</tt>:: true if the server expects
-                         (but does not guarantee!) that the
-                         application will only be invoked this one
-                         time during the life of its containing
-                         process. Normally, this will only be true
-                         for a server based on CGI
-                         (or something similar).
-<tt>rack.hijack?</tt>:: present and true if the server supports
-                        connection hijacking. See below, hijacking.
-<tt>rack.hijack</tt>:: an object responding to #call that must be
-                       called at least once before using
-                       rack.hijack_io.
-                       It is recommended #call return rack.hijack_io
-                       as well as setting it in env if necessary.
-<tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
-                          has received #call, this will contain
-                          an object resembling an IO. See hijacking.
+<tt>rack.hijack?</tt>:: See below, if present and true, indicates
+                        that the server supports partial hijacking.
+<tt>rack.hijack</tt>:: See below, if present, an object responding
+                       to +call+ that is used to perform a full
+                       hijack.
 Additional environment specifications have approved to
-standardized middleware APIs.  None of these are required to
+standardized middleware APIs. None of these are required to
 be implemented by the server.
-<tt>rack.session</tt>:: A hash like interface for storing
+<tt>rack.session</tt>:: A hash-like interface for storing
                         request session data.
                         The store must implement:
                         store(key, value)         (aliased as []=);
@@ -122,6 +108,11 @@ and should be prefixed uniquely.  The prefix <tt>rack.</tt>
 is reserved for use with the Rack core distribution and other
 accepted specifications and must not be used otherwise.
 
+The <tt>SERVER_PORT</tt> must be an Integer if set.
+The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
+The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
+The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>.
+If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>.
 The environment must not contain the keys
 <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
 (use the versions without <tt>HTTP_</tt>).
@@ -129,26 +120,32 @@ The CGI keys (named without a period) must have String values.
 If the string values for CGI keys contain non-ASCII characters,
 they should use ASCII-8BIT encoding.
 There are the following restrictions:
-* <tt>rack.version</tt> must be an array of Integers.
 * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
 * There must be a valid input stream in <tt>rack.input</tt>.
 * There must be a valid error stream in <tt>rack.errors</tt>.
-* There may be a valid hijack stream in <tt>rack.hijack_io</tt>
+* There may be a valid hijack callback in <tt>rack.hijack</tt>
 * The <tt>REQUEST_METHOD</tt> must be a valid token.
 * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
 * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
 * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
 * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
-  set.  <tt>PATH_INFO</tt> should be <tt>/</tt> if
+  set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
   <tt>SCRIPT_NAME</tt> is empty.
   <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
+<tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
+processed. This would typically be invoked after sending the response to the client, but it could also be
+invoked if an error occurs while generating the response or sending the response; in that case, the error
+argument will be a subclass of +Exception+.
+The callables are invoked with +env, status, headers, error+ arguments and should not raise any
+exceptions. They should be invoked in reverse order of registration.
+
 === The Input Stream
 
 The input stream is an IO-like object which contains the raw HTTP
 POST data.
 When applicable, its external encoding must be "ASCII-8BIT" and it
 must be opened in binary mode, for Ruby 1.9 compatibility.
-The input stream must respond to +gets+, +each+, +read+ and +rewind+.
+The input stream must respond to +gets+, +each+, and +read+.
 * +gets+ must be called without arguments and return a string,
   or +nil+ on EOF.
 * +read+ behaves like IO#read.
@@ -169,120 +166,175 @@ The input stream must respond to +gets+, +each+, +read+ and +rewind+.
   If +buffer+ is given, then the read data will be placed
   into +buffer+ instead of a newly created String object.
 * +each+ must be called without arguments and only yield Strings.
-* +rewind+ must be called without arguments. It rewinds the input
-  stream back to the beginning. It must not raise Errno::ESPIPE:
-  that is, it may not be a pipe or a socket. Therefore, handler
-  developers must buffer the input data into some rewindable object
-  if the underlying input stream is not rewindable.
-* +close+ must never be called on the input stream.
+* +close+ can be called on the input stream to indicate that the
+any remaining input is not needed.
+
 === The Error Stream
+
 The error stream must respond to +puts+, +write+ and +flush+.
 * +puts+ must be called with a single argument that responds to +to_s+.
 * +write+ must be called with a single argument that is a String.
 * +flush+ must be called without arguments and must be called
   in order to make the error appear for sure.
 * +close+ must never be called on the error stream.
+
 === Hijacking
-==== Request (before status)
-If rack.hijack? is true then rack.hijack must respond to #call.
-rack.hijack must return the io that will also be assigned (or is
-already present, in rack.hijack_io.
 
-rack.hijack_io must respond to:
-<tt>read, write, read_nonblock, write_nonblock, flush, close,
-close_read, close_write, closed?</tt>
+The hijacking interfaces provides a means for an application to take
+control of the HTTP connection. There are two distinct hijack
+interfaces: full hijacking where the application takes over the raw
+connection, and partial hijacking where the application takes over
+just the response body stream. In both cases, the application is
+responsible for closing the hijacked stream.
 
-The semantics of these IO methods must be a best effort match to
-those of a normal ruby IO or Socket object, using standard
-arguments and raising standard exceptions. Servers are encouraged
-to simply pass on real IO objects, although it is recognized that
-this approach is not directly compatible with SPDY and HTTP 2.0.
-
-IO provided in rack.hijack_io should preference the
-IO::WaitReadable and IO::WaitWritable APIs wherever supported.
-
-There is a deliberate lack of full specification around
-rack.hijack_io, as semantics will change from server to server.
-Users are encouraged to utilize this API with a knowledge of their
-server choice, and servers may extend the functionality of
-hijack_io to provide additional features to users. The purpose of
-rack.hijack is for Rack to "get out of the way", as such, Rack only
-provides the minimum of specification and support.
-
-If rack.hijack? is false, then rack.hijack should not be set.
-
-If rack.hijack? is false, then rack.hijack_io should not be set.
-==== Response (after headers)
-It is also possible to hijack a response after the status and headers
-have been sent.
-In order to do this, an application may set the special header
-<tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
-accepting an argument that conforms to the <tt>rack.hijack_io</tt>
-protocol.
-
-After the headers have been sent, and this hijack callback has been
-called, the application is now responsible for the remaining lifecycle
-of the IO. The application is also responsible for maintaining HTTP
-semantics. Of specific note, in almost all cases in the current SPEC,
-applications will have wanted to specify the header Connection:close in
-HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
-returning hijacked sockets to the web server. For that purpose, use the
-body streaming API instead (progressively yielding strings via each).
-
-Servers must ignore the <tt>body</tt> part of the response tuple when
-the <tt>rack.hijack</tt> response API is in use.
-
-The special response header <tt>rack.hijack</tt> must only be set
-if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
-==== Conventions
-* Middleware should not use hijack unless it is handling the whole
-  response.
-* Middleware may wrap the IO object for the response pattern.
-* Middleware should not wrap the IO object for the request pattern. The
-  request pattern is intended to provide the hijacker with "raw tcp".
+Full hijacking only works with HTTP/1. Partial hijacking is functionally
+equivalent to streaming bodies, and is still optionally supported for
+backwards compatibility with older Rack versions.
+
+==== Full Hijack
+
+Full hijack is used to completely take over an HTTP/1 connection. It
+occurs before any headers are written and causes the request to
+ignores any response generated by the application.
+
+It is intended to be used when applications need access to raw HTTP/1
+connection.
+
+If +rack.hijack+ is present in +env+, it must respond to +call+
+and return an +IO+ instance which can be used to read and write
+to the underlying connection using HTTP/1 semantics and
+formatting.
+
+==== Partial Hijack
+
+Partial hijack is used for bi-directional streaming of the request and
+response body. It occurs after the status and headers are written by
+the server and causes the server to ignore the Body of the response.
+
+It is intended to be used when applications need bi-directional
+streaming.
+
+If +rack.hijack?+ is present in +env+ and truthy,
+an application may set the special response header +rack.hijack+
+to an object that responds to +call+,
+accepting a +stream+ argument.
+
+After the response status and headers have been sent, this hijack
+callback will be invoked with a +stream+ argument which follows the
+same interface as outlined in "Streaming Body". Servers must
+ignore the +body+ part of the response tuple when the
++rack.hijack+ response header is present. Using an empty +Array+
+instance is recommended.
+
+The special response header +rack.hijack+ must only be set
+if the request +env+ has a truthy +rack.hijack?+.
 == The Response
+
 === The Status
-This is an HTTP status. When parsed as integer (+to_i+), it must be
-greater than or equal to 100.
+
+This is an HTTP status. It must be an Integer greater than or equal to
+100.
+
 === The Headers
-The header must respond to +each+, and yield values of key and value.
+
+The headers must be a unfrozen Hash.
 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 must not contain a +Status+ key.
-The header must conform to RFC7230 token specification, i.e. cannot
+Header keys must conform to RFC7230 token specification, i.e. cannot
 contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
-The values of the header must be Strings,
-consisting of lines (for multiple header values, e.g. multiple
-<tt>Set-Cookie</tt> values) separated by "\\n".
-The lines must not contain characters below 037.
-=== The Content-Type
-There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-204 or 304.
-=== The Content-Length
-There must not be a <tt>Content-Length</tt> header when the
-+Status+ is 1xx, 204 or 304.
+Header keys must not contain uppercase ASCII characters (A-Z).
+Header values must be either a String instance,
+or an Array of String instances,
+such that each String instance must not contain characters below 037.
+
+=== The content-type
+
+There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
+204, or 304.
+
+=== The content-length
+
+There must not be a <tt>content-length</tt> header key when the
++Status+ is 1xx, 204, or 304.
+
 === The Body
-The Body must respond to +each+
+
+The Body is typically an +Array+ of +String+ instances, an enumerable
+that yields +String+ instances, a +Proc+ instance, or a File-like
+object.
+
+The Body must respond to +each+ or +call+. It may optionally respond
+to +to_path+ or +to_ary+. A Body that responds to +each+ is considered
+to be an Enumerable Body. A Body that responds to +call+ is considered
+to be a Streaming Body.
+
+A Body that responds to both +each+ and +call+ must be treated as an
+Enumerable Body, not a Streaming Body. If it responds to +each+, you
+must call +each+ and not +call+. If the Body doesn't respond to
++each+, then you can assume it responds to +call+.
+
+The Body must either be consumed or returned. The Body is consumed by
+optionally calling either +each+ or +call+.
+Then, if the Body responds to +close+, it must be called to release
+any resources associated with the generation of the body.
+In other words, +close+ must always be called at least once; typically
+after the web server has sent the response to the client, but also in
+cases where the Rack application makes internal/virtual requests and
+discards the response.
+
+
+After calling +close+, the Body is considered closed and should not
+be consumed again.
+If the original Body is replaced by a new Body, the new Body must
+also consume the original Body by calling +close+ if possible.
+
+If the Body responds to +to_path+, it must return a +String+
+path for the local file system whose contents are identical
+to that produced by calling +each+; this may be used by the
+server as an alternative, possibly more efficient way to
+transport the response. The +to_path+ method does not consume
+the body.
+
+==== Enumerable Body
+
+The Enumerable Body must respond to +each+.
+It must only be called once.
+It must not be called after being closed.
 and must only yield String values.
 
 The Body itself should not be an instance of String, as this will
 break in Ruby 1.9.
 
-If the Body responds to +close+, it will be called after iteration. If
-the body is replaced by a middleware after action, the original body
-must be closed first, if it responds to close.
+Middleware must not call +each+ directly on the Body.
+Instead, middleware can return a new Body that calls +each+ on the
+original Body, yielding at least once per iteration.
 
-If the Body responds to +to_path+, it must return a String
-identifying the location of a file whose contents are identical
-to that produced by calling +each+; this may be used by the
-server as an alternative, possibly more efficient way to
-transport the response.
+If the Body responds to +to_ary+, it must return an +Array+ whose
+contents are identical to that produced by calling +each+.
+Middleware may call +to_ary+ directly on the Body and return a new
+Body in its place. In other words, middleware can only process the
+Body directly if it responds to +to_ary+. If the Body responds to both
++to_ary+ and +close+, its implementation of +to_ary+ must call
++close+.
+
+==== Streaming Body
+
+The Streaming Body must respond to +call+.
+It must only be called once.
+It must not be called after being closed.
+It takes a +stream+ argument.
+
+The +stream+ argument must implement:
+<tt>read, write, <<, flush, close, close_read, close_write, closed?</tt>
+
+The semantics of these IO methods must be a best effort match to
+those of a normal Ruby IO or Socket object, using standard arguments
+and raising standard exceptions. Servers are encouraged to simply
+pass on real IO objects, although it is recognized that this approach
+is not directly compatible with HTTP/2.
 
-The Body commonly is an Array of Strings, the application
-instance itself, or a File-like object.
 == Thanks
-Some parts of this specification are adopted from PEP333: Python
-Web Server Gateway Interface
-v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
-everyone involved in that effort.
+Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/]
+I'd like to thank everyone involved in that effort.
diff --git a/UPGRADE-GUIDE.md b/UPGRADE-GUIDE.md
new file mode 100644
index 0000000000000000000000000000000000000000..290fac24d96636964206adcdaa26cdb601f39956
--- /dev/null
+++ b/UPGRADE-GUIDE.md
@@ -0,0 +1,328 @@
+# Rack 3 Upgrade Guide
+
+This document is a work in progress, but outlines some of the key changes in
+Rack 3 which you should be aware of in order to update your server, middleware
+and/or applications.
+
+## Interface Changes
+
+### Rack 2 & Rack 3 compatibility
+
+Most applications can be compatible with Rack 2 and 3 by following the strict intersection of the Rack Specifications, notably:
+
+- Response array must now be non-frozen.
+- Response `status` must now be an integer greater than or equal to 100.
+- Response `headers` must now be an unfrozen hash.
+- Response header keys can no longer include uppercase characters.
+- `rack.input` is no longer required to be rewindable.
+- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys.
+- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
+- `rack.hijack_io` has been removed completely.
+- `SERVER_PROTOCOL` is now a required key, matching the HTTP protocol used in the request.
+- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`.
+
+There is one changed feature in Rack 3 which is not backwards compatible:
+
+- Response header values can be an `Array` to handle multiple values (and no longer supports `\n` encoded headers).
+
+You can achieve compatibility by using `Rack::Response#add_header` which provides an interface for adding headers without concern for the underlying format.
+
+There is one new feature in Rack 3 which is not directly backwards compatible:
+
+- Response body can now respond to `#call` (streaming body) instead of `#each` (enumerable body), for the equivalent of response hijacking in previous versions.
+
+If supported by your server, you can use partial rack hijack instead (or wrap this behaviour in a middleware).
+
+### `config.ru` `Rack::Builder#run` now accepts block
+
+Previously, `Rack::Builder#run` method would only accept a callable argument:
+
+```ruby
+run lambda{|env| [200, {}, ["Hello World"]]}
+```
+
+This can be rewritten more simply:
+
+```ruby
+run do |env|
+  [200, {}, ["Hello World"]]
+end
+```
+
+### Response bodies can be used for bi-directional streaming
+
+Previously, the `rack.hijack` response header could be used for implementing
+bi-directional streaming (e.g. WebSockets).
+
+```ruby
+def call(env)
+  stream_callback = proc do |stream|
+    stream.read(...)
+    stream.write(...)
+  ensure
+    stream.close(...)
+  end
+
+  return [200, {'rack.hijack' => stream_callback}, []]
+end
+```
+
+This feature was optional and tricky to use correctly. You can now achieve the
+same thing by giving `stream_callback` as the response body:
+
+```ruby
+def call(env)
+  stream_callback = proc do |stream|
+    stream.read(...)
+    stream.write(...)
+  ensure
+    stream.close(...)
+  end
+
+  return [200, {}, stream_callback]
+end
+```
+
+### `Rack::Session` was moved to a separate gem.
+
+Previously, `Rack::Session` was part of the `rack` gem. Not every application
+needs it, and it increases the security surface area of the `rack`, so it was
+decided to extract it into its own gem `rack-session` which can be updated
+independently.
+
+Applications that make use of `rack-session` will need to add that gem as a
+dependency:
+
+```ruby
+gem 'rack-session'
+```
+
+This provides all the previously available functionality.
+
+### `bin/rackup` was moved to a separate gem.
+
+Previously, the `rackup` executable was included with Rack. Because WEBrick is
+no longer a default gem with Ruby, we had to make a decision: either `rack`
+should depend on `webrick` or we should move that functionality into a
+separate gem. We chose the latter which will hopefully allow us to innovate
+more rapidly on the design and implementation of `rackup` separately from
+"rack the interface".
+
+In Rack 3, you will need to include:
+
+```ruby
+gem 'rackup'
+```
+
+This provides all the previously available functionality.
+
+## Request Changes
+
+### `rack.version` is no longer required
+
+Previously, the "rack protocol version" was available in `rack.version` but it
+was not practically useful, so it has been removed as a requirement.
+
+### `rack.multithread`/`rack.multiprocess`/`rack.run_once` are no longer required
+
+Previously, servers tried to provide these keys to reflect the execution
+environment. These come too late to be useful, so they have been removed as  a
+requirement.
+
+### `rack.hijack?` now only applies to partial hijack
+
+Previously, both full and partial hijiack were controlled by the presence and
+value of `rack.hijack?`. Now, it only applies to partial hijack (which now can
+be replaced by streaming bodies).
+
+### `rack.hijack` alone indicates that you can execute a full hijack
+
+Previously, `rack.hijack?` had to be truthy, as well as having `rack.hijack`
+present in the request environment. Now, the presence of the `rack.hijack`
+callback is enough.
+
+### `rack.hijack_io` is removed
+
+Previously, the server would try to set `rack.hijack_io` into the request
+environment when `rack.hijack` was invoked for a full hijack. This was often
+impossible if a middleware had called `env.dup`, so this requirement has been
+dropped entirely.
+
+### `rack.input` is no longer required to be rewindable
+
+Previously, `rack.input` was required to be rewindable, i.e. `io.seek(0)` but
+this was only generally possible with a file based backing, which prevented
+efficient streaming of request bodies. Now, `rack.input` is not required to be
+rewindable.
+
+## Response Changes
+
+### Response must be mutable
+
+Rack 3 requires the response Array `[status, headers, body]` to be mutable.
+Existing code that uses a frozen response will need to be changed:
+
+```ruby
+NOT_FOUND = [404, {}, ["Not Found"]].freeze
+
+def call(env)
+  ...
+  return NOT_FOUND
+end
+```
+
+should be rewritten as:
+
+```ruby
+def not_found
+  [404, {}, ["Not Found"]]
+end
+
+def call(env)
+  ...
+  return not_found
+end
+```
+
+Note there is a subtle bug in the former version: the headers hash is mutable
+and can be modified, and these modifications can leak into subsequent requests.
+
+### Response headers must be a mutable hash
+
+Rack 3 requires response headers to be a mutable hash. Previously it could be
+any object that would respond to `#each` and yield `key`/`value` pairs.
+Previously, the following was acceptable:
+
+```ruby
+def call(env)
+  return [200, [['content-type', 'text/plain']], ["Hello World"]]
+end
+```
+
+Now you must use a hash instance:
+
+```ruby
+def call(env)
+  return [200, {'content-type' => 'text/plain'}, ["Hello World"]]
+end
+```
+
+This ensures middleware can predictably update headers as needed.
+
+### Response Headers must be lower case
+
+Rack 3 requires all response headers to be lower case. This is to simplify
+fetching and updating response headers. Previously you had to use something like
+`Rack::HeadersHash`
+
+```ruby
+def call(env)
+  response = @app.call(env)
+  # HeaderHash must allocate internal objects and compute lower case keys:
+  headers = Rack::Utils::HeaderHash[response[1]]
+
+  cache_response(headers['ETag'], response)
+
+  ...
+end
+```
+
+but now you must just use the normal form for HTTP header:
+
+```ruby
+def call(env)
+  response = @app.call(env)
+  # A plain hash with lower case keys:
+  headers = response[1]
+
+  cache_response(headers['etag'], response)
+
+  ...
+end
+```
+
+If you want your code to work with Rack 3 without having to manually lowercase
+each header key used, instead of using a plain hash for headers, you can use
+`Rack::Headers` on Rack 3.
+
+```ruby
+  headers = defined?(Rack::Headers) ? Rack::Headers.new : {}
+```
+
+`Rack::Headers` is a subclass of Hash that will automatically lowercase keys:
+
+```ruby
+  headers = Rack::Headers.new
+  headers['Foo'] = 'bar'
+  headers['FOO'] # => 'bar'
+  headers.keys   # => ['foo']
+```
+
+### Multiple response header values are encoded using an `Array`
+
+Response header values can be an Array to handle multiple values (and no longer
+supports `\n` encoded headers). If you use `Rack::Response`, you don't need to
+do anything, but if manually append values to response headers, you will need to
+promote them to an Array, e.g.
+
+```ruby
+def set_cookie_header!(headers, key, value)
+  if header = headers[SET_COOKIE]
+    if header.is_a?(Array)
+      header << set_cookie_header(key, value)
+    else
+      headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
+    end
+  else
+    headers[SET_COOKIE] = set_cookie_header(key, value)
+  end
+end
+```
+
+### Response body might not respond to `#each`
+
+Rack 3 has more strict requirements on response bodies. Previously, response
+body would only need to respond to `#each` and optionally `#close`. In addition,
+there was no way to determine whether it was safe to call `#each` and buffer the
+response.
+
+### Response bodies can be buffered if they expose `#to_ary`
+
+If your body responds to `#to_ary` then it must return an `Array` whose contents
+are identical to that produced by calling `#each`. If the body responds to both
+`#to_ary` and `#close` then its implementation of `#to_ary` must also call
+`#close`.
+
+Previously, it was not possible to determine whether a response body was
+immediately available (could be buffered) or was streaming chunks. This case is
+now unambiguously exposed by `#to_ary`:
+
+```ruby
+def call(env)
+  status, headers, body = @app.call(env)
+
+  # Check if we can buffer the body into an Array, so we can compute a digest:
+  if body.respond_to?(:to_ary)
+    body = body.to_ary
+    digest = digest_body(body)
+    headers[ETAG_STRING] = %(W/"#{digest}") if digest
+  end
+
+  return [status, headers, body]
+end
+```
+
+### Middleware should not directly modify the response body
+
+Be aware that the response body might not respond to `#each` and you must now
+check if the body responds to `#each` or not to determine if it is an enumerable
+or streaming body.
+
+You must not call `#each` directly on the body and instead you should return a
+new body that calls `#each` on the original body.
+
+### Status needs to be an `Integer`
+
+The response status is now required to be an `Integer` with a value greater or equal to 100.
+
+Previously any object that responded to `#to_i` was allowed, so a response like `["200", {}, ""]` will need to be replaced with `[200, {}, ""]` and so on. This can be done by calling `#to_i` on the status object yourself.
diff --git a/bin/rackup b/bin/rackup
deleted file mode 100755
index 58988a0b32d16a93e818e02533fd5745b943dc09..0000000000000000000000000000000000000000
--- a/bin/rackup
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require "rack"
-Rack::Server.start
diff --git a/config/external.yaml b/config/external.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..92e64509bd461b00e8d8c1878affe5e6590387e4
--- /dev/null
+++ b/config/external.yaml
@@ -0,0 +1,3 @@
+protocol-rack:
+  url: https://github.com/socketry/protocol-rack
+  command: bundle exec bake test
diff --git a/contrib/LICENSE.md b/contrib/LICENSE.md
new file mode 100644
index 0000000000000000000000000000000000000000..fc9e093b9124e963a30ae484aa34932653b3aa6f
--- /dev/null
+++ b/contrib/LICENSE.md
@@ -0,0 +1,7 @@
+# Contributed Materials
+
+## Logo
+
+Copyright, 2022, by Malene Laugesen.
+
+This work is licensed under a [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](https://creativecommons.org/licenses/by-nc-nd/4.0/).
diff --git a/contrib/logo.webp b/contrib/logo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..62ef28f5b15fe88034d1285c802458005da07292
Binary files /dev/null and b/contrib/logo.webp differ
diff --git a/contrib/rack.png b/contrib/rack.png
deleted file mode 100644
index 0920c4fb0c127121f458abfb9d9a18f3bb8ce6a5..0000000000000000000000000000000000000000
Binary files a/contrib/rack.png and /dev/null differ
diff --git a/contrib/rack.svg b/contrib/rack.svg
deleted file mode 100644
index 0d3f961cc4f7a5dfa9c2627d8f3e3538eb3aee13..0000000000000000000000000000000000000000
--- a/contrib/rack.svg
+++ /dev/null
@@ -1,150 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<!-- Created with Inkscape (http://www.inkscape.org/) -->
-
-<svg
-   xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   width="744.09448819"
-   height="1052.3622047"
-   id="svg2"
-   version="1.1"
-   inkscape:version="0.48.3.1 r9886"
-   sodipodi:docname="rack.svg">
-  <defs
-     id="defs4">
-    <linearGradient
-       id="linearGradient3837"
-       osb:paint="solid">
-      <stop
-         style="stop-color:#000000;stop-opacity:1;"
-         offset="0"
-         id="stop3839" />
-    </linearGradient>
-  </defs>
-  <sodipodi:namedview
-     id="base"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageopacity="0.0"
-     inkscape:pageshadow="2"
-     inkscape:zoom="0.98994949"
-     inkscape:cx="230.49849"
-     inkscape:cy="656.46253"
-     inkscape:document-units="px"
-     inkscape:current-layer="layer1"
-     showgrid="false"
-     showguides="false"
-     inkscape:guide-bbox="true"
-     inkscape:window-width="1920"
-     inkscape:window-height="1056"
-     inkscape:window-x="1920"
-     inkscape:window-y="24"
-     inkscape:window-maximized="1">
-    <sodipodi:guide
-       orientation="1,0"
-       position="645.99255,757.10933"
-       id="guide2995" />
-    <sodipodi:guide
-       orientation="1,0"
-       position="488.40876,686.90373"
-       id="guide2997" />
-    <sodipodi:guide
-       orientation="1,0"
-       position="176.7767,748.52304"
-       id="guide2999" />
-    <sodipodi:guide
-       orientation="1,0"
-       position="355.71429,782.85714"
-       id="guide3005" />
-    <sodipodi:guide
-       orientation="0,1"
-       position="527.14286,642.85714"
-       id="guide3007" />
-    <sodipodi:guide
-       orientation="0,1"
-       position="431.42857,507.85714"
-       id="guide3009" />
-    <sodipodi:guide
-       orientation="0,1"
-       position="488.40876,783.57143"
-       id="guide3011" />
-    <sodipodi:guide
-       orientation="0,1"
-       position="505,372.85714"
-       id="guide3013" />
-  </sodipodi:namedview>
-  <metadata
-     id="metadata7">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <g
-     inkscape:label="Layer 1"
-     inkscape:groupmode="layer"
-     id="layer1">
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 176.28125,201.03125 0,0.625 0,395.125 0,0.375 0.34375,0.0937 312.34375,91.6875 0.625,0.1875 0,-0.65625 0.125,-419.09375 0,-0.40625 -0.40625,-0.0937 -312.4375,-67.71875 -0.59375,-0.125 z m 1,1.21875 311.4375,67.5 -0.125,418.0625 -311.3125,-91.375 0,-394.1875 z"
-       id="path2985"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 647.21875,206.59375 -0.6875,0.28125 -157.59375,62.21875 -0.3125,0.125 0,0.34375 0.1875,419.125 0,0.75 0.6875,-0.28125 156.0625,-63.1875 0.3125,-0.125 0,-0.34375 1.34375,-418.15625 0,-0.75 z m -1,1.4375 -1.34375,417.125 -155.0625,62.78125 -0.1875,-418.03125 156.59375,-61.875 z"
-       id="path2993"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.6875,137.40625 -0.15625,0.0625 L 176.96875,201.0625 177.3125,202 355.75,138.4375 646.78125,207.53125 647,206.5625 l -291.15625,-69.125 -0.15625,-0.0312 z"
-       id="path3003"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.71875,277.53125 -0.125,0.0312 -178.9375,47.96875 -1.8125,0.46875 1.8125,0.5 311.625,83.5 0.15625,0.0312 0.125,-0.0625 157.59375,-53.65625 1.5625,-0.53125 -1.59375,-0.4375 -290.28125,-77.78125 -0.125,-0.0312 z m 0,1.03125 L 644.3125,355.90625 488.375,409 178.71875,326 l 177,-47.4375 z"
-       id="path3015"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.21875,240.9375 0,37.125 1,0 0,-37.125 -1,0 z"
-       id="path3017"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 176.28125,202.65625 0,393.28125 1,0 0,-393.28125 -1,0 z"
-       id="path3019"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.71875,409 -0.125,0.0312 L 177,455.8125 l -1.78125,0.46875 1.78125,0.5 L 488.28125,545 l 0.15625,0.0312 0.125,-0.0625 156.71875,-56.25 1.46875,-0.53125 -1.53125,-0.40625 -289.375,-78.75 -0.125,-0.0312 z m 0,1.03125 287.6875,78.28125 L 488.375,544 179.03125,456.3125 355.71875,410.03125 z"
-       id="path3021"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.71875,544 -0.15625,0.0312 -178.5625,52.3125 0.28125,0.96875 178.4375,-52.28125 289.59375,80.25 0.28125,-0.96875 -289.75,-80.28125 -0.125,-0.0312 z"
-       id="path3023"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.21875,374.34375 0,35.15625 1,0 0,-35.15625 -1,0 z"
-       id="path3025"
-       inkscape:connector-curvature="0" />
-    <path
-       style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:10;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans;stroke-opacity:1;stroke-miterlimit:30;stroke-dasharray:none;stroke-linecap:round;stroke-linejoin:round"
-       d="m 355.1875,507.03125 0,37.4375 1.03125,0 0,-37.4375 -1.03125,0 z"
-       id="path3027"
-       inkscape:connector-curvature="0" />
-  </g>
-</svg>
diff --git a/contrib/rack_logo.svg b/contrib/rack_logo.svg
deleted file mode 100644
index 8287c9da09d7c331c4c173e20dc02ef17e509f75..0000000000000000000000000000000000000000
--- a/contrib/rack_logo.svg
+++ /dev/null
@@ -1,164 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
-<svg
-	xmlns:dc="http://purl.org/dc/elements/1.1/"
-	xmlns:cc="http://web.resource.org/cc/"
-	xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-	xmlns:svg="http://www.w3.org/2000/svg"
-	xmlns:xlink="http://www.w3.org/1999/xlink"
-	xmlns="http://www.w3.org/2000/svg"
-	version="1.1"
-	x="0px"
-	y="0px"
-	viewBox="-480 209.5 750 375"
-	style="enable-background:new -480 209.5 750 375;"
-	xml:space="preserve">
-<style type="text/css">
-	.st0{display:none;}
-	.st1{display:inline;}
-	.st2{fill:none;stroke:#000000;stroke-width:11.4452;stroke-linejoin:round;}
-	.st3{fill:none;stroke:#000000;stroke-width:11.6061;stroke-linejoin:round;}
-	.st4{fill:none;stroke:#000000;stroke-width:7.7374;stroke-linejoin:round;}
-	.st5{fill:none;stroke:#000000;stroke-width:7.7374;}
-	.st6{stroke:#000000;stroke-width:5.8905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:30;}
-</style>
-<g id="Logo_Text" inkscape:export-ydpi="360" inkscape:export-xdpi="360" inkscape:export-filename="/home/blackbird/Desktop/rack_logo_final.png" inkscape:version="0.44" sodipodi:version="0.32" sodipodi:docname="rack_logo.svg" sodipodi:docbase="/home/blackbird/Desktop" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://web.resource.org/cc/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">
-	
-		<sodipodi:namedview  width="200px" height="100px" inkscape:window-y="24" inkscape:window-x="0" inkscape:window-height="975" inkscape:window-width="1400" inkscape:current-layer="layer1" inkscape:document-units="px" inkscape:pageshadow="2" inkscape:pageopacity="0.0" inkscape:cy="67.007749" inkscape:cx="148.56163" inkscape:zoom="3.959798" objecttolerance="10" guidetolerance="10" gridtolerance="10000" borderopacity="1.0" bordercolor="#666666" pagecolor="#ffffff" id="base">
-		</sodipodi:namedview>
-	<path id="text2980" d="M-84.9,419.5v27.1c-2-0.7-4.2-1.1-6.8-1.1c-3,0-5.8,0.7-8.6,2.1c-2.7,1.4-4.9,3.1-6.5,5.3
-		c-1.2,1.6-2,3.6-2.6,6c-0.4,1.5-0.6,3.6-0.6,6.2v23.5h-33.7v-68.5h31.5v15.3c2.8-5.9,5.8-10.2,9.2-12.6c3.4-2.5,7.6-3.8,12.5-3.8
-		C-88.7,419.1-86.9,419.2-84.9,419.5 M-33.2,488.7V479c-5.3,7.8-13.2,11.7-23.7,11.7c-7.8,0-13.8-2-18.1-5.9
-		c-4.3-3.9-6.5-8.9-6.5-14.9c0-4.4,1-8.3,3-11.6c2.1-3.4,5-6.1,8.7-8.1c2.4-1.3,5.7-2.4,10-3.3c4.4-0.9,9.5-1.3,15.5-1.3
-		c2.7,0,6.2,0.1,10.4,0.3c-0.1-3.1-1.3-5.6-3.7-7.4c-2.4-1.9-6.4-2.8-12-2.8c-3.6,0-7.4,0.4-11.2,1.3c-2.8,0.7-7.1,2.2-12.7,4.5v-19
-		c5.2-1.5,9.9-2.5,14.3-3.1c4.4-0.6,9.3-0.9,14.6-0.9c8.4,0,15.6,0.8,21.6,2.3c3,0.7,5.7,1.9,8.3,3.4c2.5,1.5,4.5,3.2,6,5.1
-		c1.5,1.8,2.7,4.2,3.7,7c1,2.8,1.5,6.7,1.5,11.7v40.7H-33.2 M-33,458.2c-5.4,0-9.1,0.1-11.2,0.6c-2.1,0.4-3.8,1.3-5,2.8
-		c-1.2,1.4-1.8,3-1.8,4.8c0,1.9,0.7,3.6,2.2,5c1.5,1.4,3.3,2.1,5.4,2.1c2.6,0,5-1.1,7.2-3.4c2.2-2.3,3.3-5.5,3.3-9.7
-		C-32.9,459.8-32.9,459.1-33,458.2 M70.9,421.1v20.4c-5.8-2.4-10.8-3.5-14.9-3.5c-5,0-9,1.5-12,4.4c-3,2.9-4.5,6.7-4.5,11.4
-		c0,5.3,1.5,9.4,4.5,12.5c3.1,3,7.2,4.6,12.5,4.6c4.3,0,9.3-1.4,15-4.1v20.5c-8.5,2.4-16.9,3.5-25.1,3.5c-6.8,0-13.2-1-19.1-3
-		c-4.4-1.5-8.3-3.7-11.6-6.5c-3.2-2.9-5.7-6.1-7.5-9.6c-2.5-5-3.8-10.7-3.8-17.1c0-12.1,4.5-21.5,13.4-28.3
-		c7.1-5.4,16.7-8.1,28.8-8.1C55.1,418.1,63.2,419.1,70.9,421.1 M116.1,447.5l19.3-27.4H173l-23.3,30.4l24.2,38.2h-39l-18.9-33.3
-		v33.3H82.4v-99.1h33.7V447.5"/>
-	<path id="text2985" d="M-142.2,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7
-		c1.4-1.9,2.1-4.2,2.1-7.2c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4
-		c-0.7,0.2-1.3,0.6-1.8,1c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4h-2.4V535.9 M-128.3,518.7
-		c0,1-0.1,2-0.3,2.9c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7
-		c-0.7-0.5-1.3-1.1-1.8-1.9c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6
-		c0.5-0.8,1.1-1.4,1.8-1.9c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5
-		C-128.4,516.7-128.3,517.6-128.3,518.7 M-122.7,518.7c0,1.3,0.2,2.6,0.5,3.8c0.4,1.2,0.9,2.2,1.7,3.2c0.7,0.9,1.7,1.6,2.8,2.2
-		c1.2,0.5,2.5,0.8,4,0.8c1.5,0,2.9-0.3,4-0.8c1.2-0.5,2.1-1.3,2.9-2.2c0.7-0.9,1.3-2,1.6-3.2c0.4-1.2,0.6-2.4,0.6-3.7
-		c0-1.3-0.2-2.6-0.6-3.7c-0.4-1.2-0.9-2.3-1.6-3.2c-0.7-0.9-1.7-1.6-2.9-2.2c-1.1-0.5-2.5-0.8-4-0.8c-1.5,0-2.9,0.3-4,0.8
-		c-1.1,0.5-2.1,1.2-2.9,2.2c-0.7,0.9-1.3,2-1.7,3.2C-122.5,516.1-122.7,517.3-122.7,518.7 M-120.1,518.7c0-2.3,0.6-4.2,1.7-5.6
-		c1.1-1.5,2.7-2.3,4.8-2.3c2,0,3.6,0.8,4.7,2.3c1.1,1.5,1.7,3.4,1.7,5.6c0,2.3-0.6,4.2-1.7,5.7c-1.1,1.5-2.7,2.2-4.7,2.2
-		c-2,0-3.6-0.7-4.8-2.2C-119.5,522.8-120.1,520.9-120.1,518.7 M-74,509.2h-2.4l-5.4,16.4h-0.1l-5.2-16.4h-2.9l-5.4,16.4h-0.1
-		l-5.1-16.4h-2.6l6.3,18.9h2.9l5.3-16.4h0.1l5.3,16.4h2.9L-74,509.2 M-58.3,525.1c-0.6,0.4-1.4,0.7-2.6,1c-1.1,0.3-2.1,0.4-2.9,0.4
-		c-2,0-3.6-0.7-4.7-2c-1.1-1.4-1.7-3.1-1.7-5.2h13.3v-1.2c0-1.3-0.2-2.5-0.5-3.6c-0.3-1.1-0.8-2.1-1.4-3c-0.6-0.9-1.4-1.5-2.4-2
-		c-1-0.5-2.1-0.7-3.5-0.7c-1.2,0-2.3,0.2-3.3,0.7c-1,0.5-1.9,1.1-2.6,2c-0.7,0.9-1.3,1.9-1.7,3.2c-0.4,1.2-0.6,2.5-0.6,4
-		c0,1.5,0.2,2.8,0.5,4c0.3,1.2,0.8,2.2,1.5,3.1c0.7,0.9,1.6,1.6,2.7,2.1c1.1,0.5,2.5,0.7,4.1,0.7c0.9,0,1.9-0.1,2.9-0.3
-		c1-0.2,1.9-0.4,2.7-0.7V525.1 M-70.2,517.3c0-0.8,0.1-1.6,0.4-2.3c0.3-0.8,0.7-1.5,1.1-2.2c0.5-0.6,1.1-1.1,1.8-1.5
-		c0.7-0.4,1.5-0.6,2.4-0.6c0.9,0,1.6,0.2,2.3,0.6c0.6,0.3,1.1,0.8,1.5,1.4c0.4,0.6,0.7,1.3,0.9,2.1c0.2,0.8,0.3,1.6,0.3,2.4H-70.2
-		 M-52.6,528.1h2.4v-9.3c0-0.9,0.1-1.8,0.2-2.7c0.2-0.9,0.4-1.7,0.8-2.4c0.4-0.7,0.9-1.3,1.5-1.8c0.6-0.4,1.4-0.7,2.3-0.7
-		c0.7,0,1.3,0.1,1.8,0.2V509c-0.2-0.1-0.5-0.1-0.8-0.1c-0.3,0-0.6-0.1-0.8-0.1c-1.1,0-2.1,0.4-3,1.1c-0.8,0.8-1.5,1.7-2,2.9h-0.1
-		v-3.6h-2.4c0.1,0.6,0.1,1.2,0.1,1.7c0,0.5,0,1.3,0,2.5V528.1 M-41.9,527.7c1.5,0.6,3.3,0.9,5.5,0.9c0.8,0,1.6-0.1,2.4-0.3
-		c0.8-0.2,1.5-0.5,2.2-0.9c0.7-0.4,1.2-1,1.6-1.7c0.4-0.7,0.6-1.5,0.6-2.5c0-0.8-0.2-1.6-0.5-2.2c-0.3-0.6-0.8-1.2-1.3-1.6
-		c-0.5-0.5-1.1-0.9-1.7-1.1c-0.6-0.3-1.3-0.7-2.3-1.1c-1.3-0.6-2.3-1.1-2.9-1.5c-0.6-0.4-0.9-1-0.9-1.7c0-1.1,0.4-1.9,1.1-2.4
-		c0.7-0.5,1.8-0.7,3.1-0.7c0.7,0,1.4,0.1,2.2,0.3c0.8,0.1,1.5,0.4,2.1,0.6l0.2-2c-0.8-0.3-1.6-0.5-2.6-0.6c-0.9-0.1-1.6-0.2-2.3-0.2
-		c-0.8,0-1.6,0.1-2.4,0.3c-0.7,0.2-1.4,0.5-2,0.9c-0.6,0.4-1.1,1-1.4,1.6c-0.3,0.6-0.5,1.4-0.5,2.3c0,0.7,0.1,1.3,0.4,1.8
-		c0.3,0.5,0.6,1,1.1,1.4c0.5,0.4,0.9,0.7,1.5,1c0.5,0.3,1.2,0.6,2.1,1c1.5,0.7,2.5,1.3,3.2,1.9c0.7,0.5,1.1,1.3,1.1,2.3
-		c0,1-0.4,1.9-1.3,2.5c-0.8,0.6-1.9,0.9-3.1,0.9c-1.8,0-3.4-0.4-5.1-1.2L-41.9,527.7 M11.2,509.2H8.8l-5.4,16.4H3.3l-5.2-16.4h-2.9
-		l-5.4,16.4h-0.1l-5.1-16.4H-18l6.3,18.9h2.9l5.3-16.4h0.1l5.3,16.4h2.9L11.2,509.2 M26.9,525.1c-0.6,0.4-1.4,0.7-2.6,1
-		c-1.1,0.3-2.1,0.4-2.9,0.4c-2,0-3.6-0.7-4.7-2c-1.1-1.4-1.7-3.1-1.7-5.2h13.3v-1.2c0-1.3-0.2-2.5-0.5-3.6c-0.3-1.1-0.8-2.1-1.4-3
-		c-0.6-0.9-1.4-1.5-2.4-2c-1-0.5-2.1-0.7-3.5-0.7c-1.2,0-2.3,0.2-3.3,0.7c-1,0.5-1.9,1.1-2.6,2c-0.7,0.9-1.3,1.9-1.7,3.2
-		c-0.4,1.2-0.6,2.5-0.6,4c0,1.5,0.2,2.8,0.5,4c0.3,1.2,0.8,2.2,1.5,3.1c0.7,0.9,1.6,1.6,2.7,2.1c1.1,0.5,2.5,0.7,4.1,0.7
-		c0.9,0,1.9-0.1,2.9-0.3c1-0.2,1.9-0.4,2.7-0.7V525.1 M15,517.3c0-0.8,0.1-1.6,0.4-2.3c0.3-0.8,0.7-1.5,1.1-2.2
-		c0.5-0.6,1.1-1.1,1.8-1.5c0.7-0.4,1.5-0.6,2.4-0.6c0.9,0,1.6,0.2,2.3,0.6c0.6,0.3,1.1,0.8,1.5,1.4c0.4,0.6,0.7,1.3,0.9,2.1
-		c0.2,0.8,0.3,1.6,0.3,2.4H15 M32.3,528.1h2.4v-3h0.1c1.2,2.3,3.3,3.4,6.1,3.4c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2
-		c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.3-1.3,0.6-1.8,1
-		c-0.5,0.4-0.9,0.8-1.2,1.2c-0.3,0.4-0.5,0.7-0.6,1h-0.1v-12.1h-2.4V528.1 M46.2,518.7c0,1-0.1,2-0.3,2.9c-0.2,0.9-0.5,1.8-1,2.5
-		c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9c-0.5-0.8-0.8-1.6-1-2.6
-		c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.9,0.3-2.8c0.2-0.9,0.5-1.8,1-2.5c0.5-0.8,1.1-1.4,1.8-1.9c0.8-0.5,1.7-0.7,2.7-0.7
-		c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C46.2,516.7,46.2,517.6,46.2,518.7 M74.9,528.1h2.4
-		c-0.1-0.6-0.1-1.2-0.2-1.7c0-0.5,0-1.3,0-2.3v-8.3c0-2.5-0.5-4.3-1.6-5.4c-1.1-1.1-2.8-1.7-5.2-1.7c-0.8,0-1.8,0.1-2.9,0.4
-		c-1.1,0.2-2,0.5-2.8,0.9v2.3c1.7-1,3.6-1.6,5.7-1.6c1.6,0,2.7,0.4,3.4,1.2c0.7,0.8,1,2,1,3.7v1h-0.5c-1.6,0-3.1,0.1-4.4,0.2
-		c-1.3,0.1-2.5,0.4-3.7,0.8c-1.2,0.4-2.1,1.1-2.9,2c-0.7,0.9-1.1,2.1-1.1,3.6c0,0.5,0.1,1.1,0.2,1.7c0.2,0.6,0.5,1.2,1,1.7
-		c0.5,0.5,1.1,1,2,1.4c0.9,0.4,1.9,0.6,3.2,0.6c1.3,0,2.5-0.3,3.7-0.9c1.2-0.6,2-1.5,2.5-2.6h0.1V528.1 M74.7,519.7
-		c0,0.8-0.1,1.5-0.2,2.2c-0.1,0.7-0.3,1.4-0.7,2.1c-0.4,0.7-0.9,1.3-1.8,1.8c-0.8,0.5-1.8,0.7-3.2,0.7c-1.2,0-2.2-0.3-2.9-0.8
-		c-0.7-0.6-1.1-1.4-1.1-2.6c0-1,0.3-1.8,0.8-2.4c0.5-0.6,1.2-1.1,2.1-1.4c0.9-0.3,1.8-0.5,2.9-0.6c1-0.1,2.1-0.1,3.4-0.1h0.7V519.7
-		 M82.2,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2
-		c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.2-1.3,0.6-1.8,1
-		c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4h-2.4V535.9 M96.1,518.7c0,1-0.1,2-0.3,2.9
-		c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9
-		c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6c0.5-0.8,1.1-1.4,1.8-1.9
-		c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C96,516.7,96.1,517.6,96.1,518.7
-		 M103,535.9h2.4v-11h0.1c0.6,1.2,1.4,2.2,2.4,2.8c1,0.6,2.2,0.9,3.7,0.9c2.6,0,4.5-0.9,5.9-2.7c1.4-1.9,2.1-4.2,2.1-7.2
-		c0-3-0.6-5.4-1.9-7.1c-0.7-0.9-1.5-1.6-2.5-2c-1-0.5-2.2-0.7-3.6-0.7c-0.9,0-1.7,0.1-2.4,0.4c-0.7,0.2-1.3,0.6-1.8,1
-		c-0.5,0.4-0.8,0.8-1.1,1.2c-0.3,0.4-0.5,0.8-0.8,1.2l-0.1,0.1h-0.1l0.2-3.4H103V535.9 M116.9,518.7c0,1-0.1,2-0.3,2.9
-		c-0.2,0.9-0.5,1.8-1,2.5c-0.4,0.7-1,1.3-1.8,1.8c-0.7,0.4-1.7,0.7-2.7,0.7c-1,0-1.9-0.2-2.6-0.7c-0.7-0.5-1.3-1.1-1.8-1.9
-		c-0.5-0.8-0.8-1.6-1-2.6c-0.2-0.9-0.3-1.9-0.3-2.8c0-0.9,0.1-1.8,0.3-2.7c0.2-0.9,0.6-1.8,1-2.6c0.5-0.8,1.1-1.4,1.8-1.9
-		c0.8-0.5,1.7-0.7,2.7-0.7c1.1,0,2,0.2,2.7,0.7c0.8,0.4,1.4,1.1,1.8,1.8c0.5,0.7,0.8,1.6,1,2.5C116.8,516.7,116.9,517.6,116.9,518.7
-		 M124,528.1h2.4v-27.8H124V528.1 M132.3,528.1h2.4v-18.9h-2.4V528.1 M134.7,501h-2.4v3.1h2.4V501 M153.5,509.5
-		c-0.6-0.2-1.3-0.4-2.1-0.5c-0.8-0.1-1.6-0.2-2.6-0.2c-1.4,0-2.7,0.2-3.9,0.7c-1.2,0.5-2.2,1.1-3.1,2c-0.8,0.9-1.5,1.9-1.9,3.2
-		c-0.5,1.2-0.7,2.5-0.7,4c0,1.3,0.2,2.6,0.6,3.8c0.4,1.2,1,2.2,1.8,3.2c0.8,0.9,1.8,1.6,2.9,2.2c1.2,0.5,2.5,0.8,4,0.8
-		c1,0,1.9,0,2.7-0.1c0.8-0.1,1.6-0.2,2.3-0.5l-0.2-2.2c-1.6,0.6-3,0.9-4.4,0.9c-1.1,0-2-0.2-2.9-0.6c-0.9-0.4-1.6-1-2.2-1.7
-		c-0.6-0.7-1.1-1.6-1.4-2.5c-0.3-1-0.5-2-0.5-3.1c0-2.3,0.6-4.3,1.9-5.7c1.3-1.5,3.1-2.2,5.4-2.2c0.6,0,1.3,0.1,2,0.3
-		c0.7,0.2,1.5,0.4,2.1,0.7L153.5,509.5 M168.5,528.1h2.4c-0.1-0.6-0.1-1.2-0.2-1.7c0-0.5,0-1.3,0-2.3v-8.3c0-2.5-0.5-4.3-1.6-5.4
-		c-1.1-1.1-2.8-1.7-5.2-1.7c-0.8,0-1.8,0.1-2.9,0.4c-1.1,0.2-2,0.5-2.8,0.9v2.3c1.7-1,3.6-1.6,5.7-1.6c1.6,0,2.7,0.4,3.4,1.2
-		c0.7,0.8,1,2,1,3.7v1h-0.5c-1.6,0-3.1,0.1-4.4,0.2c-1.3,0.1-2.5,0.4-3.7,0.8c-1.2,0.4-2.1,1.1-2.9,2c-0.7,0.9-1.1,2.1-1.1,3.6
-		c0,0.5,0.1,1.1,0.2,1.7c0.2,0.6,0.5,1.2,1,1.7c0.5,0.5,1.1,1,2,1.4c0.9,0.4,1.9,0.6,3.2,0.6c1.3,0,2.5-0.3,3.7-0.9
-		c1.2-0.6,2-1.5,2.5-2.6h0.1V528.1 M168.3,519.7c0,0.8-0.1,1.5-0.2,2.2c-0.1,0.7-0.3,1.4-0.7,2.1c-0.4,0.7-0.9,1.3-1.8,1.8
-		c-0.8,0.5-1.8,0.7-3.2,0.7c-1.2,0-2.2-0.3-2.9-0.8c-0.7-0.6-1.1-1.4-1.1-2.6c0-1,0.3-1.8,0.8-2.4c0.5-0.6,1.2-1.1,2.1-1.4
-		c0.9-0.3,1.8-0.5,2.9-0.6c1-0.1,2.1-0.1,3.4-0.1h0.7V519.7 M184.4,509.2H180v-5.4l-2.4,0.8v4.6h-3.8v2h3.8v11c0,1.1,0,1.9,0.1,2.6
-		c0.1,0.7,0.3,1.3,0.5,1.9c0.3,0.5,0.7,1,1.3,1.3c0.6,0.3,1.4,0.5,2.4,0.5c0.5,0,1.1-0.1,1.6-0.2c0.6-0.1,1-0.2,1.3-0.3l-0.2-1.9
-		c-0.4,0.1-0.8,0.3-1.1,0.3c-0.3,0.1-0.7,0.1-1.1,0.1c-0.9,0-1.6-0.3-2-0.9c-0.4-0.6-0.6-1.3-0.6-2.3v-12.2h4.4V509.2 M188.5,528.1
-		h2.4v-18.9h-2.4V528.1 M190.9,501h-2.4v3.1h2.4V501 M195.3,518.7c0,1.3,0.2,2.6,0.5,3.8c0.4,1.2,0.9,2.2,1.7,3.2
-		c0.7,0.9,1.7,1.6,2.8,2.2c1.2,0.5,2.5,0.8,4,0.8c1.5,0,2.9-0.3,4-0.8c1.2-0.5,2.1-1.3,2.9-2.2c0.7-0.9,1.3-2,1.6-3.2
-		c0.4-1.2,0.6-2.4,0.6-3.7s-0.2-2.6-0.6-3.7c-0.4-1.2-0.9-2.3-1.6-3.2c-0.7-0.9-1.7-1.6-2.9-2.2c-1.1-0.5-2.5-0.8-4-0.8
-		c-1.5,0-2.9,0.3-4,0.8c-1.1,0.5-2.1,1.2-2.9,2.2c-0.7,0.9-1.3,2-1.7,3.2C195.5,516.1,195.3,517.3,195.3,518.7 M198,518.7
-		c0-2.3,0.6-4.2,1.7-5.6c1.1-1.5,2.7-2.3,4.8-2.3c2,0,3.6,0.8,4.7,2.3c1.1,1.5,1.7,3.4,1.7,5.6c0,2.3-0.6,4.2-1.7,5.7
-		c-1.1,1.5-2.7,2.2-4.7,2.2c-2,0-3.6-0.7-4.8-2.2C198.5,522.8,198,520.9,198,518.7 M217.6,528.1h2.4v-10.5c0-0.8,0.1-1.7,0.3-2.4
-		c0.2-0.8,0.5-1.6,1-2.2c0.5-0.7,1.1-1.2,1.8-1.6c0.8-0.4,1.7-0.6,2.7-0.6c1.6,0,2.8,0.5,3.5,1.5c0.7,1,1.1,2.5,1.1,4.3v11.5h2.4
-		V516c0-2.2-0.5-4-1.6-5.3c-1.1-1.3-2.7-2-5-2c-0.9,0-1.8,0.1-2.5,0.3c-0.7,0.2-1.3,0.5-1.8,0.9c-0.5,0.4-0.8,0.8-1.1,1.2
-		c-0.3,0.4-0.6,0.9-0.8,1.4h-0.1v-3.4h-2.3c0.1,1,0.2,2.5,0.2,4.4V528.1 M236.8,527.7c1.5,0.6,3.3,0.9,5.5,0.9
-		c0.8,0,1.6-0.1,2.4-0.3c0.8-0.2,1.5-0.5,2.2-0.9c0.7-0.4,1.2-1,1.6-1.7c0.4-0.7,0.6-1.5,0.6-2.5c0-0.8-0.2-1.6-0.5-2.2
-		c-0.3-0.6-0.8-1.2-1.3-1.6c-0.5-0.5-1.1-0.9-1.7-1.1c-0.6-0.3-1.3-0.7-2.3-1.1c-1.3-0.6-2.3-1.1-2.9-1.5s-0.9-1-0.9-1.7
-		c0-1.1,0.4-1.9,1.1-2.4c0.7-0.5,1.8-0.7,3.1-0.7c0.7,0,1.4,0.1,2.2,0.3c0.8,0.1,1.5,0.4,2.1,0.6l0.2-2c-0.8-0.3-1.6-0.5-2.6-0.6
-		c-0.9-0.1-1.6-0.2-2.3-0.2c-0.8,0-1.6,0.1-2.4,0.3c-0.7,0.2-1.4,0.5-2,0.9c-0.6,0.4-1.1,1-1.4,1.6c-0.3,0.6-0.5,1.4-0.5,2.3
-		c0,0.7,0.1,1.3,0.4,1.8c0.3,0.5,0.6,1,1.1,1.4c0.5,0.4,0.9,0.7,1.5,1c0.5,0.3,1.2,0.6,2.1,1c1.5,0.7,2.5,1.3,3.2,1.9
-		c0.7,0.5,1.1,1.3,1.1,2.3c0,1-0.4,1.9-1.3,2.5c-0.8,0.6-1.9,0.9-3.1,0.9c-1.8,0-3.4-0.4-5.1-1.2L236.8,527.7"/>
-</g>
-<g id="Old_Logo" class="st0">
-	<g class="st1">
-		<path id="path2990_1_" class="st2" d="M-434.2,530l1.4-261.6l173.6,30.1l-1.5,258.2L-434.2,530z"/>
-		<path id="path3877_1_" class="st3" d="M-432,267.9l91.6-30.5l157.1,31.9l-80,26.3L-432,267.9z"/>
-		<path id="path3879_1_" sodipodi:nodetypes="ccccc" class="st3" d="M-259.8,556.5l75.4-26.6l1.1-260l-74.5,26.7L-259.8,556.5z"/>
-		<path id="path3881_1_" sodipodi:nodetypes="ccccc" class="st4" d="M-428.7,343.6l87.4-25l153.7,30.6l-70.8,25L-428.7,343.6z"/>
-		<path id="path3883_1_" sodipodi:nodetypes="ccccc" class="st4" d="M-429.4,448.2l87.4-25l157,25l-75.5,27.2L-429.4,448.2z"/>
-		<path id="path3887_1_" class="st5" d="M-430.7,528.4l82.4-26.4l161.3,24.5"/>
-		<path id="path3889_1_" class="st5" d="M-349.3,500l1.1-36.7"/>
-		<path id="path3891_1_" class="st5" d="M-344.3,421.7l1.5-61.6"/>
-		<path id="path3893_1_" sodipodi:nodetypes="cc" class="st5" d="M-341.9,317.1l0.3-32"/>
-	</g>
-</g>
-<g id="New_Logo">
-	<g id="layer1_1_" inkscape:groupmode="layer" inkscape:label="Layer 1">
-		<path id="path2985" inkscape:connector-curvature="0" class="st6" d="M-450.2,275v0.4v232.7v0.2l0.2,0.1l184,54l0.4,0.1v-0.4
-			l0.1-246.9v-0.2l-0.2-0.1l-184-39.9L-450.2,275z M-449.6,275.8l183.5,39.8l-0.1,246.3L-449.6,508V275.8z"/>
-		<path id="path2993" inkscape:connector-curvature="0" class="st6" d="M-172.7,278.3l-0.4,0.2l-92.8,36.6l-0.2,0.1v0.2l0.1,246.9
-			v0.4l0.4-0.2l91.9-37.2l0.2-0.1v-0.2l0.8-246.3V278.3z M-173.3,279.2l-0.8,245.7l-91.3,37l-0.1-246.2L-173.3,279.2z"/>
-		<path id="path3003" inkscape:connector-curvature="0" class="st6" d="M-344.5,237.6L-344.5,237.6l-105.3,37.5l0.2,0.6l105.1-37.4
-			l171.4,40.7l0.1-0.6L-344.5,237.6L-344.5,237.6L-344.5,237.6z"/>
-		<path id="path3015" inkscape:connector-curvature="0" class="st6" d="M-344.5,320.1L-344.5,320.1l-105.5,28.3l-1.1,0.3l1.1,0.3
-			l183.6,49.2l0.1,0l0.1,0l92.8-31.6l0.9-0.3l-0.9-0.3L-344.5,320.1L-344.5,320.1L-344.5,320.1z M-344.5,320.7l170,45.6l-91.9,31.3
-			l-182.4-48.9L-344.5,320.7z"/>
-		<path id="path3017" inkscape:connector-curvature="0" class="st6" d="M-344.8,298.5v21.9h0.6v-21.9H-344.8z"/>
-		<path id="path3019" inkscape:connector-curvature="0" class="st6" d="M-450.2,276v231.7h0.6V276H-450.2z"/>
-		<path id="path3021" inkscape:connector-curvature="0" class="st6" d="M-344.5,397.5L-344.5,397.5l-105.3,27.6l-1,0.3l1,0.3
-			l183.4,52l0.1,0l0.1,0l92.3-33.1l0.9-0.3l-0.9-0.2L-344.5,397.5L-344.5,397.5z M-344.5,398.1l169.5,46.1l-91.3,32.8l-182.2-51.7
-			L-344.5,398.1z"/>
-		<path id="path3023" inkscape:connector-curvature="0" class="st6" d="M-344.5,477.1L-344.5,477.1l-105.3,30.8l0.2,0.6l105.1-30.8
-			l170.6,47.3l0.2-0.6L-344.5,477.1L-344.5,477.1z"/>
-		<path id="path3025" inkscape:connector-curvature="0" class="st6" d="M-344.8,377.1v20.7h0.6v-20.7H-344.8z"/>
-		<path id="path3027" inkscape:connector-curvature="0" class="st6" d="M-344.8,455.3v22.1h0.6v-22.1H-344.8z"/>
-	</g>
-</g>
-</svg>
diff --git a/lib/rack.rb b/lib/rack.rb
index e4494e5bac40721ac9bd64aee047bc1c304f563b..b37c00cdee67492222d2a750c155daa39ecf4680 100644
--- a/lib/rack.rb
+++ b/lib/rack.rb
@@ -12,70 +12,9 @@
 # so it should be enough just to <tt>require 'rack'</tt> in your code.
 
 require_relative 'rack/version'
+require_relative 'rack/constants'
 
 module Rack
-  HTTP_HOST         = 'HTTP_HOST'
-  HTTP_PORT         = 'HTTP_PORT'
-  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_PORT       = 'SERVER_PORT'
-  CACHE_CONTROL     = 'Cache-Control'
-  EXPIRES           = 'Expires'
-  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'
-  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'
-  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"
   autoload :Cascade, "rack/cascade"
@@ -94,6 +33,7 @@ module Rack
   autoload :ForwardRequest, "rack/recursive"
   autoload :Handler, "rack/handler"
   autoload :Head, "rack/head"
+  autoload :Headers, "rack/headers"
   autoload :Lint, "rack/lint"
   autoload :Lock, "rack/lock"
   autoload :Logger, "rack/logger"
@@ -101,6 +41,7 @@ module Rack
   autoload :MethodOverride, "rack/method_override"
   autoload :Mime, "rack/mime"
   autoload :NullLogger, "rack/null_logger"
+  autoload :QueryParser, "rack/query_parser"
   autoload :Recursive, "rack/recursive"
   autoload :Reloader, "rack/reloader"
   autoload :RewindableInput, "rack/rewindable_input"
@@ -115,8 +56,8 @@ module Rack
   autoload :Utils, "rack/utils"
   autoload :Multipart, "rack/multipart"
 
-  autoload :MockRequest, "rack/mock"
-  autoload :MockResponse, "rack/mock"
+  autoload :MockRequest, "rack/mock_request"
+  autoload :MockResponse, "rack/mock_response"
 
   autoload :Request, "rack/request"
   autoload :Response, "rack/response"
@@ -125,17 +66,6 @@ module Rack
     autoload :Basic, "rack/auth/basic"
     autoload :AbstractRequest, "rack/auth/abstract/request"
     autoload :AbstractHandler, "rack/auth/abstract/handler"
-    module Digest
-      autoload :MD5, "rack/auth/digest/md5"
-      autoload :Nonce, "rack/auth/digest/nonce"
-      autoload :Params, "rack/auth/digest/params"
-      autoload :Request, "rack/auth/digest/request"
-    end
-  end
-
-  module Session
-    autoload :Cookie, "rack/session/cookie"
-    autoload :Pool, "rack/session/pool"
-    autoload :Memcache, "rack/session/memcache"
+    autoload :Digest, "rack/auth/digest"
   end
 end
diff --git a/lib/rack/auth/abstract/handler.rb b/lib/rack/auth/abstract/handler.rb
index 3ed87091c7e7a33f8bcaa76e523c254a66e4f9fe..4731ee8c85d7b9f3474a90a459db5ec770422241 100644
--- a/lib/rack/auth/abstract/handler.rb
+++ b/lib/rack/auth/abstract/handler.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative '../../constants'
+
 module Rack
   module Auth
     # Rack::Auth::AbstractHandler implements common authentication functionality.
@@ -21,7 +23,7 @@ module Rack
         return [ 401,
           { CONTENT_TYPE => 'text/plain',
             CONTENT_LENGTH => '0',
-            'WWW-Authenticate' => www_authenticate.to_s },
+            'www-authenticate' => www_authenticate.to_s },
           []
         ]
       end
diff --git a/lib/rack/auth/abstract/request.rb b/lib/rack/auth/abstract/request.rb
index 34042c401b7b89a38734632134bef3459185f342..f872331563ebbd8bf9527c1cd83969a3553b2160 100644
--- a/lib/rack/auth/abstract/request.rb
+++ b/lib/rack/auth/abstract/request.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative '../../request'
+
 module Rack
   module Auth
     class AbstractRequest
@@ -25,7 +27,7 @@ module Rack
       end
 
       def scheme
-        @scheme ||= parts.first && parts.first.downcase
+        @scheme ||= parts.first&.downcase
       end
 
       def params
diff --git a/lib/rack/auth/basic.rb b/lib/rack/auth/basic.rb
index d5b4ea16da3d9ceef01bdd499062b7c0d3178a2f..019efde75e9db519ffa2651196ec90f390c4b120 100644
--- a/lib/rack/auth/basic.rb
+++ b/lib/rack/auth/basic.rb
@@ -10,8 +10,6 @@ module Rack
     #
     # Initialize with the Rack application that you want protecting,
     # and a block that checks if a username and password pair are valid.
-    #
-    # See also: <tt>example/protectedlobster.rb</tt>
 
     class Basic < AbstractHandler
 
diff --git a/lib/rack/auth/digest.rb b/lib/rack/auth/digest.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9f818b9e58db9a1f30546c9cf64bc79e4a8c186
--- /dev/null
+++ b/lib/rack/auth/digest.rb
@@ -0,0 +1,256 @@
+# frozen_string_literal: true
+
+require_relative 'abstract/handler'
+require_relative 'abstract/request'
+require 'digest/md5'
+require 'base64'
+
+module Rack
+  warn "Rack::Auth::Digest is deprecated and will be removed in Rack 3.1", uplevel: 1
+
+  module Auth
+    module Digest
+      # Rack::Auth::Digest::Nonce is the default nonce generator for the
+      # Rack::Auth::Digest::MD5 authentication handler.
+      #
+      # +private_key+ needs to set to a constant string.
+      #
+      # +time_limit+ can be optionally set to an integer (number of seconds),
+      # to limit the validity of the generated nonces.
+
+      class Nonce
+
+        class << self
+          attr_accessor :private_key, :time_limit
+        end
+
+        def self.parse(string)
+          new(*Base64.decode64(string).split(' ', 2))
+        end
+
+        def initialize(timestamp = Time.now, given_digest = nil)
+          @timestamp, @given_digest = timestamp.to_i, given_digest
+        end
+
+        def to_s
+          Base64.encode64("#{@timestamp} #{digest}").strip
+        end
+
+        def digest
+          ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}")
+        end
+
+        def valid?
+          digest == @given_digest
+        end
+
+        def stale?
+          !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
+        end
+
+        def fresh?
+          !stale?
+        end
+
+      end
+
+      class Params < Hash
+
+        def self.parse(str)
+          Params[*split_header_value(str).map do |param|
+            k, v = param.split('=', 2)
+            [k, dequote(v)]
+          end.flatten]
+        end
+
+        def self.dequote(str) # From WEBrick::HTTPUtils
+          ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
+          ret.gsub!(/\\(.)/, "\\1")
+          ret
+        end
+
+        def self.split_header_value(str)
+          str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n)
+        end
+
+        def initialize
+          super()
+
+          yield self if block_given?
+        end
+
+        def [](k)
+          super k.to_s
+        end
+
+        def []=(k, v)
+          super k.to_s, v.to_s
+        end
+
+        UNQUOTED = ['nc', 'stale']
+
+        def to_s
+          map do |k, v|
+            "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}"
+          end.join(', ')
+        end
+
+        def quote(str) # From WEBrick::HTTPUtils
+          '"' + str.gsub(/[\\\"]/o, "\\\1") + '"'
+        end
+
+      end
+
+      class Request < Auth::AbstractRequest
+        def method
+          @env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD]
+        end
+
+        def digest?
+          "digest" == scheme
+        end
+
+        def correct_uri?
+          request.fullpath == uri
+        end
+
+        def nonce
+          @nonce ||= Nonce.parse(params['nonce'])
+        end
+
+        def params
+          @params ||= Params.parse(parts.last)
+        end
+
+        def respond_to?(sym, *)
+          super or params.has_key? sym.to_s
+        end
+
+        def method_missing(sym, *args)
+          return super unless params.has_key?(key = sym.to_s)
+          return params[key] if args.size == 0
+          raise ArgumentError, "wrong number of arguments (#{args.size} for 0)"
+        end
+      end
+
+      # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
+      # HTTP Digest Authentication, as per RFC 2617.
+      #
+      # Initialize with the [Rack] application that you want protecting,
+      # and a block that looks up a plaintext password for a given username.
+      #
+      # +opaque+ needs to be set to a constant base64/hexadecimal string.
+      #
+      class MD5 < AbstractHandler
+
+        attr_accessor :opaque
+
+        attr_writer :passwords_hashed
+
+        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
+          end
+          super(app, realm, &authenticator)
+          @opaque = opaque
+        end
+
+        def passwords_hashed?
+          !!@passwords_hashed
+        end
+
+        def call(env)
+          auth = Request.new(env)
+
+          unless auth.provided?
+            return unauthorized
+          end
+
+          if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
+            return bad_request
+          end
+
+          if valid?(auth)
+            if auth.nonce.stale?
+              return unauthorized(challenge(stale: true))
+            else
+              env['REMOTE_USER'] = auth.username
+
+              return @app.call(env)
+            end
+          end
+
+          unauthorized
+        end
+
+
+        private
+
+        QOP = 'auth'
+
+        def params(hash = {})
+          Params.new do |params|
+            params['realm'] = realm
+            params['nonce'] = Nonce.new.to_s
+            params['opaque'] = H(opaque)
+            params['qop'] = QOP
+
+            hash.each { |k, v| params[k] = v }
+          end
+        end
+
+        def challenge(hash = {})
+          "Digest #{params(hash)}"
+        end
+
+        def valid?(auth)
+          valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
+        end
+
+        def valid_qop?(auth)
+          QOP == auth.qop
+        end
+
+        def valid_opaque?(auth)
+          H(opaque) == auth.opaque
+        end
+
+        def valid_nonce?(auth)
+          auth.nonce.valid?
+        end
+
+        def valid_digest?(auth)
+          pw = @authenticator.call(auth.username)
+          pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response)
+        end
+
+        def md5(data)
+          ::Digest::MD5.hexdigest(data)
+        end
+
+        alias :H :md5
+
+        def KD(secret, data)
+          H "#{secret}:#{data}"
+        end
+
+        def A1(auth, password)
+          "#{auth.username}:#{auth.realm}:#{password}"
+        end
+
+        def A2(auth)
+          "#{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)}"
+        end
+
+      end
+    end
+  end
+end
+
diff --git a/lib/rack/auth/digest/md5.rb b/lib/rack/auth/digest/md5.rb
index 04b103e2583fc41157a2720b6b04ed8c1643df5f..828eccac87e90520be4781b1765fdd0a0100fda5 100644
--- a/lib/rack/auth/digest/md5.rb
+++ b/lib/rack/auth/digest/md5.rb
@@ -1,131 +1 @@
-# frozen_string_literal: true
-
-require_relative '../abstract/handler'
-require_relative 'request'
-require_relative 'params'
-require_relative 'nonce'
-require 'digest/md5'
-
-module Rack
-  module Auth
-    module Digest
-      # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
-      # HTTP Digest Authentication, as per RFC 2617.
-      #
-      # Initialize with the [Rack] application that you want protecting,
-      # and a block that looks up a plaintext password for a given username.
-      #
-      # +opaque+ needs to be set to a constant base64/hexadecimal string.
-      #
-      class MD5 < AbstractHandler
-
-        attr_accessor :opaque
-
-        attr_writer :passwords_hashed
-
-        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
-          end
-          super(app, realm, &authenticator)
-          @opaque = opaque
-        end
-
-        def passwords_hashed?
-          !!@passwords_hashed
-        end
-
-        def call(env)
-          auth = Request.new(env)
-
-          unless auth.provided?
-            return unauthorized
-          end
-
-          if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
-            return bad_request
-          end
-
-          if valid?(auth)
-            if auth.nonce.stale?
-              return unauthorized(challenge(stale: true))
-            else
-              env['REMOTE_USER'] = auth.username
-
-              return @app.call(env)
-            end
-          end
-
-          unauthorized
-        end
-
-
-        private
-
-        QOP = 'auth'
-
-        def params(hash = {})
-          Params.new do |params|
-            params['realm'] = realm
-            params['nonce'] = Nonce.new.to_s
-            params['opaque'] = H(opaque)
-            params['qop'] = QOP
-
-            hash.each { |k, v| params[k] = v }
-          end
-        end
-
-        def challenge(hash = {})
-          "Digest #{params(hash)}"
-        end
-
-        def valid?(auth)
-          valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
-        end
-
-        def valid_qop?(auth)
-          QOP == auth.qop
-        end
-
-        def valid_opaque?(auth)
-          H(opaque) == auth.opaque
-        end
-
-        def valid_nonce?(auth)
-          auth.nonce.valid?
-        end
-
-        def valid_digest?(auth)
-          pw = @authenticator.call(auth.username)
-          pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response)
-        end
-
-        def md5(data)
-          ::Digest::MD5.hexdigest(data)
-        end
-
-        alias :H :md5
-
-        def KD(secret, data)
-          H "#{secret}:#{data}"
-        end
-
-        def A1(auth, password)
-          "#{auth.username}:#{auth.realm}:#{password}"
-        end
-
-        def A2(auth)
-          "#{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)}"
-        end
-
-      end
-    end
-  end
-end
+require_relative '../digest'
diff --git a/lib/rack/auth/digest/nonce.rb b/lib/rack/auth/digest/nonce.rb
index 3216d973e0daddfe0493588fed27d35c4f46a665..828eccac87e90520be4781b1765fdd0a0100fda5 100644
--- a/lib/rack/auth/digest/nonce.rb
+++ b/lib/rack/auth/digest/nonce.rb
@@ -1,54 +1 @@
-# frozen_string_literal: true
-
-require 'digest/md5'
-require 'base64'
-
-module Rack
-  module Auth
-    module Digest
-      # Rack::Auth::Digest::Nonce is the default nonce generator for the
-      # Rack::Auth::Digest::MD5 authentication handler.
-      #
-      # +private_key+ needs to set to a constant string.
-      #
-      # +time_limit+ can be optionally set to an integer (number of seconds),
-      # to limit the validity of the generated nonces.
-
-      class Nonce
-
-        class << self
-          attr_accessor :private_key, :time_limit
-        end
-
-        def self.parse(string)
-          new(*Base64.decode64(string).split(' ', 2))
-        end
-
-        def initialize(timestamp = Time.now, given_digest = nil)
-          @timestamp, @given_digest = timestamp.to_i, given_digest
-        end
-
-        def to_s
-          Base64.encode64("#{@timestamp} #{digest}").strip
-        end
-
-        def digest
-          ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}")
-        end
-
-        def valid?
-          digest == @given_digest
-        end
-
-        def stale?
-          !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
-        end
-
-        def fresh?
-          !stale?
-        end
-
-      end
-    end
-  end
-end
+require_relative '../digest'
diff --git a/lib/rack/auth/digest/params.rb b/lib/rack/auth/digest/params.rb
index f611b3c35eb4dee2a05a1cfae8c7ceb66e5feb0d..828eccac87e90520be4781b1765fdd0a0100fda5 100644
--- a/lib/rack/auth/digest/params.rb
+++ b/lib/rack/auth/digest/params.rb
@@ -1,54 +1 @@
-# frozen_string_literal: true
-
-module Rack
-  module Auth
-    module Digest
-      class Params < Hash
-
-        def self.parse(str)
-          Params[*split_header_value(str).map do |param|
-            k, v = param.split('=', 2)
-            [k, dequote(v)]
-          end.flatten]
-        end
-
-        def self.dequote(str) # From WEBrick::HTTPUtils
-          ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
-          ret.gsub!(/\\(.)/, "\\1")
-          ret
-        end
-
-        def self.split_header_value(str)
-          str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n)
-        end
-
-        def initialize
-          super()
-
-          yield self if block_given?
-        end
-
-        def [](k)
-          super k.to_s
-        end
-
-        def []=(k, v)
-          super k.to_s, v.to_s
-        end
-
-        UNQUOTED = ['nc', 'stale']
-
-        def to_s
-          map do |k, v|
-            "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}"
-          end.join(', ')
-        end
-
-        def quote(str) # From WEBrick::HTTPUtils
-          '"' + str.gsub(/[\\\"]/o, "\\\1") + '"'
-        end
-
-      end
-    end
-  end
-end
+require_relative '../digest'
diff --git a/lib/rack/auth/digest/request.rb b/lib/rack/auth/digest/request.rb
index 7b89b7605227294788c340ac63e8a3fab7a28229..828eccac87e90520be4781b1765fdd0a0100fda5 100644
--- a/lib/rack/auth/digest/request.rb
+++ b/lib/rack/auth/digest/request.rb
@@ -1,43 +1 @@
-# frozen_string_literal: true
-
-require_relative '../abstract/request'
-require_relative 'params'
-require_relative 'nonce'
-
-module Rack
-  module Auth
-    module Digest
-      class Request < Auth::AbstractRequest
-        def method
-          @env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD]
-        end
-
-        def digest?
-          "digest" == scheme
-        end
-
-        def correct_uri?
-          request.fullpath == uri
-        end
-
-        def nonce
-          @nonce ||= Nonce.parse(params['nonce'])
-        end
-
-        def params
-          @params ||= Params.parse(parts.last)
-        end
-
-        def respond_to?(sym, *)
-          super or params.has_key? sym.to_s
-        end
-
-        def method_missing(sym, *args)
-          return super unless params.has_key?(key = sym.to_s)
-          return params[key] if args.size == 0
-          raise ArgumentError, "wrong number of arguments (#{args.size} for 0)"
-        end
-      end
-    end
-  end
-end
+require_relative '../digest'
diff --git a/lib/rack/body_proxy.rb b/lib/rack/body_proxy.rb
index cfc0796a612e0c980ab58e809cbee93509430c9b..fbb344b81029f9284628d636be35fb2c0982be81 100644
--- a/lib/rack/body_proxy.rb
+++ b/lib/rack/body_proxy.rb
@@ -24,7 +24,7 @@ module Rack
       return if @closed
       @closed = true
       begin
-        @body.close if @body.respond_to? :close
+        @body.close if @body.respond_to?(:close)
       ensure
         @block.call
       end
@@ -40,6 +40,8 @@ module Rack
     def method_missing(method_name, *args, &block)
       @body.__send__(method_name, *args, &block)
     end
+    # :nocov:
     ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
+    # :nocov:
   end
 end
diff --git a/lib/rack/builder.rb b/lib/rack/builder.rb
index 816ecf62085eff3518a0c7efd3a5122406777e4a..0b9c3d24a2d493219e79dfd9f86bd8100bb61e71 100644
--- a/lib/rack/builder.rb
+++ b/lib/rack/builder.rb
@@ -1,35 +1,35 @@
 # frozen_string_literal: true
 
+require_relative 'urlmap'
+
 module Rack
-  # Rack::Builder implements a small DSL to iteratively construct Rack
-  # applications.
+  # Rack::Builder provides a domain-specific language (DSL) to construct Rack
+  # applications. It is primarily used to parse +config.ru+ files which
+  # instantiate several middleware and a final application which are hosted
+  # by a Rack-compatible web server.
   #
   # Example:
   #
-  #  require 'rack/lobster'
-  #  app = Rack::Builder.new do
-  #    use Rack::CommonLogger
-  #    use Rack::ShowExceptions
-  #    map "/lobster" do
-  #      use Rack::Lint
-  #      run Rack::Lobster.new
-  #    end
-  #  end
+  #   app = Rack::Builder.new do
+  #     use Rack::CommonLogger
+  #     map "/ok" do
+  #       run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
+  #     end
+  #   end
   #
-  #  run app
+  #   run app
   #
   # Or
   #
-  #  app = Rack::Builder.app do
-  #    use Rack::CommonLogger
-  #    run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
-  #  end
+  #   app = Rack::Builder.app do
+  #     use Rack::CommonLogger
+  #     run lambda { |env| [200, {'content-type' => 'text/plain'}, ['OK']] }
+  #   end
   #
-  #  run app
+  #   run app
   #
   # +use+ adds middleware to the stack, +run+ dispatches to an application.
   # 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
@@ -39,13 +39,11 @@ module Rack
     #
     # If the config file ends in +.ru+, it is treated as a
     # rackup file and the contents will be treated as if
-    # specified inside a Rack::Builder block, using the given
-    # options.
+    # specified inside a Rack::Builder block.
     #
     # If the config file does not end in +.ru+, it is
     # required and Rack will use the basename of the file
     # to guess which constant will be the Rack application to run.
-    # The options given will be ignored in this case.
     #
     # Examples:
     #
@@ -61,23 +59,18 @@ module Rack
     #   # requires ./my_app.rb, which should be in the
     #   # process's current directory.  After requiring,
     #   # assumes MyApp constant contains Rack application
-    def self.parse_file(config, opts = Server::Options.new)
-      if config.end_with?('.ru')
-        return self.load_file(config, opts)
+    def self.parse_file(path)
+      if path.end_with?('.ru')
+        return self.load_file(path)
       else
-        require config
-        app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
-        return app, {}
+        require path
+        return Object.const_get(::File.basename(path, '.rb').split('_').map(&:capitalize).join(''))
       end
     end
 
     # Load the given file as a rackup file, treating the
     # contents as if specified inside a Rack::Builder block.
     #
-    # Treats the first comment at the beginning of a line
-    # that starts with a backslash as options similar to
-    # options passed on a rackup command line.
-    #
     # Ignores content in the file after +__END__+, so that
     # use of +__END__+ will not result in a syntax error.
     #
@@ -85,26 +78,20 @@ module Rack
     #
     #   $ cat config.ru
     #
-    #   #\ -p 9393
-    #
     #   use Rack::ContentLength
     #   require './app.rb'
     #   run App
-    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
+    def self.load_file(path)
+      config = ::File.read(path)
+      config.slice!(/\A#{UTF_8_BOM}/) if config.encoding == Encoding::UTF_8
 
-      if cfgfile[/^#\\(.*)/] && opts
-        warn "Parsing options from the first comment line is deprecated!"
-        options = opts.parse! $1.split(/\s+/)
+      if config[/^#\\(.*)/]
+        fail "Parsing options from the first comment line is no longer supported: #{path}"
       end
 
-      cfgfile.sub!(/^__END__\n.*\Z/m, '')
-      app = new_from_string cfgfile, path
+      config.sub!(/^__END__\n.*\Z/m, '')
 
-      return app, options
+      return new_from_string(config, path)
     end
 
     # Evaluate the given +builder_script+ string in the context of
@@ -114,14 +101,20 @@ module Rack
       # We cannot use instance_eval(String) as that would resolve constants differently.
       binding, builder = TOPLEVEL_BINDING.eval('Rack::Builder.new.instance_eval { [binding, self] }')
       eval builder_script, binding, file
-      builder.to_app
+
+      return builder.to_app
     end
 
     # Initialize a new Rack::Builder instance.  +default_app+ specifies the
     # default application if +run+ is not called later.  If a block
-    # is given, it is evaluted in the context of the instance.
+    # is given, it is evaluated in the context of the instance.
     def initialize(default_app = nil, &block)
-      @use, @map, @run, @warmup, @freeze_app = [], nil, default_app, nil, false
+      @use = []
+      @map = nil
+      @run = default_app
+      @warmup = nil
+      @freeze_app = false
+
       instance_eval(&block) if block_given?
     end
 
@@ -145,7 +138,7 @@ module Rack
     #   end
     #
     #   use Middleware
-    #   run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
+    #   run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] }
     #
     # All requests through to this application will first be processed by the middleware class.
     # The +call+ method in this example sets an additional environment key which then can be
@@ -157,24 +150,37 @@ module Rack
       end
       @use << proc { |app| middleware.new(app, *args, &block) }
     end
+    # :nocov:
     ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
+    # :nocov:
 
-    # 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:
+    # Takes a block or argument that is an object that responds to #call and
+    # returns a Rack response.
+    #
+    # You can use a block:
+    #
+    #   run do |env|
+    #     [200, { "content-type" => "text/plain" }, ["Hello World!"]]
+    #   end
+    #
+    # You can also provide a lambda:
     #
-    #   run lambda { |env| [200, { "Content-Type" => "text/plain" }, ["OK"]] }
+    #   run lambda { |env| [200, { "content-type" => "text/plain" }, ["OK"]] }
     #
-    # However this could also be a class:
+    # You can also provide a class instance:
     #
     #   class Heartbeat
-    #     def self.call(env)
-    #      [200, { "Content-Type" => "text/plain" }, ["OK"]]
+    #     def call(env)
+    #      [200, { "content-type" => "text/plain" }, ["OK"]]
     #     end
     #   end
     #
-    #   run Heartbeat
-    def run(app)
-      @run = app
+    #   run Heartbeat.new
+    #
+    def run(app = nil, &block)
+      raise ArgumentError, "Both app and block given!" if app && block_given?
+
+      @run = app || block
     end
 
     # Takes a lambda or block that is used to warm-up the application. This block is called
@@ -195,21 +201,35 @@ module Rack
     # the Rack application specified by run inside the block.  Other requests will be sent to the
     # default application specified by run outside the block.
     #
-    #   Rack::Builder.app do
+    #   class App
+    #     def call(env)
+    #       [200, {'content-type' => 'text/plain'}, ["Hello World"]]
+    #     end
+    #   end
+    #
+    #   class Heartbeat
+    #     def call(env)
+    #       [200, { "content-type" => "text/plain" }, ["OK"]]
+    #     end
+    #   end
+    #
+    #   app = Rack::Builder.app do
     #     map '/heartbeat' do
-    #       run Heartbeat
+    #       run Heartbeat.new
     #     end
-    #     run App
+    #     run App.new
     #   end
     #
+    #   run app
+    #
     # The +use+ method can also be used inside the block to specify middleware to run under a specific path:
     #
-    #   Rack::Builder.app do
+    #   app = Rack::Builder.app do
     #     map '/heartbeat' do
     #       use Middleware
-    #       run Heartbeat
+    #       run Heartbeat.new
     #     end
-    #     run App
+    #     run App.new
     #   end
     #
     # This example includes a piece of middleware which will run before +/heartbeat+ requests hit +Heartbeat+.
diff --git a/lib/rack/cascade.rb b/lib/rack/cascade.rb
index d71274c2b7cca26d65a455e1635662f543053fc9..027d7e4045fb87d8f718f74c5ec92f66a2405ebe 100644
--- a/lib/rack/cascade.rb
+++ b/lib/rack/cascade.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+
 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 configured
diff --git a/lib/rack/chunked.rb b/lib/rack/chunked.rb
index 84c6600140524118800b87918cc8fed8dcff33a0..47fb36ac1fe0e96bac7ef05eebd0951527eb1a08 100644
--- a/lib/rack/chunked.rb
+++ b/lib/rack/chunked.rb
@@ -1,22 +1,26 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+
 module Rack
+  warn "Rack::Chunked is deprecated and will be removed in Rack 3.1", uplevel: 1
 
   # Middleware that applies chunked transfer encoding to response bodies
-  # when the response does not include a Content-Length header.
+  # when the response does not include a content-length header.
   #
-  # This supports the Trailer response header to allow the use of trailing
+  # This supports the trailer response header to allow the use of trailing
   # headers in the chunked encoding.  However, using this requires you manually
   # specify a response body that supports a +trailers+ method.  Example:
   #
-  #   [200, { 'Trailer' => 'Expires'}, ["Hello", "World"]]
+  #   [200, { 'trailer' => 'expires'}, ["Hello", "World"]]
   #   # error raised
   #
   #   body = ["Hello", "World"]
   #   def body.trailers
-  #     { 'Expires' => Time.now.to_s }
+  #     { 'expires' => Time.now.to_s }
   #   end
-  #   [200, { 'Trailer' => 'Expires'}, body]
+  #   [200, { 'trailer' => 'expires'}, body]
   #   # No exception raised
   class Chunked
     include Rack::Utils
@@ -92,11 +96,10 @@ module Rack
     end
 
     # If the rack app returns a response that should have a body,
-    # but does not have Content-Length or Transfer-Encoding headers,
-    # modify the response to use chunked Transfer-Encoding.
+    # but does not have content-length or transfer-encoding headers,
+    # modify the response to use chunked transfer-encoding.
     def call(env)
-      status, headers, body = @app.call(env)
-      headers = HeaderHash[headers]
+      status, headers, body = response = @app.call(env)
 
       if chunkable_version?(env[SERVER_PROTOCOL]) &&
          !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
@@ -104,14 +107,14 @@ module Rack
          !headers[TRANSFER_ENCODING]
 
         headers[TRANSFER_ENCODING] = 'chunked'
-        if headers['Trailer']
-          body = TrailerBody.new(body)
+        if headers['trailer']
+          response[2] = TrailerBody.new(body)
         else
-          body = Body.new(body)
+          response[2] = Body.new(body)
         end
       end
 
-      [status, headers, body]
+      response
     end
   end
 end
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index 9c6f92147d946ce5af73a819722e1819a3f774a5..2feb0674649e716e50064a86bae7f663c77b391e 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -1,5 +1,10 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'body_proxy'
+require_relative 'request'
+
 module Rack
   # Rack::CommonLogger forwards every request to the given +app+, and
   # logs a line in the
@@ -35,35 +40,35 @@ module Rack
     # cause the request not to be logged.
     def call(env)
       began_at = Utils.clock_time
-      status, headers, body = @app.call(env)
-      headers = Utils::HeaderHash[headers]
-      body = BodyProxy.new(body) { log(env, status, headers, began_at) }
-      [status, headers, body]
+      status, headers, body = response = @app.call(env)
+
+      response[2] = BodyProxy.new(body) { log(env, status, headers, began_at) }
+      response
     end
 
     private
 
     # Log the request to the configured logger.
-    def log(env, status, header, began_at)
-      length = extract_content_length(header)
+    def log(env, status, response_headers, began_at)
+      request = Rack::Request.new(env)
+      length = extract_content_length(response_headers)
 
-      msg = FORMAT % [
-        env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
-        env["REMOTE_USER"] || "-",
+      msg = sprintf(FORMAT,
+        request.ip || "-",
+        request.get_header("REMOTE_USER") || "-",
         Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
-        env[REQUEST_METHOD],
-        env[SCRIPT_NAME],
-        env[PATH_INFO],
-        env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
-        env[SERVER_PROTOCOL],
+        request.request_method,
+        request.script_name,
+        request.path_info,
+        request.query_string.empty? ? "" : "?#{request.query_string}",
+        request.get_header(SERVER_PROTOCOL),
         status.to_s[0..3],
         length,
-        Utils.clock_time - began_at ]
-
-      msg.gsub!(/[^[:print:]\n]/) { |c| "\\x#{c.ord}" }
+        Utils.clock_time - began_at)
 
-      logger = @logger || env[RACK_ERRORS]
+      msg.gsub!(/[^[:print:]\n]/) { |c| sprintf("\\x%x", c.ord) }
 
+      logger = @logger || request.get_header(RACK_ERRORS)
       # Standard library logger doesn't support write but it supports << which actually
       # calls to write on the log device without formatting
       if logger.respond_to?(:write)
diff --git a/lib/rack/conditional_get.rb b/lib/rack/conditional_get.rb
index 7b7808ac1f77095264c90650d8878de1d736e9c0..c3b334a2e2b4d227fffb29a410c6a30efd3e046d 100644
--- a/lib/rack/conditional_get.rb
+++ b/lib/rack/conditional_get.rb
@@ -1,10 +1,14 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'body_proxy'
+
 module Rack
 
-  # Middleware that enables conditional GET using If-None-Match and
-  # If-Modified-Since. The application should set either or both of the
-  # Last-Modified or Etag response headers according to RFC 2616. When
+  # Middleware that enables conditional GET using if-none-match and
+  # if-modified-since. The application should set either or both of the
+  # last-modified or etag response headers according to RFC 2616. When
   # either of the conditions is met, the response body is set to be zero
   # length and the response status is set to 304 Not Modified.
   #
@@ -24,18 +28,17 @@ module Rack
     def call(env)
       case env[REQUEST_METHOD]
       when "GET", "HEAD"
-        status, headers, body = @app.call(env)
-        headers = Utils::HeaderHash[headers]
+        status, headers, body = response = @app.call(env)
+
         if status == 200 && fresh?(env, headers)
-          status = 304
+          response[0] = 304
           headers.delete(CONTENT_TYPE)
           headers.delete(CONTENT_LENGTH)
-          original_body = body
-          body = Rack::BodyProxy.new([]) do
-            original_body.close if original_body.respond_to?(:close)
+          response[2] = Rack::BodyProxy.new([]) do
+            body.close if body.respond_to?(:close)
           end
         end
-        [status, headers, body]
+        response
       else
         @app.call(env)
       end
@@ -46,7 +49,7 @@ module Rack
     # Return whether the response has not been modified since the
     # last request.
     def fresh?(env, headers)
-      # If-None-Match has priority over If-Modified-Since per RFC 7232
+      # if-none-match has priority over if-modified-since per RFC 7232
       if none_match = env['HTTP_IF_NONE_MATCH']
         etag_matches?(none_match, headers)
       elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
@@ -54,16 +57,16 @@ module Rack
       end
     end
 
-    # Whether the ETag response header matches the If-None-Match request header.
+    # Whether the etag response header matches the if-none-match request header.
     # If so, the request has not been modified.
     def etag_matches?(none_match, headers)
-      headers['ETag'] == none_match
+      headers[ETAG] == none_match
     end
 
-    # Whether the Last-Modified response header matches the If-Modified-Since
+    # Whether the last-modified response header matches the if-modified-since
     # request header.  If so, the request has not been modified.
     def modified_since?(modified_since, headers)
-      last_modified = to_rfc2822(headers['Last-Modified']) and
+      last_modified = to_rfc2822(headers['last-modified']) and
         modified_since >= last_modified
     end
 
diff --git a/lib/rack/constants.rb b/lib/rack/constants.rb
new file mode 100644
index 0000000000000000000000000000000000000000..13365935b1b5b8d679598823f776f1b9ee5a4a89
--- /dev/null
+++ b/lib/rack/constants.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Rack
+  # Request env keys
+  HTTP_HOST         = 'HTTP_HOST'
+  HTTP_PORT         = 'HTTP_PORT'
+  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_PORT       = 'SERVER_PORT'
+  HTTP_COOKIE       = 'HTTP_COOKIE'
+
+  # Response Header Keys
+  CACHE_CONTROL     = 'cache-control'
+  CONTENT_LENGTH    = 'content-length'
+  CONTENT_TYPE      = 'content-type'
+  ETAG              = 'etag'
+  EXPIRES           = 'expires'
+  SET_COOKIE        = 'set-cookie'
+  TRANSFER_ENCODING = 'transfer-encoding'
+
+  # HTTP method verbs
+  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'
+  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_URL_SCHEME                     = 'rack.url_scheme'
+  RACK_HIJACK                         = 'rack.hijack'
+  RACK_IS_HIJACK                      = 'rack.hijack?'
+  RACK_RECURSIVE_INCLUDE              = 'rack.recursive.include'
+  RACK_MULTIPART_BUFFER_SIZE          = 'rack.multipart.buffer_size'
+  RACK_MULTIPART_TEMPFILE_FACTORY     = 'rack.multipart.tempfile_factory'
+  RACK_RESPONSE_FINISHED              = 'rack.response_finished'
+  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_FORM_ERROR             = 'rack.request.form_error'
+  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'
+end
diff --git a/lib/rack/content_length.rb b/lib/rack/content_length.rb
index 9e2b5fc42a1c15c4bf36e6d741ff1a98ed45b88c..cbac93abcf37ec442babf308794206d2ff1524e7 100644
--- a/lib/rack/content_length.rb
+++ b/lib/rack/content_length.rb
@@ -1,10 +1,13 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+
 module Rack
 
-  # Sets the Content-Length header on responses that do not specify
-  # a Content-Length or Transfer-Encoding header.  Note that this
-  # does not fix responses that have an invalid Content-Length
+  # Sets the content-length header on responses that do not specify
+  # a content-length or transfer-encoding header.  Note that this
+  # does not fix responses that have an invalid content-length
   # header specified.
   class ContentLength
     include Rack::Utils
@@ -14,25 +17,18 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
-      headers = HeaderHash[headers]
+      status, headers, body = response = @app.call(env)
 
       if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
          !headers[CONTENT_LENGTH] &&
-         !headers[TRANSFER_ENCODING]
-
-        obody = body
-        body, length = [], 0
-        obody.each { |part| body << part; length += part.bytesize }
-
-        body = BodyProxy.new(body) do
-          obody.close if obody.respond_to?(:close)
-        end
+         !headers[TRANSFER_ENCODING] &&
+         body.respond_to?(:to_ary)
 
-        headers[CONTENT_LENGTH] = length.to_s
+        response[2] = body = body.to_ary
+        headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s
       end
 
-      [status, headers, body]
+      response
     end
   end
 end
diff --git a/lib/rack/content_type.rb b/lib/rack/content_type.rb
index 503f7070621e7e86ae9d31914f5931e364d0a3a3..19f07824f563bc197c6f92a695d50f29f687ee55 100644
--- a/lib/rack/content_type.rb
+++ b/lib/rack/content_type.rb
@@ -1,8 +1,11 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+
 module Rack
 
-  # Sets the Content-Type header on responses which don't have one.
+  # Sets the content-type header on responses which don't have one.
   #
   # Builder Usage:
   #   use Rack::ContentType, "text/plain"
@@ -13,18 +16,18 @@ module Rack
     include Rack::Utils
 
     def initialize(app, content_type = "text/html")
-      @app, @content_type = app, content_type
+      @app = app
+      @content_type = content_type
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
-      headers = Utils::HeaderHash[headers]
+      status, headers, _ = response = @app.call(env)
 
       unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
         headers[CONTENT_TYPE] ||= @content_type
       end
 
-      [status, headers, body]
+      response
     end
   end
 end
diff --git a/lib/rack/core_ext/regexp.rb b/lib/rack/core_ext/regexp.rb
deleted file mode 100644
index a32fcdf629a3fff24c5d680ed26ae23ac2ae29d2..0000000000000000000000000000000000000000
--- a/lib/rack/core_ext/regexp.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# 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 e177fabb017c966f1a0fda7234db7956e56da39c..cc01c32a0ad572ed795ac43cf954e7a79fcd5868 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -3,6 +3,11 @@
 require "zlib"
 require "time"  # for Time.httpdate
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'request'
+require_relative 'body_proxy'
+
 module Rack
   # This middleware enables content encoding of http responses,
   # usually for purposes of compression.
@@ -21,8 +26,6 @@ module Rack
   # Note that despite the name, Deflater does not support the +deflate+
   # encoding.
   class Deflater
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
     # Creates Rack::Deflater middleware. Options:
     #
     # :if :: a lambda enabling / disabling deflation based on returned boolean value
@@ -41,11 +44,10 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
-      headers = Utils::HeaderHash[headers]
+      status, headers, body = response = @app.call(env)
 
       unless should_deflate?(env, status, headers, body)
-        return [status, headers, body]
+        return response
       end
 
       request = Request.new(env)
@@ -54,21 +56,23 @@ module Rack
                                             request.accept_encoding)
 
       # Set the Vary HTTP header.
-      vary = headers["Vary"].to_s.split(",").map(&:strip)
-      unless vary.include?("*") || vary.include?("Accept-Encoding")
-        headers["Vary"] = vary.push("Accept-Encoding").join(",")
+      vary = headers["vary"].to_s.split(",").map(&:strip)
+      unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
+        headers["vary"] = vary.push("Accept-Encoding").join(",")
       end
 
       case encoding
       when "gzip"
-        headers['Content-Encoding'] = "gzip"
+        headers['content-encoding'] = "gzip"
         headers.delete(CONTENT_LENGTH)
-        mtime = headers["Last-Modified"]
+        mtime = headers["last-modified"]
         mtime = Time.httpdate(mtime).to_i if mtime
-        [status, headers, GzipStream.new(body, mtime, @sync)]
+        response[2] = GzipStream.new(body, mtime, @sync)
+        response
       when "identity"
-        [status, headers, body]
-      when nil
+        response
+      else # when nil
+        # Only possible encoding values here are 'gzip', 'identity', and 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]
@@ -77,6 +81,9 @@ module Rack
 
     # Body class used for gzip encoded responses.
     class GzipStream
+
+      BUFFER_LENGTH = 128 * 1_024
+
       # Initialize the gzip stream.  Arguments:
       # body :: Response body to compress with gzip
       # mtime :: The modification time of the body, used to set the
@@ -93,19 +100,26 @@ module Rack
         @writer = block
         gzip = ::Zlib::GzipWriter.new(self)
         gzip.mtime = @mtime if @mtime
-        @body.each { |part|
-          # Skip empty strings, as they would result in no output,
-          # and flushing empty parts would raise Zlib::BufError.
-          next if part.empty?
-
-          gzip.write(part)
-          gzip.flush if @sync
-        }
+        # @body.each is equivalent to @body.gets (slow)
+        if @body.is_a? ::File # XXX: Should probably be ::IO
+          while part = @body.read(BUFFER_LENGTH)
+            gzip.write(part)
+            gzip.flush if @sync
+          end
+        else
+          @body.each { |part|
+            # Skip empty strings, as they would result in no output,
+            # and flushing empty parts would raise Zlib::BufError.
+            next if part.empty?
+            gzip.write(part)
+            gzip.flush if @sync
+          }
+        end
       ensure
-        gzip.close
+        gzip.finish
       end
 
-      # Call the block passed to #each with the the gzipped data.
+      # Call the block passed to #each with the gzipped data.
       def write(data)
         @writer.call(data)
       end
@@ -123,13 +137,13 @@ module Rack
       # Skip compressing empty entity body responses and responses with
       # no-transform set.
       if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
-          /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
-          headers['Content-Encoding']&.!~(/\bidentity\b/)
+          /\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) ||
+          headers['content-encoding']&.!~(/\bidentity\b/)
         return false
       end
 
       # Skip if @compressible_types are given and does not include request's content type
-      return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
+      return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
 
       # Skip if @condition lambda is given and evaluates to false
       return false if @condition && !@condition.call(env, status, headers, body)
diff --git a/lib/rack/directory.rb b/lib/rack/directory.rb
index be72be0144405996c18993a750f9de822a8a28ba..089623f91d934ec0a958df901745f6c5c726697d 100644
--- a/lib/rack/directory.rb
+++ b/lib/rack/directory.rb
@@ -2,6 +2,12 @@
 
 require 'time'
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'head'
+require_relative 'mime'
+require_relative 'files'
+
 module Rack
   # Rack::Directory serves entries below the +root+ given, according to the
   # path info of the Rack request. If a directory is found, the file's contents
@@ -106,7 +112,7 @@ table { width:100%%; }
       body = "Bad Request\n"
       [400, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => body.bytesize.to_s,
-        "X-Cascade" => "pass" }, [body]]
+        "x-cascade" => "pass" }, [body]]
     end
 
     # Rack response to use for requests with paths outside the root, or nil if path is inside the root.
@@ -117,7 +123,7 @@ table { width:100%%; }
       body = "Forbidden\n"
       [403, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => body.bytesize.to_s,
-        "X-Cascade" => "pass" }, [body]]
+        "x-cascade" => "pass" }, [body]]
     end
 
     # Rack response to use for directories under the root.
@@ -176,7 +182,7 @@ table { width:100%%; }
       body = "Entity not found: #{path_info}\n"
       [404, { CONTENT_TYPE => "text/plain",
         CONTENT_LENGTH => body.bytesize.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 5039437e1c13e6ad458d071fb4d3c438247589ad..fa78b472fd91469e82274c83bc8ec611318b837c 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -1,17 +1,19 @@
 # frozen_string_literal: true
 
-require_relative '../rack'
 require 'digest/sha2'
 
+require_relative 'constants'
+require_relative 'utils'
+
 module Rack
-  # Automatically sets the ETag header on all String bodies.
+  # Automatically sets the etag header on all String bodies.
   #
-  # The ETag header is skipped if ETag or Last-Modified headers are sent or if
+  # The etag header is skipped if etag or last-modified headers are sent or if
   # a sendfile body (body.responds_to :to_path) is given (since such cases
   # should be handled by apache/nginx).
   #
-  # On initialization, you can pass two parameters: a Cache-Control directive
-  # used when Etag is absent and a directive when it is present. The first
+  # On initialization, you can pass two parameters: a cache-control directive
+  # used when etag is absent and a directive when it is present. The first
   # defaults to nil, while the second defaults to "max-age=0, private, must-revalidate"
   class ETag
     ETAG_STRING = Rack::ETAG
@@ -24,16 +26,11 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
-
-      headers = Utils::HeaderHash[headers]
+      status, headers, body = response = @app.call(env)
 
-      if etag_status?(status) && etag_body?(body) && !skip_caching?(headers)
-        original_body = body
-        digest, new_body = digest_body(body)
-        body = Rack::BodyProxy.new(new_body) do
-          original_body.close if original_body.respond_to?(:close)
-        end
+      if etag_status?(status) && body.respond_to?(:to_ary) && !skip_caching?(headers)
+        body = body.to_ary
+        digest = digest_body(body)
         headers[ETAG_STRING] = %(W/"#{digest}") if digest
       end
 
@@ -45,7 +42,7 @@ module Rack
         end
       end
 
-      [status, headers, body]
+      response
     end
 
     private
@@ -54,24 +51,18 @@ module Rack
         status == 200 || status == 201
       end
 
-      def etag_body?(body)
-        !body.respond_to?(:to_path)
-      end
-
       def skip_caching?(headers)
-        headers.key?(ETAG_STRING) || headers.key?('Last-Modified')
+        headers.key?(ETAG_STRING) || headers.key?('last-modified')
       end
 
       def digest_body(body)
-        parts = []
         digest = nil
 
         body.each do |part|
-          parts << part
           (digest ||= Digest::SHA256.new) << part unless part.empty?
         end
 
-        [digest && digest.hexdigest.byteslice(0, 32), parts]
+        digest && digest.hexdigest.byteslice(0,32)
       end
   end
 end
diff --git a/lib/rack/events.rb b/lib/rack/events.rb
index 65055fdc51e6d8e8a77d66d013cceb414f06a334..c7bb201f05aee7924b7cc583a0476cf2c15004b2 100644
--- a/lib/rack/events.rb
+++ b/lib/rack/events.rb
@@ -1,5 +1,9 @@
 # frozen_string_literal: true
 
+require_relative 'body_proxy'
+require_relative 'request'
+require_relative 'response'
+
 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
diff --git a/lib/rack/file.rb b/lib/rack/file.rb
index fdcf9b3ec064550319844363e57d0031061ed112..52c7b4166793f24a41922a1e0cf41e9068e039a9 100644
--- a/lib/rack/file.rb
+++ b/lib/rack/file.rb
@@ -3,5 +3,7 @@
 require_relative 'files'
 
 module Rack
+  warn "Rack::File is deprecated and will be removed in Rack 3.1", uplevel: 1
+
   File = Files
 end
diff --git a/lib/rack/files.rb b/lib/rack/files.rb
index e745eb3984373dde5dded309376f722b5f697cdf..5b8353f5b5257a240f70ecafe2f57b099236dfd1 100644
--- a/lib/rack/files.rb
+++ b/lib/rack/files.rb
@@ -2,6 +2,12 @@
 
 require 'time'
 
+require_relative 'constants'
+require_relative 'head'
+require_relative 'utils'
+require_relative 'request'
+require_relative 'mime'
+
 module Rack
   # Rack::Files serves files below the +root+ directory given, according to the
   # path info of the Rack request.
@@ -16,14 +22,6 @@ module Rack
     ALLOW_HEADER = ALLOWED_VERBS.join(', ')
     MULTIPART_BOUNDARY = 'AaB03x'
 
-    # @todo remove in 3.0
-    def self.method_added(name)
-      if name == :response_body
-        raise "#{self.class}\#response_body is no longer supported."
-      end
-      super
-    end
-
     attr_reader :root
 
     def initialize(root, headers = {}, default_mime = 'text/plain')
@@ -41,7 +39,7 @@ module Rack
     def get(env)
       request = Rack::Request.new env
       unless ALLOWED_VERBS.include? request.request_method
-        return fail(405, "Method Not Allowed", { 'Allow' => ALLOW_HEADER })
+        return fail(405, "Method Not Allowed", { 'allow' => ALLOW_HEADER })
       end
 
       path_info = Utils.unescape_path request.path_info
@@ -69,12 +67,12 @@ module Rack
 
     def serving(request, path)
       if request.options?
-        return [200, { 'Allow' => ALLOW_HEADER, CONTENT_LENGTH => '0' }, []]
+        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 }
+      headers = { "last-modified" => last_modified }
       mime_type = mime_type path, @default_mime
       headers[CONTENT_TYPE] = mime_type if mime_type
 
@@ -91,15 +89,15 @@ module Rack
       elsif ranges.empty?
         # Unsatisfiable. Return error, and file size:
         response = fail(416, "Byte range unsatisfiable")
-        response[1]["Content-Range"] = "bytes */#{size}"
+        response[1]["content-range"] = "bytes */#{size}"
         return response
-      elsif ranges.size >= 1
+      else
         # Partial content
         partial_content = true
 
         if ranges.size == 1
           range = ranges[0]
-          headers["Content-Range"] = "bytes #{range.begin}-#{range.end}/#{size}"
+          headers["content-range"] = "bytes #{range.begin}-#{range.end}/#{size}"
         else
           headers[CONTENT_TYPE] = "multipart/byteranges; boundary=#{MULTIPART_BOUNDARY}"
         end
@@ -164,8 +162,8 @@ module Rack
 <<-EOF
 \r
 --#{MULTIPART_BOUNDARY}\r
-Content-Type: #{options[:mime_type]}\r
-Content-Range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
+content-type: #{options[:mime_type]}\r
+content-range: bytes #{range.begin}-#{range.end}/#{options[:size]}\r
 \r
 EOF
       end
@@ -197,7 +195,7 @@ EOF
         {
           CONTENT_TYPE   => "text/plain",
           CONTENT_LENGTH => body.size.to_s,
-          "X-Cascade" => "pass"
+          "x-cascade" => "pass"
         }.merge!(headers),
         [body]
       ]
diff --git a/lib/rack/handler.rb b/lib/rack/handler.rb
deleted file mode 100644
index df17b238ddb73d599fce01a934149b539e4e695f..0000000000000000000000000000000000000000
--- a/lib/rack/handler.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  # *Handlers* connect web servers with Rack.
-  #
-  # Rack includes Handlers for Thin, WEBrick, FastCGI, CGI, SCGI
-  # and LiteSpeed.
-  #
-  # Handlers usually are activated by calling <tt>MyHandler.run(myapp)</tt>.
-  # A second optional hash can be passed to include server-specific
-  # configuration.
-  module Handler
-    def self.get(server)
-      return unless server
-      server = server.to_s
-
-      unless @handlers.include? server
-        load_error = try_require('rack/handler', server)
-      end
-
-      if klass = @handlers[server]
-        const_get(klass)
-      else
-        const_get(server, false)
-      end
-
-    rescue NameError => name_error
-      raise load_error || name_error
-    end
-
-    # Select first available Rack handler given an `Array` of server names.
-    # Raises `LoadError` if no handler was found.
-    #
-    #   > pick ['thin', 'webrick']
-    #   => Rack::Handler::WEBrick
-    def self.pick(server_names)
-      server_names = Array(server_names)
-      server_names.each do |server_name|
-        begin
-          return get(server_name.to_s)
-        rescue LoadError, NameError
-        end
-      end
-
-      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")
-        Rack::Handler::FastCGI
-      elsif ENV.include?(REQUEST_METHOD)
-        Rack::Handler::CGI
-      elsif ENV.include?("RACK_HANDLER")
-        self.get(ENV["RACK_HANDLER"])
-      else
-        pick SERVER_NAMES
-      end
-    end
-
-    # Transforms server-name constants to their canonical form as filenames,
-    # then tries to require them but silences the LoadError if not found
-    #
-    # Naming convention:
-    #
-    #   Foo # => 'foo'
-    #   FooBar # => 'foo_bar.rb'
-    #   FooBAR # => 'foobar.rb'
-    #   FOObar # => 'foobar.rb'
-    #   FOOBAR # => 'foobar.rb'
-    #   FooBarBaz # => 'foo_bar_baz.rb'
-    def self.try_require(prefix, const_name)
-      file = const_name.gsub(/^[A-Z]+/) { |pre| pre.downcase }.
-        gsub(/[A-Z]+[^A-Z]/, '_\&').downcase
-
-      require(::File.join(prefix, file))
-      nil
-    rescue LoadError => error
-      error
-    end
-
-    def self.register(server, klass)
-      @handlers ||= {}
-      @handlers[server.to_s] = klass.to_s
-    end
-
-    autoload :CGI, "rack/handler/cgi"
-    autoload :FastCGI, "rack/handler/fastcgi"
-    autoload :WEBrick, "rack/handler/webrick"
-    autoload :LSWS, "rack/handler/lsws"
-    autoload :SCGI, "rack/handler/scgi"
-    autoload :Thin, "rack/handler/thin"
-
-    register 'cgi', 'Rack::Handler::CGI'
-    register 'fastcgi', 'Rack::Handler::FastCGI'
-    register 'webrick', 'Rack::Handler::WEBrick'
-    register 'lsws', 'Rack::Handler::LSWS'
-    register 'scgi', 'Rack::Handler::SCGI'
-    register 'thin', 'Rack::Handler::Thin'
-  end
-end
diff --git a/lib/rack/handler/cgi.rb b/lib/rack/handler/cgi.rb
deleted file mode 100644
index 1c11ab360622c22068bf353d2b80d7a288344833..0000000000000000000000000000000000000000
--- a/lib/rack/handler/cgi.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  module Handler
-    class CGI
-      def self.run(app, **options)
-        $stdin.binmode
-        serve app
-      end
-
-      def self.serve(app)
-        env = ENV.to_hash
-        env.delete "HTTP_CONTENT_LENGTH"
-
-        env[SCRIPT_NAME] = ""  if env[SCRIPT_NAME] == "/"
-
-        env.update(
-          RACK_VERSION      => Rack::VERSION,
-          RACK_INPUT        => Rack::RewindableInput.new($stdin),
-          RACK_ERRORS       => $stderr,
-          RACK_MULTITHREAD  => false,
-          RACK_MULTIPROCESS => true,
-          RACK_RUNONCE      => true,
-          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(ENV[HTTPS]) ? "https" : "http"
-        )
-
-        env[QUERY_STRING] ||= ""
-        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
-        env[REQUEST_PATH] ||= "/"
-
-        status, headers, body = app.call(env)
-        begin
-          send_headers status, headers
-          send_body body
-        ensure
-          body.close  if body.respond_to? :close
-        end
-      end
-
-      def self.send_headers(status, headers)
-        $stdout.print "Status: #{status}\r\n"
-        headers.each { |k, vs|
-          vs.split("\n").each { |v|
-            $stdout.print "#{k}: #{v}\r\n"
-          }
-        }
-        $stdout.print "\r\n"
-        $stdout.flush
-      end
-
-      def self.send_body(body)
-        body.each { |part|
-          $stdout.print part
-          $stdout.flush
-        }
-      end
-    end
-  end
-end
diff --git a/lib/rack/handler/fastcgi.rb b/lib/rack/handler/fastcgi.rb
deleted file mode 100644
index 1df123e02a65a73c2a365215082ffe1787562122..0000000000000000000000000000000000000000
--- a/lib/rack/handler/fastcgi.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-require 'fcgi'
-require 'socket'
-
-if defined? FCGI::Stream
-  class FCGI::Stream
-    alias _rack_read_without_buffer read
-
-    def read(n, buffer = nil)
-      buf = _rack_read_without_buffer n
-      buffer.replace(buf.to_s)  if buffer
-      buf
-    end
-  end
-end
-
-module Rack
-  module Handler
-    class FastCGI
-      def self.run(app, **options)
-        if options[:File]
-          STDIN.reopen(UNIXServer.new(options[:File]))
-        elsif options[:Port]
-          STDIN.reopen(TCPServer.new(options[:Host], options[:Port]))
-        end
-        FCGI.each { |request|
-          serve request, app
-        }
-      end
-
-      def self.valid_options
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-        {
-          "Host=HOST" => "Hostname to listen on (default: #{default_host})",
-          "Port=PORT" => "Port to listen on (default: 8080)",
-          "File=PATH" => "Creates a Domain socket at PATH instead of a TCP socket. Ignores Host and Port if set.",
-        }
-      end
-
-      def self.serve(request, app)
-        env = request.env
-        env.delete "HTTP_CONTENT_LENGTH"
-
-        env[SCRIPT_NAME] = ""  if env[SCRIPT_NAME] == "/"
-
-        rack_input = RewindableInput.new(request.in)
-
-        env.update(
-          RACK_VERSION      => Rack::VERSION,
-          RACK_INPUT        => rack_input,
-          RACK_ERRORS       => request.err,
-          RACK_MULTITHREAD  => false,
-          RACK_MULTIPROCESS => true,
-          RACK_RUNONCE      => false,
-          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http"
-        )
-
-        env[QUERY_STRING] ||= ""
-        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
-        env[REQUEST_PATH] ||= "/"
-        env.delete "CONTENT_TYPE"  if env["CONTENT_TYPE"] == ""
-        env.delete "CONTENT_LENGTH"  if env["CONTENT_LENGTH"] == ""
-
-        begin
-          status, headers, body = app.call(env)
-          begin
-            send_headers request.out, status, headers
-            send_body request.out, body
-          ensure
-            body.close  if body.respond_to? :close
-          end
-        ensure
-          rack_input.close
-          request.finish
-        end
-      end
-
-      def self.send_headers(out, status, headers)
-        out.print "Status: #{status}\r\n"
-        headers.each { |k, vs|
-          vs.split("\n").each { |v|
-            out.print "#{k}: #{v}\r\n"
-          }
-        }
-        out.print "\r\n"
-        out.flush
-      end
-
-      def self.send_body(out, body)
-        body.each { |part|
-          out.print part
-          out.flush
-        }
-      end
-    end
-  end
-end
diff --git a/lib/rack/handler/lsws.rb b/lib/rack/handler/lsws.rb
deleted file mode 100644
index f12090bd62df9968784d1f769f6aaf8ae50eef0b..0000000000000000000000000000000000000000
--- a/lib/rack/handler/lsws.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-# frozen_string_literal: true
-
-require 'lsapi'
-
-module Rack
-  module Handler
-    class LSWS
-      def self.run(app, **options)
-        while LSAPI.accept != nil
-          serve app
-        end
-      end
-      def self.serve(app)
-        env = ENV.to_hash
-        env.delete "HTTP_CONTENT_LENGTH"
-        env[SCRIPT_NAME] = "" if env[SCRIPT_NAME] == "/"
-
-        rack_input = RewindableInput.new($stdin.read.to_s)
-
-        env.update(
-          RACK_VERSION      => Rack::VERSION,
-          RACK_INPUT        => rack_input,
-          RACK_ERRORS       => $stderr,
-          RACK_MULTITHREAD  => false,
-          RACK_MULTIPROCESS => true,
-          RACK_RUNONCE      => false,
-          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(ENV[HTTPS]) ? "https" : "http"
-        )
-
-        env[QUERY_STRING] ||= ""
-        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
-        env[REQUEST_PATH] ||= "/"
-        status, headers, body = app.call(env)
-        begin
-          send_headers status, headers
-          send_body body
-        ensure
-          body.close if body.respond_to? :close
-        end
-      ensure
-        rack_input.close
-      end
-      def self.send_headers(status, headers)
-        print "Status: #{status}\r\n"
-        headers.each { |k, vs|
-          vs.split("\n").each { |v|
-            print "#{k}: #{v}\r\n"
-          }
-        }
-        print "\r\n"
-        STDOUT.flush
-      end
-      def self.send_body(body)
-        body.each { |part|
-          print part
-          STDOUT.flush
-        }
-      end
-    end
-  end
-end
diff --git a/lib/rack/handler/scgi.rb b/lib/rack/handler/scgi.rb
deleted file mode 100644
index e3b8d3c6f240a9c6b426b28b84eabefc8a36c9e3..0000000000000000000000000000000000000000
--- a/lib/rack/handler/scgi.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'scgi'
-require 'stringio'
-
-module Rack
-  module Handler
-    class SCGI < ::SCGI::Processor
-      attr_accessor :app
-
-      def self.run(app, **options)
-        options[:Socket] = UNIXServer.new(options[:File]) if options[:File]
-        new(options.merge(app: app,
-                          host: options[:Host],
-                          port: options[:Port],
-                          socket: options[:Socket])).listen
-      end
-
-      def self.valid_options
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-        {
-          "Host=HOST" => "Hostname to listen on (default: #{default_host})",
-          "Port=PORT" => "Port to listen on (default: 8080)",
-        }
-      end
-
-      def initialize(settings = {})
-        @app = settings[:app]
-        super(settings)
-      end
-
-      def process_request(request, input_body, socket)
-        env = Hash[request]
-        env.delete "HTTP_CONTENT_TYPE"
-        env.delete "HTTP_CONTENT_LENGTH"
-        env[REQUEST_PATH], env[QUERY_STRING] = env["REQUEST_URI"].split('?', 2)
-        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
-        env[PATH_INFO] = env[REQUEST_PATH]
-        env[QUERY_STRING] ||= ""
-        env[SCRIPT_NAME] = ""
-
-        rack_input = StringIO.new(input_body)
-        rack_input.set_encoding(Encoding::BINARY)
-
-        env.update(
-          RACK_VERSION      => Rack::VERSION,
-          RACK_INPUT        => rack_input,
-          RACK_ERRORS       => $stderr,
-          RACK_MULTITHREAD  => true,
-          RACK_MULTIPROCESS => true,
-          RACK_RUNONCE      => false,
-          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http"
-        )
-
-        status, headers, body = app.call(env)
-        begin
-          socket.write("Status: #{status}\r\n")
-          headers.each do |k, vs|
-            vs.split("\n").each { |v| socket.write("#{k}: #{v}\r\n")}
-          end
-          socket.write("\r\n")
-          body.each {|s| socket.write(s)}
-        ensure
-          body.close if body.respond_to? :close
-        end
-      end
-    end
-  end
-end
diff --git a/lib/rack/handler/thin.rb b/lib/rack/handler/thin.rb
deleted file mode 100644
index 393a6e98699dca62de2b928e8183f9f56eeafbf9..0000000000000000000000000000000000000000
--- a/lib/rack/handler/thin.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require "thin"
-require "thin/server"
-require "thin/logging"
-require "thin/backends/tcp_server"
-
-module Rack
-  module Handler
-    class Thin
-      def self.run(app, **options)
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-        host = options.delete(:Host) || default_host
-        port = options.delete(:Port) || 8080
-        args = [host, port, app, options]
-        # Thin versions below 0.8.0 do not support additional options
-        args.pop if ::Thin::VERSION::MAJOR < 1 && ::Thin::VERSION::MINOR < 8
-        server = ::Thin::Server.new(*args)
-        yield server if block_given?
-        server.start
-      end
-
-      def self.valid_options
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-        {
-          "Host=HOST" => "Hostname to listen on (default: #{default_host})",
-          "Port=PORT" => "Port to listen on (default: 8080)",
-        }
-      end
-    end
-  end
-end
diff --git a/lib/rack/handler/webrick.rb b/lib/rack/handler/webrick.rb
deleted file mode 100644
index d2f389758a33c48e125a4dc4af76dc041b9830de..0000000000000000000000000000000000000000
--- a/lib/rack/handler/webrick.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-# frozen_string_literal: true
-
-require 'webrick'
-require 'stringio'
-
-# This monkey patch allows for applications to perform their own chunking
-# through WEBrick::HTTPResponse if rack is set to true.
-class WEBrick::HTTPResponse
-  attr_accessor :rack
-
-  alias _rack_setup_header setup_header
-  def setup_header
-    app_chunking = rack && @header['transfer-encoding'] == 'chunked'
-
-    @chunked = app_chunking if app_chunking
-
-    _rack_setup_header
-
-    @chunked = false if app_chunking
-  end
-end
-
-module Rack
-  module Handler
-    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
-      def self.run(app, **options)
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : nil
-
-        if !options[:BindAddress] || options[:Host]
-          options[:BindAddress] = options.delete(:Host) || default_host
-        end
-        options[:Port] ||= 8080
-        if options[:SSLEnable]
-          require 'webrick/https'
-        end
-
-        @server = ::WEBrick::HTTPServer.new(options)
-        @server.mount "/", Rack::Handler::WEBrick, app
-        yield @server  if block_given?
-        @server.start
-      end
-
-      def self.valid_options
-        environment  = ENV['RACK_ENV'] || 'development'
-        default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-        {
-          "Host=HOST" => "Hostname to listen on (default: #{default_host})",
-          "Port=PORT" => "Port to listen on (default: 8080)",
-        }
-      end
-
-      def self.shutdown
-        if @server
-          @server.shutdown
-          @server = nil
-        end
-      end
-
-      def initialize(server, app)
-        super server
-        @app = app
-      end
-
-      def service(req, res)
-        res.rack = true
-        env = req.meta_vars
-        env.delete_if { |k, v| v.nil? }
-
-        rack_input = StringIO.new(req.body.to_s)
-        rack_input.set_encoding(Encoding::BINARY)
-
-        env.update(
-          RACK_VERSION      => Rack::VERSION,
-          RACK_INPUT        => rack_input,
-          RACK_ERRORS       => $stderr,
-          RACK_MULTITHREAD  => true,
-          RACK_MULTIPROCESS => false,
-          RACK_RUNONCE      => false,
-          RACK_URL_SCHEME   => ["yes", "on", "1"].include?(env[HTTPS]) ? "https" : "http",
-          RACK_IS_HIJACK    => true,
-          RACK_HIJACK       => lambda { raise NotImplementedError, "only partial hijack is supported."},
-          RACK_HIJACK_IO    => nil
-        )
-
-        env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
-        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]
-        end
-        env[REQUEST_PATH] ||= [env[SCRIPT_NAME], env[PATH_INFO]].join
-
-        status, headers, body = @app.call(env)
-        begin
-          res.status = status.to_i
-          io_lambda = nil
-          headers.each { |k, vs|
-            if k == RACK_HIJACK
-              io_lambda = vs
-            elsif k.downcase == "set-cookie"
-              res.cookies.concat vs.split("\n")
-            else
-              # Since WEBrick won't accept repeated headers,
-              # merge the values per RFC 1945 section 4.2.
-              res[k] = vs.split("\n").join(", ")
-            end
-          }
-
-          if io_lambda
-            rd, wr = IO.pipe
-            res.body = rd
-            res.chunked = true
-            io_lambda.call wr
-          elsif body.respond_to?(:to_path)
-            res.body = ::File.open(body.to_path, 'rb')
-          else
-            body.each { |part|
-              res.body << part
-            }
-          end
-        ensure
-          body.close  if body.respond_to? :close
-        end
-      end
-    end
-  end
-end
diff --git a/lib/rack/head.rb b/lib/rack/head.rb
index 8025a27d514c2fe57ad8436718639da532f2a996..c1c430f653e603320fcc2f691af18bfa7b326811 100644
--- a/lib/rack/head.rb
+++ b/lib/rack/head.rb
@@ -1,5 +1,8 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'body_proxy'
+
 module Rack
   # Rack::Head returns an empty body for all HEAD requests. It leaves
   # all other requests unchanged.
@@ -9,17 +12,15 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
+      _, _, body = response = @app.call(env)
 
       if env[REQUEST_METHOD] == HEAD
-        [
-          status, headers, Rack::BodyProxy.new([]) do
-            body.close if body.respond_to? :close
-          end
-        ]
-      else
-        [status, headers, body]
+        response[2] = Rack::BodyProxy.new([]) do
+          body.close if body.respond_to? :close
+        end
       end
+
+      response
     end
   end
 end
diff --git a/lib/rack/headers.rb b/lib/rack/headers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ae1a89d12fceeae76b085292e6fd9cc7fcb1645f
--- /dev/null
+++ b/lib/rack/headers.rb
@@ -0,0 +1,154 @@
+module Rack
+  # Rack::Headers is a Hash subclass that downcases all keys.  It's designed
+  # to be used by rack applications that don't implement the Rack 3 SPEC
+  # (by using non-lowercase response header keys), automatically handling
+  # the downcasing of keys.
+  class Headers < Hash
+    def self.[](*items)
+      if items.length % 2 != 0
+        if items.length == 1 && items.first.is_a?(Hash)
+          new.merge!(items.first)
+        else
+          raise ArgumentError, "odd number of arguments for Rack::Headers"
+        end
+      else
+        hash = new
+        loop do
+          break if items.length == 0
+          key = items.shift
+          value = items.shift
+          hash[key] = value
+        end
+        hash
+      end
+    end
+
+    def [](key)
+      super(downcase_key(key))
+    end
+
+    def []=(key, value)
+      super(key.downcase.freeze, value)
+    end
+    alias store []=
+
+    def assoc(key)
+      super(downcase_key(key))
+    end
+
+    def compare_by_identity
+      raise TypeError, "Rack::Headers cannot compare by identity, use regular Hash"
+    end
+
+    def delete(key)
+      super(downcase_key(key))
+    end
+
+    def dig(key, *a)
+      super(downcase_key(key), *a)
+    end
+
+    def fetch(key, *default, &block)
+      key = downcase_key(key)
+      super
+    end
+
+    def fetch_values(*a)
+      super(*a.map!{|key| downcase_key(key)})
+    end
+
+    def has_key?(key)
+      super(downcase_key(key))
+    end
+    alias include? has_key?
+    alias key? has_key?
+    alias member? has_key?
+
+    def invert
+      hash = self.class.new
+      each{|key, value| hash[value] = key}
+      hash
+    end
+
+    def merge(hash, &block)
+      dup.merge!(hash, &block)
+    end
+
+    def reject(&block)
+      hash = dup
+      hash.reject!(&block)
+      hash
+    end
+
+    def replace(hash)
+      clear
+      update(hash)
+    end
+
+    def select(&block)
+      hash = dup
+      hash.select!(&block)
+      hash
+    end
+
+    def to_proc
+      lambda{|x| self[x]}
+    end
+
+    def transform_values(&block)
+      dup.transform_values!(&block)
+    end
+
+    def update(hash, &block)
+      hash.each do |key, value|
+        self[key] = if block_given? && include?(key)
+          block.call(key, self[key], value)
+        else
+          value
+        end
+      end
+      self
+    end
+    alias merge! update
+
+    def values_at(*keys)
+      keys.map{|key| self[key]}
+    end
+
+    # :nocov:
+    if RUBY_VERSION >= '2.5'
+    # :nocov:
+      def slice(*a)
+        h = self.class.new
+        a.each{|k| h[k] = self[k] if has_key?(k)}
+        h
+      end
+
+      def transform_keys(&block)
+        dup.transform_keys!(&block)
+      end
+
+      def transform_keys!
+        hash = self.class.new
+        each do |k, v|
+          hash[yield k] = v
+        end
+        replace(hash)
+      end
+    end
+
+    # :nocov:
+    if RUBY_VERSION >= '3.0'
+    # :nocov:
+      def except(*a)
+        super(*a.map!{|key| downcase_key(key)})
+      end
+    end
+
+    private
+
+    def downcase_key(key)
+      key.is_a?(String) ? key.downcase : key
+    end
+  end
+end
diff --git a/lib/rack/lint.rb b/lib/rack/lint.rb
old mode 100644
new mode 100755
index 67d2eb1294c27005e097d9d82aa50094b2d68fbc..ee3ec7161a83bfb4346650d114c87645a929a438
--- a/lib/rack/lint.rb
+++ b/lib/rack/lint.rb
@@ -2,6 +2,9 @@
 
 require 'forwardable'
 
+require_relative 'constants'
+require_relative 'utils'
+
 module Rack
   # Rack::Lint validates your application and the requests and
   # responses according to the Rack spec.
@@ -9,798 +12,896 @@ module Rack
   class Lint
     def initialize(app)
       @app = app
-      @content_length = nil
     end
 
     # :stopdoc:
 
     class LintError < RuntimeError; end
-    module Assertion
-      def assert(message)
-        unless yield
-          raise LintError, message
-        end
-      end
-    end
-    include Assertion
-
-    ## This specification aims to formalize the Rack protocol.  You
+    # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
+    # should not be removed. The whitespace creates paragraphs in the RDoc
+    # output.
+    #
+    ## This specification aims to formalize the Rack protocol. You
     ## can (and should) use Rack::Lint to enforce it.
     ##
     ## When you develop middleware, be sure to add a Lint before and
     ## after to catch all mistakes.
-
+    ##
     ## = Rack applications
-
+    ##
     ## A Rack application is a Ruby object (not a class) that
     ## responds to +call+.
     def call(env = nil)
-      dup._call(env)
+      Wrapper.new(@app, env).response
     end
 
-    def _call(env)
-      ## It takes exactly one argument, the *environment*
-      assert("No env given") { env }
-      check_env env
-
-      env[RACK_INPUT] = InputWrapper.new(env[RACK_INPUT])
-      env[RACK_ERRORS] = ErrorWrapper.new(env[RACK_ERRORS])
-
-      ## and returns an Array of exactly three values:
-      ary = @app.call(env)
-      assert("response is not an Array, but #{ary.class}") {
-        ary.kind_of? Array
-      }
-      assert("response array has #{ary.size} elements instead of 3") {
-        ary.size == 3
-      }
-
-      status, headers, @body = ary
-      ## The *status*,
-      check_status status
-      ## the *headers*,
-      check_headers headers
-
-      hijack_proc = check_hijack_response headers, env
-      if hijack_proc && headers.is_a?(Hash)
-        headers[RACK_HIJACK] = hijack_proc
+    class Wrapper
+      def initialize(app, env)
+        @app = app
+        @env = env
+        @response = nil
+        @head_request = false
+
+        @status = nil
+        @headers = nil
+        @body = nil
+        @invoked = nil
+        @content_length = nil
+        @closed = false
+        @size = 0
       end
 
-      ## and the *body*.
-      check_content_type status, headers
-      check_content_length status, headers
-      @head_request = env[REQUEST_METHOD] == HEAD
-      [status, headers, self]
-    end
+      def response
+        ## It takes exactly one argument, the *environment*
+        raise LintError, "No env given" unless @env
+        check_environment(@env)
 
-    ## == The Environment
-    def check_env(env)
-      ## The environment must be an unfrozen instance of Hash that includes
-      ## CGI-like headers.  The application is free to modify the
-      ## environment.
-      assert("env #{env.inspect} is not a Hash, but #{env.class}") {
-        env.kind_of? Hash
-      }
-      assert("env should not be frozen, but is") {
-        !env.frozen?
-      }
-
-      ##
-      ## The environment is required to include these variables
-      ## (adopted from PEP333), except when they'd be empty, but see
-      ## below.
-
-      ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
-      ##                           "GET" or "POST". This cannot ever
-      ##                           be an empty string, and so is
-      ##                           always required.
-
-      ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
-      ##                        URL's "path" that corresponds to the
-      ##                        application object, so that the
-      ##                        application knows its virtual
-      ##                        "location". This may be an empty
-      ##                        string, if the application corresponds
-      ##                        to the "root" of the server.
-
-      ## <tt>PATH_INFO</tt>:: The remainder of the request URL's
-      ##                      "path", designating the virtual
-      ##                      "location" of the request's target
-      ##                      within the application. This may be an
-      ##                      empty string, if the request URL targets
-      ##                      the application root and does not have a
-      ##                      trailing slash. This value may be
-      ##                      percent-encoded when originating from
-      ##                      a URL.
-
-      ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
-      ##                         follows the <tt>?</tt>, if any. May be
-      ##                         empty, but is always required!
-
-      ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
-      ##                        <tt>PATH_INFO</tt>, these variables can be
-      ##                        used to complete the URL. Note, however,
-      ##                        that <tt>HTTP_HOST</tt>, if present,
-      ##                        should be used in preference to
-      ##                        <tt>SERVER_NAME</tt> for reconstructing
-      ##                        the request URL.
-      ##                        <tt>SERVER_NAME</tt> can never be an empty
-      ##                        string, and so is always required.
-
-      ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
-      ##                        server is running on. Should be specified if
-      ##                        the server is running on a non-standard port.
-
-      ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
-      ##                            client-supplied HTTP request
-      ##                            headers (i.e., variables whose
-      ##                            names begin with <tt>HTTP_</tt>). The
-      ##                            presence or absence of these
-      ##                            variables should correspond with
-      ##                            the presence or absence of the
-      ##                            appropriate HTTP header in the
-      ##                            request. See
-      ##                            {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
-      ##                         See Rack::VERSION, that corresponds to
-      ##                         the version of this SPEC.
-
-      ## <tt>rack.url_scheme</tt>:: +http+ or +https+, depending on the
-      ##                            request URL.
-
-      ## <tt>rack.input</tt>:: See below, the input stream.
-
-      ## <tt>rack.errors</tt>:: See below, the error stream.
-
-      ## <tt>rack.multithread</tt>:: true if the application object may be
-      ##                             simultaneously invoked by another thread
-      ##                             in the same process, false otherwise.
-
-      ## <tt>rack.multiprocess</tt>:: true if an equivalent application object
-      ##                              may be simultaneously invoked by another
-      ##                              process, false otherwise.
-
-      ## <tt>rack.run_once</tt>:: true if the server expects
-      ##                          (but does not guarantee!) that the
-      ##                          application will only be invoked this one
-      ##                          time during the life of its containing
-      ##                          process. Normally, this will only be true
-      ##                          for a server based on CGI
-      ##                          (or something similar).
-
-      ## <tt>rack.hijack?</tt>:: present and true if the server supports
-      ##                         connection hijacking. See below, hijacking.
-
-      ## <tt>rack.hijack</tt>:: an object responding to #call that must be
-      ##                        called at least once before using
-      ##                        rack.hijack_io.
-      ##                        It is recommended #call return rack.hijack_io
-      ##                        as well as setting it in env if necessary.
-
-      ## <tt>rack.hijack_io</tt>:: if rack.hijack? is true, and rack.hijack
-      ##                           has received #call, this will contain
-      ##                           an object resembling an IO. See hijacking.
-
-      ## Additional environment specifications have approved to
-      ## standardized middleware APIs.  None of these are required to
-      ## be implemented by the server.
-
-      ## <tt>rack.session</tt>:: A hash like interface for storing
-      ##                         request session data.
-      ##                         The store must implement:
-      if session = env[RACK_SESSION]
-        ##                         store(key, value)         (aliased as []=);
-        assert("session #{session.inspect} must respond to store and []=") {
-          session.respond_to?(:store) && session.respond_to?(:[]=)
-        }
+        @env[RACK_INPUT] = InputWrapper.new(@env[RACK_INPUT])
+        @env[RACK_ERRORS] = ErrorWrapper.new(@env[RACK_ERRORS])
 
-        ##                         fetch(key, default = nil) (aliased as []);
-        assert("session #{session.inspect} must respond to fetch and []") {
-          session.respond_to?(:fetch) && session.respond_to?(:[])
-        }
+        ## and returns a non-frozen Array of exactly three values:
+        @response = @app.call(@env)
+        raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array
+        raise LintError, "response is frozen" if @response.frozen?
+        raise LintError, "response array has #{@response.size} elements instead of 3" unless @response.size == 3
 
-        ##                         delete(key);
-        assert("session #{session.inspect} must respond to delete") {
-          session.respond_to?(:delete)
-        }
+        @status, @headers, @body = @response
+        ## The *status*,
+        check_status(@status)
 
-        ##                         clear;
-        assert("session #{session.inspect} must respond to clear") {
-          session.respond_to?(:clear)
-        }
+        ## the *headers*,
+        check_headers(@headers)
 
-        ##                         to_hash (returning unfrozen Hash instance);
-        assert("session #{session.inspect} must respond to to_hash and return unfrozen Hash instance") {
-          session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
-        }
+        hijack_proc = check_hijack_response(@headers, @env)
+        if hijack_proc
+          @headers[RACK_HIJACK] = hijack_proc
+        end
+
+        ## and the *body*.
+        check_content_type(@status, @headers)
+        check_content_length(@status, @headers)
+        @head_request = @env[REQUEST_METHOD] == HEAD
+
+        @lint = (@env['rack.lint'] ||= []) << self
+
+        if (@env['rack.lint.body_iteration'] ||= 0) > 0
+          raise LintError, "Middleware must not call #each directly"
+        end
+
+        return [@status, @headers, self]
       end
 
-      ## <tt>rack.logger</tt>:: A common object interface for logging messages.
-      ##                        The object must implement:
-      if logger = env[RACK_LOGGER]
-        ##                         info(message, &block)
-        assert("logger #{logger.inspect} must respond to info") {
-          logger.respond_to?(:info)
-        }
+      ##
+      ## == The Environment
+      ##
+      def check_environment(env)
+        ## The environment must be an unfrozen instance of Hash that includes
+        ## CGI-like headers. The Rack application is free to modify the
+        ## environment.
+        raise LintError, "env #{env.inspect} is not a Hash, but #{env.class}" unless env.kind_of? Hash
+        raise LintError, "env should not be frozen, but is" if env.frozen?
 
-        ##                         debug(message, &block)
-        assert("logger #{logger.inspect} must respond to debug") {
-          logger.respond_to?(:debug)
-        }
+        ##
+        ## The environment is required to include these variables
+        ## (adopted from {PEP 333}[https://peps.python.org/pep-0333/]), except when they'd be empty, but see
+        ## below.
+
+        ## <tt>REQUEST_METHOD</tt>:: The HTTP request method, such as
+        ##                           "GET" or "POST". This cannot ever
+        ##                           be an empty string, and so is
+        ##                           always required.
+
+        ## <tt>SCRIPT_NAME</tt>:: The initial portion of the request
+        ##                        URL's "path" that corresponds to the
+        ##                        application object, so that the
+        ##                        application knows its virtual
+        ##                        "location". This may be an empty
+        ##                        string, if the application corresponds
+        ##                        to the "root" of the server.
+
+        ## <tt>PATH_INFO</tt>:: The remainder of the request URL's
+        ##                      "path", designating the virtual
+        ##                      "location" of the request's target
+        ##                      within the application. This may be an
+        ##                      empty string, if the request URL targets
+        ##                      the application root and does not have a
+        ##                      trailing slash. This value may be
+        ##                      percent-encoded when originating from
+        ##                      a URL.
+
+        ## <tt>QUERY_STRING</tt>:: The portion of the request URL that
+        ##                         follows the <tt>?</tt>, if any. May be
+        ##                         empty, but is always required!
+
+        ## <tt>SERVER_NAME</tt>:: When combined with <tt>SCRIPT_NAME</tt> and
+        ##                        <tt>PATH_INFO</tt>, these variables can be
+        ##                        used to complete the URL. Note, however,
+        ##                        that <tt>HTTP_HOST</tt>, if present,
+        ##                        should be used in preference to
+        ##                        <tt>SERVER_NAME</tt> for reconstructing
+        ##                        the request URL.
+        ##                        <tt>SERVER_NAME</tt> can never be an empty
+        ##                        string, and so is always required.
+
+        ## <tt>SERVER_PORT</tt>:: An optional +Integer+ which is the port the
+        ##                        server is running on. Should be specified if
+        ##                        the server is running on a non-standard port.
+
+        ## <tt>SERVER_PROTOCOL</tt>:: A string representing the HTTP version used
+        ##                            for the request.
+
+        ## <tt>HTTP_</tt> Variables:: Variables corresponding to the
+        ##                            client-supplied HTTP request
+        ##                            headers (i.e., variables whose
+        ##                            names begin with <tt>HTTP_</tt>). The
+        ##                            presence or absence of these
+        ##                            variables should correspond with
+        ##                            the presence or absence of the
+        ##                            appropriate HTTP header in the
+        ##                            request. See
+        ##                            {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.url_scheme</tt>:: +http+ or +https+, depending on the
+        ##                            request URL.
+
+        ## <tt>rack.input</tt>:: See below, the input stream.
+
+        ## <tt>rack.errors</tt>:: See below, the error stream.
+
+        ## <tt>rack.hijack?</tt>:: See below, if present and true, indicates
+        ##                         that the server supports partial hijacking.
+
+        ## <tt>rack.hijack</tt>:: See below, if present, an object responding
+        ##                        to +call+ that is used to perform a full
+        ##                        hijack.
+
+        ## Additional environment specifications have approved to
+        ## standardized middleware APIs. None of these are required to
+        ## be implemented by the server.
+
+        ## <tt>rack.session</tt>:: A hash-like interface for storing
+        ##                         request session data.
+        ##                         The store must implement:
+        if session = env[RACK_SESSION]
+          ##                         store(key, value)         (aliased as []=);
+          unless session.respond_to?(:store) && session.respond_to?(:[]=)
+            raise LintError, "session #{session.inspect} must respond to store and []="
+          end
 
-        ##                         warn(message, &block)
-        assert("logger #{logger.inspect} must respond to warn") {
-          logger.respond_to?(:warn)
-        }
+          ##                         fetch(key, default = nil) (aliased as []);
+          unless session.respond_to?(:fetch) && session.respond_to?(:[])
+            raise LintError, "session #{session.inspect} must respond to fetch and []"
+          end
 
-        ##                         error(message, &block)
-        assert("logger #{logger.inspect} must respond to error") {
-          logger.respond_to?(:error)
-        }
+          ##                         delete(key);
+          unless session.respond_to?(:delete)
+            raise LintError, "session #{session.inspect} must respond to delete"
+          end
 
-        ##                         fatal(message, &block)
-        assert("logger #{logger.inspect} must respond to fatal") {
-          logger.respond_to?(:fatal)
-        }
-      end
+          ##                         clear;
+          unless session.respond_to?(:clear)
+            raise LintError, "session #{session.inspect} must respond to clear"
+          end
 
-      ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
-      if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
-        assert("rack.multipart.buffer_size must be an Integer > 0 if specified") {
-          bufsize.is_a?(Integer) && bufsize > 0
-        }
-      end
+          ##                         to_hash (returning unfrozen Hash instance);
+          unless session.respond_to?(:to_hash) && session.to_hash.kind_of?(Hash) && !session.to_hash.frozen?
+            raise LintError, "session #{session.inspect} must respond to to_hash and return unfrozen Hash instance"
+          end
+        end
+
+        ## <tt>rack.logger</tt>:: A common object interface for logging messages.
+        ##                        The object must implement:
+        if logger = env[RACK_LOGGER]
+          ##                         info(message, &block)
+          unless logger.respond_to?(:info)
+            raise LintError, "logger #{logger.inspect} must respond to info"
+          end
+
+          ##                         debug(message, &block)
+          unless logger.respond_to?(:debug)
+            raise LintError, "logger #{logger.inspect} must respond to debug"
+          end
+
+          ##                         warn(message, &block)
+          unless logger.respond_to?(:warn)
+            raise LintError, "logger #{logger.inspect} must respond to warn"
+          end
+
+          ##                         error(message, &block)
+          unless logger.respond_to?(:error)
+            raise LintError, "logger #{logger.inspect} must respond to error"
+          end
 
-      ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
-      if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
-        assert("rack.multipart.tempfile_factory must respond to #call") { tempfile_factory.respond_to?(:call) }
-        env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
-          io = tempfile_factory.call(filename, content_type)
-          assert("rack.multipart.tempfile_factory return value must respond to #<<") { io.respond_to?(:<<) }
-          io
+          ##                         fatal(message, &block)
+          unless logger.respond_to?(:fatal)
+            raise LintError, "logger #{logger.inspect} must respond to fatal"
+          end
         end
-      end
 
-      ## The server or the application can store their own data in the
-      ## environment, too.  The keys must contain at least one dot,
-      ## and should be prefixed uniquely.  The prefix <tt>rack.</tt>
-      ## is reserved for use with the Rack core distribution and other
-      ## accepted specifications and must not be used otherwise.
-      ##
+        ## <tt>rack.multipart.buffer_size</tt>:: An Integer hint to the multipart parser as to what chunk size to use for reads and writes.
+        if bufsize = env[RACK_MULTIPART_BUFFER_SIZE]
+          unless bufsize.is_a?(Integer) && bufsize > 0
+            raise LintError, "rack.multipart.buffer_size must be an Integer > 0 if specified"
+          end
+        end
+
+        ## <tt>rack.multipart.tempfile_factory</tt>:: An object responding to #call with two arguments, the filename and content_type given for the multipart form field, and returning an IO-like object that responds to #<< and optionally #rewind. This factory will be used to instantiate the tempfile for each multipart form file upload field, rather than the default class of Tempfile.
+        if tempfile_factory = env[RACK_MULTIPART_TEMPFILE_FACTORY]
+          raise LintError, "rack.multipart.tempfile_factory must respond to #call" unless tempfile_factory.respond_to?(:call)
+          env[RACK_MULTIPART_TEMPFILE_FACTORY] = lambda do |filename, content_type|
+            io = tempfile_factory.call(filename, content_type)
+            raise LintError, "rack.multipart.tempfile_factory return value must respond to #<<" unless io.respond_to?(:<<)
+            io
+          end
+        end
 
-      %w[REQUEST_METHOD SERVER_NAME QUERY_STRING
-         rack.version rack.input rack.errors
-         rack.multithread rack.multiprocess rack.run_once].each { |header|
-        assert("env missing required key #{header}") { env.include? header }
-      }
+        ## The server or the application can store their own data in the
+        ## environment, too.  The keys must contain at least one dot,
+        ## and should be prefixed uniquely.  The prefix <tt>rack.</tt>
+        ## is reserved for use with the Rack core distribution and other
+        ## accepted specifications and must not be used otherwise.
+        ##
+
+        %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL
+           rack.input rack.errors].each { |header|
+          raise LintError, "env missing required key #{header}" unless env.include? header
+        }
 
-      ## The <tt>SERVER_PORT</tt> must be an Integer if set.
-      assert("env[SERVER_PORT] is not an Integer") do
+        ## The <tt>SERVER_PORT</tt> must be an Integer if set.
         server_port = env["SERVER_PORT"]
-        server_port.nil? || (Integer(server_port) rescue false)
-      end
+        unless server_port.nil? || (Integer(server_port) rescue false)
+          raise LintError, "env[SERVER_PORT] is not an Integer"
+        end
 
-      ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
-      assert("#{env[SERVER_NAME]} must be a valid authority") do
-        URI.parse("http://#{env[SERVER_NAME]}/") rescue false
-      end
+        ## The <tt>SERVER_NAME</tt> must be a valid authority as defined by RFC7540.
+        unless (URI.parse("http://#{env[SERVER_NAME]}/") rescue false)
+          raise LintError, "#{env[SERVER_NAME]} must be a valid authority"
+        end
 
-      ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
-      assert("#{env[HTTP_HOST]} must be a valid authority") do
-        URI.parse("http://#{env[HTTP_HOST]}/") rescue false
-      end
+        ## The <tt>HTTP_HOST</tt> must be a valid authority as defined by RFC7540.
+        unless (URI.parse("http://#{env[HTTP_HOST]}/") rescue false)
+          raise LintError, "#{env[HTTP_HOST]} must be a valid authority"
+        end
 
-      ## The environment must not contain the keys
-      ## <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]}") {
-          not env.include? header
-        }
-      }
-
-      ## The CGI keys (named without a period) must have String values.
-      ## If the string values for CGI keys contain non-ASCII characters,
-      ## they should use ASCII-8BIT encoding.
-      env.each { |key, value|
-        next  if key.include? "."   # Skip extensions
-        assert("env variable #{key} has non-string value #{value.inspect}") {
-          value.kind_of? String
-        }
-        next if value.encoding == Encoding::ASCII_8BIT
-        assert("env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}") {
-          value.b !~ /[\x80-\xff]/n
+        ## The <tt>SERVER_PROTOCOL</tt> must match the regexp <tt>HTTP/\d(\.\d)?</tt>.
+        server_protocol = env['SERVER_PROTOCOL']
+        unless %r{HTTP/\d(\.\d)?}.match?(server_protocol)
+          raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?"
+        end
+
+        ## If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>.
+        if env['HTTP_VERSION'] && env['HTTP_VERSION'] != server_protocol
+          raise LintError, "env[HTTP_VERSION] does not equal env[SERVER_PROTOCOL]"
+        end
+
+        ## The environment must not contain the keys
+        ## <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|
+          if env.include? header
+            raise LintError, "env contains #{header}, must use #{header[5..-1]}"
+          end
         }
-      }
-
-      ## There are the following restrictions:
-
-      ## * <tt>rack.version</tt> must be an array of Integers.
-      assert("rack.version must be an Array, was #{env[RACK_VERSION].class}") {
-        env[RACK_VERSION].kind_of? Array
-      }
-      ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
-      assert("rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}") {
-        %w[http https].include?(env[RACK_URL_SCHEME])
-      }
-
-      ## * There must be a valid input stream in <tt>rack.input</tt>.
-      check_input env[RACK_INPUT]
-      ## * There must be a valid error stream in <tt>rack.errors</tt>.
-      check_error env[RACK_ERRORS]
-      ## * There may be a valid hijack stream in <tt>rack.hijack_io</tt>
-      check_hijack env
-
-      ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
-      assert("REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}") {
-        env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
-      }
-
-      ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
-      assert("SCRIPT_NAME must start with /") {
-        !env.include?(SCRIPT_NAME) ||
-        env[SCRIPT_NAME] == "" ||
-        env[SCRIPT_NAME] =~ /\A\//
-      }
-      ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
-      assert("PATH_INFO must start with /") {
-        !env.include?(PATH_INFO) ||
-        env[PATH_INFO] == "" ||
-        env[PATH_INFO] =~ /\A\//
-      }
-      ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
-      assert("Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}") {
-        !env.include?("CONTENT_LENGTH") || env["CONTENT_LENGTH"] =~ /\A\d+\z/
-      }
-
-      ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
-      ##   set.  <tt>PATH_INFO</tt> should be <tt>/</tt> if
-      ##   <tt>SCRIPT_NAME</tt> is empty.
-      assert("One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)") {
-        env[SCRIPT_NAME] || env[PATH_INFO]
-      }
-      ##   <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
-      assert("SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'") {
-        env[SCRIPT_NAME] != "/"
-      }
-    end
 
-    ## === The Input Stream
-    ##
-    ## The input stream is an IO-like object which contains the raw HTTP
-    ## POST data.
-    def check_input(input)
-      ## When applicable, its external encoding must be "ASCII-8BIT" and it
-      ## must be opened in binary mode, for Ruby 1.9 compatibility.
-      assert("rack.input #{input} does not have ASCII-8BIT as its external encoding") {
-        input.external_encoding == Encoding::ASCII_8BIT
-      } if input.respond_to?(:external_encoding)
-      assert("rack.input #{input} is not opened in binary mode") {
-        input.binmode?
-      } if input.respond_to?(:binmode?)
-
-      ## The input stream must respond to +gets+, +each+, +read+ and +rewind+.
-      [:gets, :each, :read, :rewind].each { |method|
-        assert("rack.input #{input} does not respond to ##{method}") {
-          input.respond_to? method
+        ## The CGI keys (named without a period) must have String values.
+        ## If the string values for CGI keys contain non-ASCII characters,
+        ## they should use ASCII-8BIT encoding.
+        env.each { |key, value|
+          next  if key.include? "."   # Skip extensions
+          unless value.kind_of? String
+            raise LintError, "env variable #{key} has non-string value #{value.inspect}"
+          end
+          next if value.encoding == Encoding::ASCII_8BIT
+          unless value.b !~ /[\x80-\xff]/n
+            raise LintError, "env variable #{key} has value containing non-ASCII characters and has non-ASCII-8BIT encoding #{value.inspect} encoding: #{value.encoding}"
+          end
         }
-      }
-    end
 
-    class InputWrapper
-      include Assertion
+        ## There are the following restrictions:
 
-      def initialize(input)
-        @input = input
-      end
+        ## * <tt>rack.url_scheme</tt> must either be +http+ or +https+.
+        unless %w[http https].include?(env[RACK_URL_SCHEME])
+          raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
+        end
 
-      ## * +gets+ must be called without arguments and return a string,
-      ##   or +nil+ on EOF.
-      def gets(*args)
-        assert("rack.input#gets called with arguments") { args.size == 0 }
-        v = @input.gets
-        assert("rack.input#gets didn't return a String") {
-          v.nil? or v.kind_of? String
-        }
-        v
+        ## * There must be a valid input stream in <tt>rack.input</tt>.
+        check_input env[RACK_INPUT]
+        ## * There must be a valid error stream in <tt>rack.errors</tt>.
+        check_error env[RACK_ERRORS]
+        ## * There may be a valid hijack callback in <tt>rack.hijack</tt>
+        check_hijack env
+
+        ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
+        unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
+          raise LintError, "REQUEST_METHOD unknown: #{env[REQUEST_METHOD].dump}"
+        end
+
+        ## * The <tt>SCRIPT_NAME</tt>, if non-empty, must start with <tt>/</tt>
+        if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
+          raise LintError, "SCRIPT_NAME must start with /"
+        end
+        ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
+        if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\//
+          raise LintError, "PATH_INFO must start with /"
+        end
+        ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
+        if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
+          raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
+        end
+
+        ## * One of <tt>SCRIPT_NAME</tt> or <tt>PATH_INFO</tt> must be
+        ##   set. <tt>PATH_INFO</tt> should be <tt>/</tt> if
+        ##   <tt>SCRIPT_NAME</tt> is empty.
+        unless env[SCRIPT_NAME] || env[PATH_INFO]
+          raise LintError, "One of SCRIPT_NAME or PATH_INFO must be set (make PATH_INFO '/' if SCRIPT_NAME is empty)"
+        end
+        ##   <tt>SCRIPT_NAME</tt> never should be <tt>/</tt>, but instead be empty.
+        unless env[SCRIPT_NAME] != "/"
+          raise LintError, "SCRIPT_NAME cannot be '/', make it '' and PATH_INFO '/'"
+        end
+
+        ## <tt>rack.response_finished</tt>:: An array of callables run by the server after the response has been
+        ## processed. This would typically be invoked after sending the response to the client, but it could also be
+        ## invoked if an error occurs while generating the response or sending the response; in that case, the error
+        ## argument will be a subclass of +Exception+.
+        ## The callables are invoked with +env, status, headers, error+ arguments and should not raise any
+        ## exceptions. They should be invoked in reverse order of registration.
+        if callables = env[RACK_RESPONSE_FINISHED]
+          raise LintError, "rack.response_finished must be an array of callable objects" unless callables.is_a?(Array)
+
+          callables.each do |callable|
+            raise LintError, "rack.response_finished values must respond to call(env, status, headers, error)" unless callable.respond_to?(:call)
+          end
+        end
       end
 
-      ## * +read+ behaves like IO#read.
-      ##   Its signature is <tt>read([length, [buffer]])</tt>.
-      ##
-      ##   If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
-      ##   and +buffer+ must be a String and may not be nil.
       ##
-      ##   If +length+ is given and not nil, then this method reads at most
-      ##   +length+ bytes from the input stream.
+      ## === The Input Stream
       ##
-      ##   If +length+ is not given or nil, then this method reads
-      ##   all data until EOF.
-      ##
-      ##   When EOF is reached, this method returns nil if +length+ is given
-      ##   and not nil, or "" if +length+ is not given or is nil.
-      ##
-      ##   If +buffer+ is given, then the read data will be placed
-      ##   into +buffer+ instead of a newly created String object.
-      def read(*args)
-        assert("rack.input#read called with too many arguments") {
-          args.size <= 2
-        }
-        if args.size >= 1
-          assert("rack.input#read called with non-integer and non-nil length") {
-            args.first.kind_of?(Integer) || args.first.nil?
-          }
-          assert("rack.input#read called with a negative length") {
-            args.first.nil? || args.first >= 0
-          }
+      ## The input stream is an IO-like object which contains the raw HTTP
+      ## POST data.
+      def check_input(input)
+        ## When applicable, its external encoding must be "ASCII-8BIT" and it
+        ## must be opened in binary mode, for Ruby 1.9 compatibility.
+        if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
+          raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
         end
-        if args.size >= 2
-          assert("rack.input#read called with non-String buffer") {
-            args[1].kind_of?(String)
-          }
+        if input.respond_to?(:binmode?) && !input.binmode?
+          raise LintError, "rack.input #{input} is not opened in binary mode"
         end
 
-        v = @input.read(*args)
-
-        assert("rack.input#read didn't return nil or a String") {
-          v.nil? or v.kind_of? String
+        ## The input stream must respond to +gets+, +each+, and +read+.
+        [:gets, :each, :read].each { |method|
+          unless input.respond_to? method
+            raise LintError, "rack.input #{input} does not respond to ##{method}"
+          end
         }
-        if args[0].nil?
-          assert("rack.input#read(nil) returned nil on EOF") {
-            !v.nil?
-          }
+      end
+
+      class InputWrapper
+        def initialize(input)
+          @input = input
         end
 
-        v
-      end
+        ## * +gets+ must be called without arguments and return a string,
+        ##   or +nil+ on EOF.
+        def gets(*args)
+          raise LintError, "rack.input#gets called with arguments" unless args.size == 0
+          v = @input.gets
+          unless v.nil? or v.kind_of? String
+            raise LintError, "rack.input#gets didn't return a String"
+          end
+          v
+        end
 
-      ## * +each+ must be called without arguments and only yield Strings.
-      def each(*args)
-        assert("rack.input#each called with arguments") { args.size == 0 }
-        @input.each { |line|
-          assert("rack.input#each didn't yield a String") {
-            line.kind_of? String
+        ## * +read+ behaves like IO#read.
+        ##   Its signature is <tt>read([length, [buffer]])</tt>.
+        ##
+        ##   If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
+        ##   and +buffer+ must be a String and may not be nil.
+        ##
+        ##   If +length+ is given and not nil, then this method reads at most
+        ##   +length+ bytes from the input stream.
+        ##
+        ##   If +length+ is not given or nil, then this method reads
+        ##   all data until EOF.
+        ##
+        ##   When EOF is reached, this method returns nil if +length+ is given
+        ##   and not nil, or "" if +length+ is not given or is nil.
+        ##
+        ##   If +buffer+ is given, then the read data will be placed
+        ##   into +buffer+ instead of a newly created String object.
+        def read(*args)
+          unless args.size <= 2
+            raise LintError, "rack.input#read called with too many arguments"
+          end
+          if args.size >= 1
+            unless args.first.kind_of?(Integer) || args.first.nil?
+              raise LintError, "rack.input#read called with non-integer and non-nil length"
+            end
+            unless args.first.nil? || args.first >= 0
+              raise LintError, "rack.input#read called with a negative length"
+            end
+          end
+          if args.size >= 2
+            unless args[1].kind_of?(String)
+              raise LintError, "rack.input#read called with non-String buffer"
+            end
+          end
+
+          v = @input.read(*args)
+
+          unless v.nil? or v.kind_of? String
+            raise LintError, "rack.input#read didn't return nil or a String"
+          end
+          if args[0].nil?
+            unless !v.nil?
+              raise LintError, "rack.input#read(nil) returned nil on EOF"
+            end
+          end
+
+          v
+        end
+
+        ## * +each+ must be called without arguments and only yield Strings.
+        def each(*args)
+          raise LintError, "rack.input#each called with arguments" unless args.size == 0
+          @input.each { |line|
+            unless line.kind_of? String
+              raise LintError, "rack.input#each didn't yield a String"
+            end
+            yield line
           }
-          yield line
-        }
+        end
+
+        ## * +close+ can be called on the input stream to indicate that the
+        ## any remaining input is not needed.
+        def close(*args)
+          @input.close(*args)
+        end
       end
 
-      ## * +rewind+ must be called without arguments. It rewinds the input
-      ##   stream back to the beginning. It must not raise Errno::ESPIPE:
-      ##   that is, it may not be a pipe or a socket. Therefore, handler
-      ##   developers must buffer the input data into some rewindable object
-      ##   if the underlying input stream is not rewindable.
-      def rewind(*args)
-        assert("rack.input#rewind called with arguments") { args.size == 0 }
-        assert("rack.input#rewind raised Errno::ESPIPE") {
-          begin
-            @input.rewind
-            true
-          rescue Errno::ESPIPE
-            false
+      ##
+      ## === The Error Stream
+      ##
+      def check_error(error)
+        ## The error stream must respond to +puts+, +write+ and +flush+.
+        [:puts, :write, :flush].each { |method|
+          unless error.respond_to? method
+            raise LintError, "rack.error #{error} does not respond to ##{method}"
           end
         }
       end
 
-      ## * +close+ must never be called on the input stream.
-      def close(*args)
-        assert("rack.input#close must not be called") { false }
-      end
-    end
+      class ErrorWrapper
+        def initialize(error)
+          @error = error
+        end
 
-    ## === The Error Stream
-    def check_error(error)
-      ## The error stream must respond to +puts+, +write+ and +flush+.
-      [:puts, :write, :flush].each { |method|
-        assert("rack.error #{error} does not respond to ##{method}") {
-          error.respond_to? method
-        }
-      }
-    end
+        ## * +puts+ must be called with a single argument that responds to +to_s+.
+        def puts(str)
+          @error.puts str
+        end
 
-    class ErrorWrapper
-      include Assertion
+        ## * +write+ must be called with a single argument that is a String.
+        def write(str)
+          raise LintError, "rack.errors#write not called with a String" unless str.kind_of? String
+          @error.write str
+        end
 
-      def initialize(error)
-        @error = error
-      end
+        ## * +flush+ must be called without arguments and must be called
+        ##   in order to make the error appear for sure.
+        def flush
+          @error.flush
+        end
 
-      ## * +puts+ must be called with a single argument that responds to +to_s+.
-      def puts(str)
-        @error.puts str
+        ## * +close+ must never be called on the error stream.
+        def close(*args)
+          raise LintError, "rack.errors#close must not be called"
+        end
       end
 
-      ## * +write+ must be called with a single argument that is a String.
-      def write(str)
-        assert("rack.errors#write not called with a String") { str.kind_of? String }
-        @error.write str
+      ##
+      ## === Hijacking
+      ##
+      ## The hijacking interfaces provides a means for an application to take
+      ## control of the HTTP connection. There are two distinct hijack
+      ## interfaces: full hijacking where the application takes over the raw
+      ## connection, and partial hijacking where the application takes over
+      ## just the response body stream. In both cases, the application is
+      ## responsible for closing the hijacked stream.
+      ##
+      ## Full hijacking only works with HTTP/1. Partial hijacking is functionally
+      ## equivalent to streaming bodies, and is still optionally supported for
+      ## backwards compatibility with older Rack versions.
+      ##
+      ## ==== Full Hijack
+      ##
+      ## Full hijack is used to completely take over an HTTP/1 connection. It
+      ## occurs before any headers are written and causes the request to
+      ## ignores any response generated by the application.
+      ##
+      ## It is intended to be used when applications need access to raw HTTP/1
+      ## connection.
+      ##
+      def check_hijack(env)
+        ## If +rack.hijack+ is present in +env+, it must respond to +call+
+        if original_hijack = env[RACK_HIJACK]
+          raise LintError, "rack.hijack must respond to call" unless original_hijack.respond_to?(:call)
+
+          env[RACK_HIJACK] = proc do
+            io = original_hijack.call
+
+            ## and return an +IO+ instance which can be used to read and write
+            ## to the underlying connection using HTTP/1 semantics and
+            ## formatting.
+            raise LintError, "rack.hijack must return an IO instance" unless io.is_a?(IO)
+
+            io
+          end
+        end
       end
 
-      ## * +flush+ must be called without arguments and must be called
-      ##   in order to make the error appear for sure.
-      def flush
-        @error.flush
+      ##
+      ## ==== Partial Hijack
+      ##
+      ## Partial hijack is used for bi-directional streaming of the request and
+      ## response body. It occurs after the status and headers are written by
+      ## the server and causes the server to ignore the Body of the response.
+      ##
+      ## It is intended to be used when applications need bi-directional
+      ## streaming.
+      ##
+      def check_hijack_response(headers, env)
+        ## If +rack.hijack?+ is present in +env+ and truthy,
+        if env[RACK_IS_HIJACK]
+          ## an application may set the special response header +rack.hijack+
+          if original_hijack = headers[RACK_HIJACK]
+            ## to an object that responds to +call+,
+            unless original_hijack.respond_to?(:call)
+              raise LintError, 'rack.hijack header must respond to #call'
+            end
+            ## accepting a +stream+ argument.
+            return proc do |io|
+              original_hijack.call StreamWrapper.new(io)
+            end
+          end
+          ##
+          ## After the response status and headers have been sent, this hijack
+          ## callback will be invoked with a +stream+ argument which follows the
+          ## same interface as outlined in "Streaming Body". Servers must
+          ## ignore the +body+ part of the response tuple when the
+          ## +rack.hijack+ response header is present. Using an empty +Array+
+          ## instance is recommended.
+        else
+          ##
+          ## The special response header +rack.hijack+ must only be set
+          ## if the request +env+ has a truthy +rack.hijack?+.
+          if headers.key?(RACK_HIJACK)
+            raise LintError, 'rack.hijack header must not be present if server does not support hijacking'
+          end
+        end
+
+        nil
       end
 
-      ## * +close+ must never be called on the error stream.
-      def close(*args)
-        assert("rack.errors#close must not be called") { false }
+      ## == The Response
+      ##
+      ## === The Status
+      ##
+      def check_status(status)
+        ## This is an HTTP status. It must be an Integer greater than or equal to
+        ## 100.
+        unless status.is_a?(Integer) && status >= 100
+          raise LintError, "Status must be an Integer >=100"
+        end
       end
-    end
 
-    class HijackWrapper
-      include Assertion
-      extend Forwardable
+      ##
+      ## === The Headers
+      ##
+      def check_headers(headers)
+        ## The headers must be a unfrozen Hash.
+        unless headers.kind_of?(Hash)
+          raise LintError, "headers object should be a hash, but isn't (got #{headers.class} as headers)"
+        end
 
-      REQUIRED_METHODS = [
-        :read, :write, :read_nonblock, :write_nonblock, :flush, :close,
-        :close_read, :close_write, :closed?
-      ]
+        if headers.frozen?
+          raise LintError, "headers object should not be frozen, but is"
+        end
 
-      def_delegators :@io, *REQUIRED_METHODS
+        headers.each do |key, value|
+          ## The header keys must be Strings.
+          unless key.kind_of? String
+            raise LintError, "header key must be a string, was #{key.class}"
+          end
 
-      def initialize(io)
-        @io = io
-        REQUIRED_METHODS.each do |meth|
-          assert("rack.hijack_io must respond to #{meth}") { io.respond_to? meth }
+          ## Special headers starting "rack." are for communicating with the
+          ## server, and must not be sent back to the client.
+          next if key.start_with?("rack.")
+
+          ## The header must not contain a +Status+ key.
+          raise LintError, "header must not contain status" if key == "status"
+          ## Header keys must conform to RFC7230 token specification, i.e. cannot
+          ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
+          raise LintError, "invalid header name: #{key}" if key =~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/
+          ## Header keys must not contain uppercase ASCII characters (A-Z).
+          raise LintError, "uppercase character in header name: #{key}" if key =~ /[A-Z]/
+
+          ## Header values must be either a String instance,
+          if value.kind_of?(String)
+            check_header_value(key, value)
+          elsif value.kind_of?(Array)
+            ## or an Array of String instances,
+            value.each{|value| check_header_value(key, value)}
+          else
+            raise LintError, "a header value must be a String or Array of Strings, but the value of '#{key}' is a #{value.class}"
+          end
         end
       end
-    end
 
-    ## === Hijacking
-    #
-    # AUTHORS: n.b. The trailing whitespace between paragraphs is important and
-    # should not be removed. The whitespace creates paragraphs in the RDoc
-    # output.
-    #
-    ## ==== Request (before status)
-    def check_hijack(env)
-      if env[RACK_IS_HIJACK]
-        ## If rack.hijack? is true then rack.hijack must respond to #call.
-        original_hijack = env[RACK_HIJACK]
-        assert("rack.hijack must respond to call") { original_hijack.respond_to?(:call) }
-        env[RACK_HIJACK] = proc do
-          ## rack.hijack must return the io that will also be assigned (or is
-          ## already present, in rack.hijack_io.
-          io = original_hijack.call
-          HijackWrapper.new(io)
-          ##
-          ## rack.hijack_io must respond to:
-          ## <tt>read, write, read_nonblock, write_nonblock, flush, close,
-          ## close_read, close_write, closed?</tt>
-          ##
-          ## The semantics of these IO methods must be a best effort match to
-          ## those of a normal ruby IO or Socket object, using standard
-          ## arguments and raising standard exceptions. Servers are encouraged
-          ## to simply pass on real IO objects, although it is recognized that
-          ## this approach is not directly compatible with SPDY and HTTP 2.0.
-          ##
-          ## IO provided in rack.hijack_io should preference the
-          ## IO::WaitReadable and IO::WaitWritable APIs wherever supported.
-          ##
-          ## There is a deliberate lack of full specification around
-          ## rack.hijack_io, as semantics will change from server to server.
-          ## Users are encouraged to utilize this API with a knowledge of their
-          ## server choice, and servers may extend the functionality of
-          ## hijack_io to provide additional features to users. The purpose of
-          ## rack.hijack is for Rack to "get out of the way", as such, Rack only
-          ## provides the minimum of specification and support.
-          env[RACK_HIJACK_IO] = HijackWrapper.new(env[RACK_HIJACK_IO])
-          io
-        end
-      else
-        ##
-        ## If rack.hijack? is false, then rack.hijack should not be set.
-        assert("rack.hijack? is false, but rack.hijack is present") { env[RACK_HIJACK].nil? }
-        ##
-        ## If rack.hijack? is false, then rack.hijack_io should not be set.
-        assert("rack.hijack? is false, but rack.hijack_io is present") { env[RACK_HIJACK_IO].nil? }
+      def check_header_value(key, value)
+        ## such that each String instance must not contain characters below 037.
+        if value =~ /[\000-\037]/
+          raise LintError, "invalid header value #{key}: #{value.inspect}"
+        end
       end
-    end
 
-    ## ==== Response (after headers)
-    ## It is also possible to hijack a response after the status and headers
-    ## have been sent.
-    def check_hijack_response(headers, env)
-
-      # this check uses headers like a hash, but the spec only requires
-      # headers respond to #each
-      headers = Rack::Utils::HeaderHash[headers]
-
-      ## In order to do this, an application may set the special header
-      ## <tt>rack.hijack</tt> to an object that responds to <tt>call</tt>
-      ## accepting an argument that conforms to the <tt>rack.hijack_io</tt>
-      ## protocol.
-      ##
-      ## After the headers have been sent, and this hijack callback has been
-      ## called, the application is now responsible for the remaining lifecycle
-      ## of the IO. The application is also responsible for maintaining HTTP
-      ## semantics. Of specific note, in almost all cases in the current SPEC,
-      ## applications will have wanted to specify the header Connection:close in
-      ## HTTP/1.1, and not Connection:keep-alive, as there is no protocol for
-      ## returning hijacked sockets to the web server. For that purpose, use the
-      ## body streaming API instead (progressively yielding strings via each).
-      ##
-      ## Servers must ignore the <tt>body</tt> part of the response tuple when
-      ## the <tt>rack.hijack</tt> response API is in use.
-
-      if env[RACK_IS_HIJACK] && headers[RACK_HIJACK]
-        assert('rack.hijack header must respond to #call') {
-          headers[RACK_HIJACK].respond_to? :call
+      ##
+      ## === The content-type
+      ##
+      def check_content_type(status, headers)
+        headers.each { |key, value|
+          ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
+          ## 204, or 304.
+          if key == "content-type"
+            if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
+              raise LintError, "content-type header found in #{status} response, not allowed"
+            end
+            return
+          end
         }
-        original_hijack = headers[RACK_HIJACK]
-        proc do |io|
-          original_hijack.call HijackWrapper.new(io)
-        end
-      else
-        ##
-        ## The special response header <tt>rack.hijack</tt> must only be set
-        ## if the request env has <tt>rack.hijack?</tt> <tt>true</tt>.
-        assert('rack.hijack header must not be present if server does not support hijacking') {
-          headers[RACK_HIJACK].nil?
+      end
+
+      ##
+      ## === The content-length
+      ##
+      def check_content_length(status, headers)
+        headers.each { |key, value|
+          if key == 'content-length'
+            ## There must not be a <tt>content-length</tt> header key when the
+            ## +Status+ is 1xx, 204, or 304.
+            if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
+              raise LintError, "content-length header found in #{status} response, not allowed"
+            end
+            @content_length = value
+          end
         }
+      end
 
-        nil
+      def verify_content_length(size)
+        if @head_request
+          unless size == 0
+            raise LintError, "Response body was given for HEAD request, but should be empty"
+          end
+        elsif @content_length
+          unless @content_length == size.to_s
+            raise LintError, "content-length header was #{@content_length}, but should be #{size}"
+          end
+        end
       end
-    end
-    ## ==== Conventions
-    ## * Middleware should not use hijack unless it is handling the whole
-    ##   response.
-    ## * Middleware may wrap the IO object for the response pattern.
-    ## * Middleware should not wrap the IO object for the request pattern. The
-    ##   request pattern is intended to provide the hijacker with "raw tcp".
-
-    ## == The Response
-
-    ## === The Status
-    def check_status(status)
-      ## This is an HTTP status. When parsed as integer (+to_i+), it must be
-      ## greater than or equal to 100.
-      assert("Status must be >=100 seen as integer") { status.to_i >= 100 }
-    end
 
-    ## === The Headers
-    def check_headers(header)
-      ## The header must respond to +each+, and yield values of key and value.
-      assert("headers object should respond to #each, but doesn't (got #{header.class} as headers)") {
-         header.respond_to? :each
-      }
-
-      header.each { |key, value|
-        ## The header keys must be Strings.
-        assert("header key must be a string, was #{key.class}") {
-          key.kind_of? String
-        }
+      ##
+      ## === The Body
+      ##
+      ## The Body is typically an +Array+ of +String+ instances, an enumerable
+      ## that yields +String+ instances, a +Proc+ instance, or a File-like
+      ## object.
+      ##
+      ## The Body must respond to +each+ or +call+. It may optionally respond
+      ## to +to_path+ or +to_ary+. A Body that responds to +each+ is considered
+      ## to be an Enumerable Body. A Body that responds to +call+ is considered
+      ## to be a Streaming Body.
+      ##
+      ## A Body that responds to both +each+ and +call+ must be treated as an
+      ## Enumerable Body, not a Streaming Body. If it responds to +each+, you
+      ## must call +each+ and not +call+. If the Body doesn't respond to
+      ## +each+, then you can assume it responds to +call+.
+      ##
+      ## The Body must either be consumed or returned. The Body is consumed by
+      ## optionally calling either +each+ or +call+.
+      ## Then, if the Body responds to +close+, it must be called to release
+      ## any resources associated with the generation of the body.
+      ## In other words, +close+ must always be called at least once; typically
+      ## after the web server has sent the response to the client, but also in
+      ## cases where the Rack application makes internal/virtual requests and
+      ## discards the response.
+      ##
+      def close
+        ##
+        ## After calling +close+, the Body is considered closed and should not
+        ## be consumed again.
+        @closed = true
 
-        ## 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
-        ## contain non-printable ASCII, DQUOTE or "(),/:;<=>?@[\]{}".
-        assert("invalid header name: #{key}") { key !~ /[\(\),\/:;<=>\?@\[\\\]{}[:cntrl:]]/ }
-
-        ## The values of the header must be Strings,
-        assert("a header value must be a String, but the value of " +
-          "'#{key}' is a #{value.class}") { value.kind_of? String }
-        ## consisting of lines (for multiple header values, e.g. multiple
-        ## <tt>Set-Cookie</tt> values) separated by "\\n".
-        value.split("\n").each { |item|
-          ## The lines must not contain characters below 037.
-          assert("invalid header value #{key}: #{item.inspect}") {
-            item !~ /[\000-\037]/
-          }
-        }
-      }
-    end
+        ## If the original Body is replaced by a new Body, the new Body must
+        ## also consume the original Body by calling +close+ if possible.
+        @body.close if @body.respond_to?(:close)
 
-    ## === The Content-Type
-    def check_content_type(status, headers)
-      headers.each { |key, value|
-        ## There must not be a <tt>Content-Type</tt>, when the +Status+ is 1xx,
-        ## 204 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.key? status.to_i
-          }
-          return
+        index = @lint.index(self)
+        unless @env['rack.lint'][0..index].all? {|lint| lint.instance_variable_get(:@closed)}
+          raise LintError, "Body has not been closed"
         end
-      }
-    end
+      end
 
-    ## === The Content-Length
-    def check_content_length(status, headers)
-      headers.each { |key, value|
-        if key.downcase == 'content-length'
-          ## There must not be a <tt>Content-Length</tt> header when the
-          ## +Status+ is 1xx, 204 or 304.
-          assert("Content-Length header found in #{status} response, not allowed") {
-            not Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.key? status.to_i
-          }
-          @content_length = value
+      def verify_to_path
+        ##
+        ## If the Body responds to +to_path+, it must return a +String+
+        ## path for the local file system whose contents are identical
+        ## to that produced by calling +each+; this may be used by the
+        ## server as an alternative, possibly more efficient way to
+        ## transport the response. The +to_path+ method does not consume
+        ## the body.
+        if @body.respond_to?(:to_path)
+          unless ::File.exist? @body.to_path
+            raise LintError, "The file identified by body.to_path does not exist"
+          end
         end
-      }
-    end
+      end
 
-    def verify_content_length(bytes)
-      if @head_request
-        assert("Response body was given for HEAD request, but should be empty") {
-          bytes == 0
-        }
-      elsif @content_length
-        assert("Content-Length header was #{@content_length}, but should be #{bytes}") {
-          @content_length == bytes.to_s
-        }
+      ##
+      ## ==== Enumerable Body
+      ##
+      def each
+        ## The Enumerable Body must respond to +each+.
+        raise LintError, "Enumerable Body must respond to each" unless @body.respond_to?(:each)
+
+        ## It must only be called once.
+        raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
+
+        ## It must not be called after being closed.
+        raise LintError, "Response body is already closed" if @closed
+
+        @invoked = :each
+
+        @body.each do |chunk|
+          ## and must only yield String values.
+          unless chunk.kind_of? String
+            raise LintError, "Body yielded non-string value #{chunk.inspect}"
+          end
+
+          ##
+          ## The Body itself should not be an instance of String, as this will
+          ## break in Ruby 1.9.
+          ##
+          ## Middleware must not call +each+ directly on the Body.
+          ## Instead, middleware can return a new Body that calls +each+ on the
+          ## original Body, yielding at least once per iteration.
+          if @lint[0] == self
+            @env['rack.lint.body_iteration'] += 1
+          else
+            if (@env['rack.lint.body_iteration'] -= 1) > 0
+              raise LintError, "New body must yield at least once per iteration of old body"
+            end
+          end
+
+          @size += chunk.bytesize
+          yield chunk
+        end
+
+        verify_content_length(@size)
+
+        verify_to_path
       end
-    end
 
-    ## === The Body
-    def each
-      @closed = false
-      bytes = 0
+      BODY_METHODS = {to_ary: true, each: true, call: true, to_path: true}
 
-      ## The Body must respond to +each+
-      assert("Response body must respond to each") do
-        @body.respond_to?(:each)
+      def to_path
+        @body.to_path
       end
 
-      @body.each { |part|
-        ## and must only yield String values.
-        assert("Body yielded non-string value #{part.inspect}") {
-          part.kind_of? String
-        }
-        bytes += part.bytesize
-        yield part
-      }
-      verify_content_length(bytes)
+      def respond_to?(name, *)
+        if BODY_METHODS.key?(name)
+          @body.respond_to?(name)
+        else
+          super
+        end
+      end
+
+      ##
+      ## If the Body responds to +to_ary+, it must return an +Array+ whose
+      ## contents are identical to that produced by calling +each+.
+      ## Middleware may call +to_ary+ directly on the Body and return a new
+      ## Body in its place. In other words, middleware can only process the
+      ## Body directly if it responds to +to_ary+. If the Body responds to both
+      ## +to_ary+ and +close+, its implementation of +to_ary+ must call
+      ## +close+.
+      def to_ary
+        @body.to_ary.tap do |content|
+          unless content == @body.enum_for.to_a
+            raise LintError, "#to_ary not identical to contents produced by calling #each"
+          end
+        end
+      ensure
+        close
+      end
 
       ##
-      ## The Body itself should not be an instance of String, as this will
-      ## break in Ruby 1.9.
+      ## ==== Streaming Body
       ##
-      ## If the Body responds to +close+, it will be called after iteration. If
-      ## the body is replaced by a middleware after action, the original body
-      ## must be closed first, if it responds to close.
-      # XXX howto: assert("Body has not been closed") { @closed }
+      def call(stream)
+        ## The Streaming Body must respond to +call+.
+        raise LintError, "Streaming Body must respond to call" unless @body.respond_to?(:call)
 
+        ## It must only be called once.
+        raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
 
-      ##
-      ## If the Body responds to +to_path+, it must return a String
-      ## identifying the location of a file whose contents are identical
-      ## to that produced by calling +each+; this may be used by the
-      ## server as an alternative, possibly more efficient way to
-      ## transport the response.
+        ## It must not be called after being closed.
+        raise LintError, "Response body is already closed" if @closed
 
-      if @body.respond_to?(:to_path)
-        assert("The file identified by body.to_path does not exist") {
-          ::File.exist? @body.to_path
-        }
+        @invoked = :call
+
+        ## It takes a +stream+ argument.
+        ##
+        ## The +stream+ argument must implement:
+        ## <tt>read, write, <<, flush, close, close_read, close_write, closed?</tt>
+        ##
+        @body.call(StreamWrapper.new(stream))
       end
 
-      ##
-      ## The Body commonly is an Array of Strings, the application
-      ## instance itself, or a File-like object.
-    end
+      class StreamWrapper
+        extend Forwardable
 
-    def close
-      @closed = true
-      @body.close  if @body.respond_to?(:close)
-    end
+        ## The semantics of these IO methods must be a best effort match to
+        ## those of a normal Ruby IO or Socket object, using standard arguments
+        ## and raising standard exceptions. Servers are encouraged to simply
+        ## pass on real IO objects, although it is recognized that this approach
+        ## is not directly compatible with HTTP/2.
+        REQUIRED_METHODS = [
+          :read, :write, :<<, :flush, :close,
+          :close_read, :close_write, :closed?
+        ]
+
+        def_delegators :@stream, *REQUIRED_METHODS
+
+        def initialize(stream)
+          @stream = stream
 
-    # :startdoc:
+          REQUIRED_METHODS.each do |method_name|
+            raise LintError, "Stream must respond to #{method_name}" unless stream.respond_to?(method_name)
+          end
+        end
+      end
 
+      # :startdoc:
+    end
   end
 end
 
+##
 ## == Thanks
-## Some parts of this specification are adopted from PEP333: Python
-## Web Server Gateway Interface
-## v1.0 (http://www.python.org/dev/peps/pep-0333/). I'd like to thank
-## everyone involved in that effort.
+## Some parts of this specification are adopted from {PEP 333 – Python Web Server Gateway Interface v1.0}[https://peps.python.org/pep-0333/]
+## I'd like to thank everyone involved in that effort.
diff --git a/lib/rack/lobster.rb b/lib/rack/lobster.rb
deleted file mode 100644
index b86a625de0c2fcc241b00badd9840344454af499..0000000000000000000000000000000000000000
--- a/lib/rack/lobster.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-require 'zlib'
-
-module Rack
-  # Paste has a Pony, Rack has a Lobster!
-  class Lobster
-    LobsterString = Zlib::Inflate.inflate("eJx9kEEOwyAMBO99xd7MAcytUhPlJyj2
-    P6jy9i4k9EQyGAnBarEXeCBqSkntNXsi/ZCvC48zGQoZKikGrFMZvgS5ZHd+aGWVuWwhVF0
-    t1drVmiR42HcWNz5w3QanT+2gIvTVCiE1lm1Y0eU4JGmIIbaKwextKn8rvW+p5PIwFl8ZWJ
-    I8jyiTlhTcYXkekJAzTyYN6E08A+dk8voBkAVTJQ==".delete("\n ").unpack("m*")[0])
-
-    LambdaLobster = lambda { |env|
-      if env[QUERY_STRING].include?("flip")
-        lobster = LobsterString.split("\n").
-          map { |line| line.ljust(42).reverse }.
-          join("\n")
-        href = "?"
-      else
-        lobster = LobsterString
-        href = "?flip"
-      end
-
-      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]
-    }
-
-    def call(env)
-      req = Request.new(env)
-      if req.GET["flip"] == "left"
-        lobster = LobsterString.split("\n").map do |line|
-          line.ljust(42).reverse.
-            gsub('\\', 'TEMP').
-            gsub('/', '\\').
-            gsub('TEMP', '/').
-            gsub('{', '}').
-            gsub('(', ')')
-        end.join("\n")
-        href = "?flip=right"
-      elsif req.GET["flip"] == "crash"
-        raise "Lobster crashed"
-      else
-        lobster = LobsterString
-        href = "?flip=left"
-      end
-
-      res = Response.new
-      res.write "<title>Lobstericious!</title>"
-      res.write "<pre>"
-      res.write lobster
-      res.write "</pre>"
-      res.write "<p><a href='#{href}'>flip!</a></p>"
-      res.write "<p><a href='?flip=crash'>crash!</a></p>"
-      res.finish
-    end
-
-  end
-end
-
-if $0 == __FILE__
-  # :nocov:
-  require_relative '../rack'
-  Rack::Server.start(
-    app: Rack::ShowExceptions.new(Rack::Lint.new(Rack::Lobster.new)), Port: 9292
-  )
-  # :nocov:
-end
diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb
index 4bae3a9034e83b50d3dfbf2fad13722df25fd0a6..342123a0f0880ea806888d4ddf787c1ff40b4419 100644
--- a/lib/rack/lock.rb
+++ b/lib/rack/lock.rb
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require 'thread'
+require_relative 'body_proxy'
 
 module Rack
   # Rack::Lock locks every request inside a mutex, so that every request
@@ -12,10 +12,8 @@ module Rack
 
     def call(env)
       @mutex.lock
-      @env = env
-      @old_rack_multithread = env[RACK_MULTITHREAD]
       begin
-        response = @app.call(env.merge!(RACK_MULTITHREAD => false))
+        response = @app.call(env)
         returned = response << BodyProxy.new(response.pop) { unlock }
       ensure
         unlock unless returned
@@ -26,7 +24,6 @@ module Rack
 
     def unlock
       @mutex.unlock
-      @env[RACK_MULTITHREAD] = @old_rack_multithread
     end
   end
 end
diff --git a/lib/rack/logger.rb b/lib/rack/logger.rb
index 6c4bede0cf37eedf036dfd8290f0df43c2b10c13..bdcc069005ab5174c352da14d2a40834290312ff 100644
--- a/lib/rack/logger.rb
+++ b/lib/rack/logger.rb
@@ -2,6 +2,8 @@
 
 require 'logger'
 
+require_relative 'constants'
+
 module Rack
   # Sets up rack.logger to write to rack.errors stream
   class Logger
diff --git a/lib/rack/media_type.rb b/lib/rack/media_type.rb
index 41937c9947e63aee85a2e4d8b48860bcce617ee0..ff3145debfcc145219af43935afbae50af178a0d 100644
--- a/lib/rack/media_type.rb
+++ b/lib/rack/media_type.rb
@@ -15,7 +15,7 @@ module Rack
       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
       def type(content_type)
         return nil unless content_type
-        content_type.split(SPLIT_PATTERN, 2).first.tap &:downcase!
+        content_type.split(SPLIT_PATTERN, 2).first.tap(&:downcase!)
       end
 
       # The media type parameters provided in CONTENT_TYPE as a Hash, or
diff --git a/lib/rack/method_override.rb b/lib/rack/method_override.rb
index b586f5339b6fb13313979fc530d5a6b0cd224fec..6125b1916f8f903fe8e3885426451f33228b7870 100644
--- a/lib/rack/method_override.rb
+++ b/lib/rack/method_override.rb
@@ -1,5 +1,9 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'request'
+require_relative 'utils'
+
 module Rack
   class MethodOverride
     HTTP_METHODS = %w[GET HEAD PUT POST DELETE OPTIONS PATCH LINK UNLINK]
@@ -42,7 +46,7 @@ module Rack
     end
 
     def method_override_param(req)
-      req.POST[METHOD_OVERRIDE_PARAM_KEY]
+      req.POST[METHOD_OVERRIDE_PARAM_KEY] if req.form_data? || req.parseable_data?
     rescue Utils::InvalidParameterError, Utils::ParameterTypeError, QueryParser::ParamsTooDeepError
       req.get_header(RACK_ERRORS).puts "Invalid or incomplete POST params"
     rescue EOFError
diff --git a/lib/rack/mime.rb b/lib/rack/mime.rb
index f6c02c1fd6ad871eb09e4bf3783a37c8dc6b91d0..b62e3e8d6fb4dd8830390375ab5d51c7b5beb3bc 100644
--- a/lib/rack/mime.rb
+++ b/lib/rack/mime.rb
@@ -63,6 +63,7 @@ module Rack
       ".aif"       => "audio/x-aiff",
       ".aiff"      => "audio/x-aiff",
       ".ami"       => "application/vnd.amiga.ami",
+      ".apng"      => "image/apng",
       ".appcache"  => "text/cache-manifest",
       ".apr"       => "application/vnd.lotus-approach",
       ".asc"       => "application/pgp-signature",
@@ -77,6 +78,7 @@ module Rack
       ".atx"       => "application/vnd.antix.game-component",
       ".au"        => "audio/basic",
       ".avi"       => "video/x-msvideo",
+      ".avif"      => "image/avif",
       ".bat"       => "application/x-msdownload",
       ".bcpio"     => "application/x-bcpio",
       ".bdm"       => "application/vnd.syncml.dm+wbxml",
@@ -197,6 +199,7 @@ module Rack
       ".fe_launch" => "application/vnd.denovo.fcselayout-link",
       ".fg5"       => "application/vnd.fujitsu.oasysgp",
       ".fli"       => "video/x-fli",
+      ".flif"      => "image/flif",
       ".flo"       => "application/vnd.micrografx.flo",
       ".flv"       => "video/x-flv",
       ".flw"       => "application/vnd.kde.kivio",
@@ -237,6 +240,10 @@ module Rack
       ".h264"      => "video/h264",
       ".hbci"      => "application/vnd.hbci",
       ".hdf"       => "application/x-hdf",
+      ".heic"      => "image/heic",
+      ".heics"     => "image/heic-sequence",
+      ".heif"      => "image/heif",
+      ".heifs"     => "image/heif-sequence",
       ".hh"        => "text/x-c",
       ".hlp"       => "application/winhlp",
       ".hpgl"      => "application/vnd.hp-hpgl",
@@ -617,6 +624,7 @@ module Rack
       ".wbs"       => "application/vnd.criticaltools.wbs+xml",
       ".wbxml"     => "application/vnd.wap.wbxml",
       ".webm"      => "video/webm",
+      ".webp"      => "image/webp",
       ".wm"        => "video/x-ms-wm",
       ".wma"       => "audio/x-ms-wma",
       ".wmd"       => "application/x-ms-wmd",
diff --git a/lib/rack/mock.rb b/lib/rack/mock.rb
index 5b2512ca091dec59ee53a65a17108a4662f3d569..5e5c457c728c42850d78c86aaa448af261569207 100644
--- a/lib/rack/mock.rb
+++ b/lib/rack/mock.rb
@@ -1,273 +1,3 @@
 # frozen_string_literal: true
 
-require 'uri'
-require 'stringio'
-require_relative '../rack'
-require 'cgi/cookie'
-
-module Rack
-  # Rack::MockRequest helps testing your Rack application without
-  # actually using HTTP.
-  #
-  # After performing a request on a URL with get/post/put/patch/delete, it
-  # returns a MockResponse with useful helper methods for effective
-  # testing.
-  #
-  # You can pass a hash with additional configuration to the
-  # get/post/put/patch/delete.
-  # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
-  # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
-  # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
-
-  class MockRequest
-    class FatalWarning < RuntimeError
-    end
-
-    class FatalWarner
-      def puts(warning)
-        raise FatalWarning, warning
-      end
-
-      def write(warning)
-        raise FatalWarning, warning
-      end
-
-      def flush
-      end
-
-      def string
-        ""
-      end
-    end
-
-    DEFAULT_ENV = {
-      RACK_VERSION      => Rack::VERSION,
-      RACK_INPUT        => StringIO.new,
-      RACK_ERRORS       => StringIO.new,
-      RACK_MULTITHREAD  => true,
-      RACK_MULTIPROCESS => true,
-      RACK_RUNONCE      => false,
-    }.freeze
-
-    def initialize(app)
-      @app = app
-    end
-
-    # Make a GET request and return a MockResponse. See #request.
-    def get(uri, opts = {})     request(GET, uri, opts)     end
-    # Make a POST request and return a MockResponse. See #request.
-    def post(uri, opts = {})    request(POST, uri, opts)    end
-    # Make a PUT request and return a MockResponse. See #request.
-    def put(uri, opts = {})     request(PUT, uri, opts)     end
-    # Make a PATCH request and return a MockResponse. See #request.
-    def patch(uri, opts = {})   request(PATCH, uri, opts)   end
-    # Make a DELETE request and return a MockResponse. See #request.
-    def delete(uri, opts = {})  request(DELETE, uri, opts)  end
-    # Make a HEAD request and return a MockResponse. See #request.
-    def head(uri, opts = {})    request(HEAD, uri, opts)    end
-    # Make an OPTIONS request and return a MockResponse. See #request.
-    def options(uri, opts = {}) request(OPTIONS, uri, opts) end
-
-    # Make a request using the given request method for the given
-    # uri to the rack application and return a MockResponse.
-    # Options given are passed to MockRequest.env_for.
-    def request(method = GET, uri = "", opts = {})
-      env = self.class.env_for(uri, opts.merge(method: method))
-
-      if opts[:lint]
-        app = Rack::Lint.new(@app)
-      else
-        app = @app
-      end
-
-      errors = env[RACK_ERRORS]
-      status, headers, body = app.call(env)
-      MockResponse.new(status, headers, body, errors)
-    ensure
-      body.close if body.respond_to?(:close)
-    end
-
-    # For historical reasons, we're pinning to RFC 2396.
-    # URI::Parser = URI::RFC2396_Parser
-    def self.parse_uri_rfc2396(uri)
-      @parser ||= URI::Parser.new
-      @parser.parse(uri)
-    end
-
-    # Return the Rack environment used for a request to +uri+.
-    # All options that are strings are added to the returned environment.
-    # Options:
-    # :fatal :: Whether to raise an exception if request outputs to rack.errors
-    # :input :: The rack.input to set
-    # :method :: The HTTP request method to use
-    # :params :: The params to use
-    # :script_name :: The SCRIPT_NAME to set
-    def self.env_for(uri = "", opts = {})
-      uri = parse_uri_rfc2396(uri)
-      uri.path = "/#{uri.path}" unless uri.path[0] == ?/
-
-      env = DEFAULT_ENV.dup
-
-      env[REQUEST_METHOD]  = (opts[:method] ? opts[:method].to_s.upcase : GET).b
-      env[SERVER_NAME]     = (uri.host || "example.org").b
-      env[SERVER_PORT]     = (uri.port ? uri.port.to_s : "80").b
-      env[QUERY_STRING]    = (uri.query.to_s).b
-      env[PATH_INFO]       = ((!uri.path || uri.path.empty?) ? "/" : uri.path).b
-      env[RACK_URL_SCHEME] = (uri.scheme || "http").b
-      env[HTTPS]           = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
-
-      env[SCRIPT_NAME] = opts[:script_name] || ""
-
-      if opts[:fatal]
-        env[RACK_ERRORS] = FatalWarner.new
-      else
-        env[RACK_ERRORS] = StringIO.new
-      end
-
-      if params = opts[:params]
-        if env[REQUEST_METHOD] == GET
-          params = Utils.parse_nested_query(params) if params.is_a?(String)
-          params.update(Utils.parse_nested_query(env[QUERY_STRING]))
-          env[QUERY_STRING] = Utils.build_nested_query(params)
-        elsif !opts.has_key?(:input)
-          opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
-          if params.is_a?(Hash)
-            if data = Rack::Multipart.build_multipart(params)
-              opts[:input] = data
-              opts["CONTENT_LENGTH"] ||= data.length.to_s
-              opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
-            else
-              opts[:input] = Utils.build_nested_query(params)
-            end
-          else
-            opts[:input] = params
-          end
-        end
-      end
-
-      empty_str = String.new
-      opts[:input] ||= empty_str
-      if String === opts[:input]
-        rack_input = StringIO.new(opts[:input])
-      else
-        rack_input = opts[:input]
-      end
-
-      rack_input.set_encoding(Encoding::BINARY)
-      env[RACK_INPUT] = rack_input
-
-      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
-      }
-
-      env
-    end
-  end
-
-  # Rack::MockResponse provides useful helpers for testing your apps.
-  # Usually, you don't create the MockResponse on your own, but use
-  # MockRequest.
-
-  class MockResponse < Rack::Response
-    class << self
-      alias [] new
-    end
-
-    # Headers
-    attr_reader :original_headers, :cookies
-
-    # Errors
-    attr_accessor :errors
-
-    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)
-      body =~ other
-    end
-
-    def match(other)
-      body.match other
-    end
-
-    def body
-      # FIXME: apparently users of MockResponse expect the return value of
-      # MockResponse#body to be a string.  However, the real response object
-      # returns the body as a list.
-      #
-      # See spec_showstatus.rb:
-      #
-      #   should "not replace existing messages" do
-      #     ...
-      #     res.body.should == "foo!"
-      #   end
-      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
+require_relative 'mock_request'
diff --git a/lib/rack/mock_request.rb b/lib/rack/mock_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b6d7ef4fea53b10e9f9186505940c707c44b2651
--- /dev/null
+++ b/lib/rack/mock_request.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require 'uri'
+require 'stringio'
+
+require_relative 'constants'
+require_relative 'mock_response'
+
+module Rack
+  # Rack::MockRequest helps testing your Rack application without
+  # actually using HTTP.
+  #
+  # After performing a request on a URL with get/post/put/patch/delete, it
+  # returns a MockResponse with useful helper methods for effective
+  # testing.
+  #
+  # You can pass a hash with additional configuration to the
+  # get/post/put/patch/delete.
+  # <tt>:input</tt>:: A String or IO-like to be used as rack.input.
+  # <tt>:fatal</tt>:: Raise a FatalWarning if the app writes to rack.errors.
+  # <tt>:lint</tt>:: If true, wrap the application in a Rack::Lint.
+
+  class MockRequest
+    class FatalWarning < RuntimeError
+    end
+
+    class FatalWarner
+      def puts(warning)
+        raise FatalWarning, warning
+      end
+
+      def write(warning)
+        raise FatalWarning, warning
+      end
+
+      def flush
+      end
+
+      def string
+        ""
+      end
+    end
+
+    DEFAULT_ENV = {
+      RACK_INPUT        => StringIO.new,
+      RACK_ERRORS       => StringIO.new,
+    }.freeze
+
+    def initialize(app)
+      @app = app
+    end
+
+    # Make a GET request and return a MockResponse. See #request.
+    def get(uri, opts = {})     request(GET, uri, opts)     end
+    # Make a POST request and return a MockResponse. See #request.
+    def post(uri, opts = {})    request(POST, uri, opts)    end
+    # Make a PUT request and return a MockResponse. See #request.
+    def put(uri, opts = {})     request(PUT, uri, opts)     end
+    # Make a PATCH request and return a MockResponse. See #request.
+    def patch(uri, opts = {})   request(PATCH, uri, opts)   end
+    # Make a DELETE request and return a MockResponse. See #request.
+    def delete(uri, opts = {})  request(DELETE, uri, opts)  end
+    # Make a HEAD request and return a MockResponse. See #request.
+    def head(uri, opts = {})    request(HEAD, uri, opts)    end
+    # Make an OPTIONS request and return a MockResponse. See #request.
+    def options(uri, opts = {}) request(OPTIONS, uri, opts) end
+
+    # Make a request using the given request method for the given
+    # uri to the rack application and return a MockResponse.
+    # Options given are passed to MockRequest.env_for.
+    def request(method = GET, uri = "", opts = {})
+      env = self.class.env_for(uri, opts.merge(method: method))
+
+      if opts[:lint]
+        app = Rack::Lint.new(@app)
+      else
+        app = @app
+      end
+
+      errors = env[RACK_ERRORS]
+      status, headers, body = app.call(env)
+      MockResponse.new(status, headers, body, errors)
+    ensure
+      body.close if body.respond_to?(:close)
+    end
+
+    # For historical reasons, we're pinning to RFC 2396.
+    # URI::Parser = URI::RFC2396_Parser
+    def self.parse_uri_rfc2396(uri)
+      @parser ||= URI::Parser.new
+      @parser.parse(uri)
+    end
+
+    # Return the Rack environment used for a request to +uri+.
+    # All options that are strings are added to the returned environment.
+    # Options:
+    # :fatal :: Whether to raise an exception if request outputs to rack.errors
+    # :input :: The rack.input to set
+    # :http_version :: The SERVER_PROTOCOL to set
+    # :method :: The HTTP request method to use
+    # :params :: The params to use
+    # :script_name :: The SCRIPT_NAME to set
+    def self.env_for(uri = "", opts = {})
+      uri = parse_uri_rfc2396(uri)
+      uri.path = "/#{uri.path}" unless uri.path[0] == ?/
+
+      env = DEFAULT_ENV.dup
+
+      env[REQUEST_METHOD]  = (opts[:method] ? opts[:method].to_s.upcase : GET).b
+      env[SERVER_NAME]     = (uri.host || "example.org").b
+      env[SERVER_PORT]     = (uri.port ? uri.port.to_s : "80").b
+      env[SERVER_PROTOCOL] = opts[:http_version] || 'HTTP/1.1'
+      env[QUERY_STRING]    = (uri.query.to_s).b
+      env[PATH_INFO]       = (uri.path).b
+      env[RACK_URL_SCHEME] = (uri.scheme || "http").b
+      env[HTTPS]           = (env[RACK_URL_SCHEME] == "https" ? "on" : "off").b
+
+      env[SCRIPT_NAME] = opts[:script_name] || ""
+
+      if opts[:fatal]
+        env[RACK_ERRORS] = FatalWarner.new
+      else
+        env[RACK_ERRORS] = StringIO.new
+      end
+
+      if params = opts[:params]
+        if env[REQUEST_METHOD] == GET
+          params = Utils.parse_nested_query(params) if params.is_a?(String)
+          params.update(Utils.parse_nested_query(env[QUERY_STRING]))
+          env[QUERY_STRING] = Utils.build_nested_query(params)
+        elsif !opts.has_key?(:input)
+          opts["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
+          if params.is_a?(Hash)
+            if data = Rack::Multipart.build_multipart(params)
+              opts[:input] = data
+              opts["CONTENT_LENGTH"] ||= data.length.to_s
+              opts["CONTENT_TYPE"] = "multipart/form-data; boundary=#{Rack::Multipart::MULTIPART_BOUNDARY}"
+            else
+              opts[:input] = Utils.build_nested_query(params)
+            end
+          else
+            opts[:input] = params
+          end
+        end
+      end
+
+      opts[:input] ||= String.new
+      if String === opts[:input]
+        rack_input = StringIO.new(opts[:input])
+      else
+        rack_input = opts[:input]
+      end
+
+      rack_input.set_encoding(Encoding::BINARY)
+      env[RACK_INPUT] = rack_input
+
+      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
+      }
+
+      env
+    end
+  end
+end
diff --git a/lib/rack/mock_response.rb b/lib/rack/mock_response.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c70bb711a92635da46f77c662dc1eeff77d3fe9
--- /dev/null
+++ b/lib/rack/mock_response.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'cgi/cookie'
+require 'time'
+
+require_relative 'response'
+
+module Rack
+  # Rack::MockResponse provides useful helpers for testing your apps.
+  # Usually, you don't create the MockResponse on your own, but use
+  # MockRequest.
+
+  class MockResponse < Rack::Response
+    class << self
+      alias [] new
+    end
+
+    # Headers
+    attr_reader :original_headers, :cookies
+
+    # Errors
+    attr_accessor :errors
+
+    def initialize(status, headers, body, errors = nil)
+      @original_headers = headers
+
+      if errors
+        @errors = errors.string if errors.respond_to?(:string)
+      else
+        @errors = ""
+      end
+
+      super(body, status, headers)
+
+      @cookies = parse_cookies_from_header
+      buffered_body!
+    end
+
+    def =~(other)
+      body =~ other
+    end
+
+    def match(other)
+      body.match other
+    end
+
+    def body
+      return @buffered_body if defined?(@buffered_body)
+
+      # FIXME: apparently users of MockResponse expect the return value of
+      # MockResponse#body to be a string.  However, the real response object
+      # returns the body as a list.
+      #
+      # See spec_showstatus.rb:
+      #
+      #   should "not replace existing messages" do
+      #     ...
+      #     res.body.should == "foo!"
+      #   end
+      buffer = @buffered_body = String.new
+
+      @body.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 headers.has_key? 'set-cookie'
+        set_cookie_header = headers.fetch('set-cookie')
+        Array(set_cookie_header).each do |header_value|
+          header_value.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
+      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.drop(1).each do |bit|
+        if bit.include? '='
+          cookie_attribute, attribute_value = bit.split('=', 2)
+          cookie_attributes.store(cookie_attribute.strip.downcase, attribute_value.strip)
+        end
+        if bit.include? 'secure'
+          cookie_attributes.store('secure', true)
+        end
+      end
+
+      if cookie_attributes.key? 'max-age'
+        cookie_attributes.store('expires', Time.now + cookie_attributes['max-age'].to_i)
+      elsif cookie_attributes.key? 'expires'
+        cookie_attributes.store('expires', Time.httpdate(cookie_attributes['expires']))
+      end
+
+      cookie_attributes
+    end
+
+  end
+end
diff --git a/lib/rack/multipart.rb b/lib/rack/multipart.rb
index fdae808a83fdc329e086e1b83194916eddf1ef8d..3347662acc3fb00acfaa29c01754251fa99c0e3b 100644
--- a/lib/rack/multipart.rb
+++ b/lib/rack/multipart.rb
@@ -1,64 +1,44 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+
 require_relative 'multipart/parser'
+require_relative 'multipart/generator'
 
 module Rack
   # A multipart form data parser, adapted from IOWA.
   #
   # Usually, Rack::Request#POST takes care of calling this.
   module Multipart
-    autoload :UploadedFile, 'rack/multipart/uploaded_file'
-    autoload :Generator, 'rack/multipart/generator'
-
-    EOL = "\r\n"
     MULTIPART_BOUNDARY = "AaB03x"
-    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
-    TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
-    CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
-    VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
-    BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
-    MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/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{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
-    ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
-    SECTION = /\*[0-9]+/
-    REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
-    REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
-    EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
-    EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
-    EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
-    EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
-    EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
-    EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
-    EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
-    DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
-    RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
 
     class << self
       def parse_multipart(env, params = Rack::Utils.default_query_parser)
-        extract_multipart Rack::Request.new(env), params
-      end
+        io = env[RACK_INPUT]
+
+        if content_length = env['CONTENT_LENGTH']
+          content_length = content_length.to_i
+        end
 
-      def extract_multipart(req, params = Rack::Utils.default_query_parser)
-        io = req.get_header(RACK_INPUT)
-        io.rewind
-        content_length = req.content_length
-        content_length = content_length.to_i if content_length
+        content_type = env['CONTENT_TYPE']
 
-        tempfile = req.get_header(RACK_MULTIPART_TEMPFILE_FACTORY) || Parser::TEMPFILE_FACTORY
-        bufsize = req.get_header(RACK_MULTIPART_BUFFER_SIZE) || Parser::BUFSIZE
+        tempfile = env[RACK_MULTIPART_TEMPFILE_FACTORY] || Parser::TEMPFILE_FACTORY
+        bufsize = env[RACK_MULTIPART_BUFFER_SIZE] || Parser::BUFSIZE
 
-        info = Parser.parse io, content_length, req.get_header('CONTENT_TYPE'), tempfile, bufsize, params
-        req.set_header(RACK_TEMPFILES, info.tmp_files)
-        info.params
+        info = Parser.parse(io, content_length, content_type, tempfile, bufsize, params)
+        env[RACK_TEMPFILES] = info.tmp_files
+
+        return info.params
+      end
+
+      def extract_multipart(request, params = Rack::Utils.default_query_parser)
+        parse_multipart(request.env)
       end
 
       def build_multipart(params, first = true)
         Generator.new(params, first).dump
       end
     end
-
   end
 end
diff --git a/lib/rack/multipart/generator.rb b/lib/rack/multipart/generator.rb
index f798a98c5101253082d62041d8ab10a4b32d3b0a..30d7f51dd1983ffce6b70ffda01ae42607321411 100644
--- a/lib/rack/multipart/generator.rb
+++ b/lib/rack/multipart/generator.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative 'uploaded_file'
+
 module Rack
   module Multipart
     class Generator
@@ -74,12 +76,12 @@ module Rack
 
       def content_for_tempfile(io, file, name)
         length = ::File.stat(file.path).size if file.path
-        filename = "; filename=\"#{Utils.escape(file.original_filename)}\"" if file.original_filename
+        filename = "; filename=\"#{Utils.escape_path(file.original_filename)}\""
 <<-EOF
 --#{MULTIPART_BOUNDARY}\r
-Content-Disposition: form-data; name="#{name}"#{filename}\r
-Content-Type: #{file.content_type}\r
-#{"Content-Length: #{length}\r\n" if length}\r
+content-disposition: form-data; name="#{name}"#{filename}\r
+content-type: #{file.content_type}\r
+#{"content-length: #{length}\r\n" if length}\r
 #{io.read}\r
 EOF
       end
@@ -87,7 +89,7 @@ EOF
       def content_for_other(file, name)
 <<-EOF
 --#{MULTIPART_BOUNDARY}\r
-Content-Disposition: form-data; name="#{name}"\r
+content-disposition: form-data; name="#{name}"\r
 \r
 #{file}\r
 EOF
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 0fc185603101c477352e9d077148478a82deac29..2469459d7ffb3f854611c4ebc06c2bf5e9d00890 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -2,22 +2,54 @@
 
 require 'strscan'
 
+require_relative '../utils'
+
 module Rack
   module Multipart
     class MultipartPartLimitError < Errno::EMFILE; end
+
     class MultipartTotalPartLimitError < StandardError; end
 
-    class Parser
-      (require_relative '../core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
+    # Use specific error class when parsing multipart request
+    # that ends early.
+    class EmptyContentError < ::EOFError; end
+
+    # Base class for multipart exceptions that do not subclass from
+    # other exception classes for backwards compatibility.
+    class Error < StandardError; end
+
+    EOL = "\r\n"
+    MULTIPART = %r|\Amultipart/.*boundary=\"?([^\";,]+)\"?|ni
+    TOKEN = /[^\s()<>,;:\\"\/\[\]?=]+/
+    CONDISP = /Content-Disposition:\s*#{TOKEN}\s*/i
+    VALUE = /"(?:\\"|[^"])*"|#{TOKEN}/
+    BROKEN = /^#{CONDISP}.*;\s*filename=(#{VALUE})/i
+    MULTIPART_CONTENT_TYPE = /Content-Type: (.*)#{EOL}/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{[^ \x00-\x1f\x7f)(><@,;:\\"/\[\]?='*%]}
+    ATTRIBUTE = /#{ATTRIBUTE_CHAR}+/
+    SECTION = /\*[0-9]+/
+    REGULAR_PARAMETER_NAME = /#{ATTRIBUTE}#{SECTION}?/
+    REGULAR_PARAMETER = /(#{REGULAR_PARAMETER_NAME})=(#{VALUE})/
+    EXTENDED_OTHER_NAME = /#{ATTRIBUTE}\*[1-9][0-9]*\*/
+    EXTENDED_OTHER_VALUE = /%[0-9a-fA-F]{2}|#{ATTRIBUTE_CHAR}/
+    EXTENDED_OTHER_PARAMETER = /(#{EXTENDED_OTHER_NAME})=(#{EXTENDED_OTHER_VALUE}*)/
+    EXTENDED_INITIAL_NAME = /#{ATTRIBUTE}(?:\*0)?\*/
+    EXTENDED_INITIAL_VALUE = /[a-zA-Z0-9\-]*'[a-zA-Z0-9\-]*'#{EXTENDED_OTHER_VALUE}*/
+    EXTENDED_INITIAL_PARAMETER = /(#{EXTENDED_INITIAL_NAME})=(#{EXTENDED_INITIAL_VALUE})/
+    EXTENDED_PARAMETER = /#{EXTENDED_INITIAL_PARAMETER}|#{EXTENDED_OTHER_PARAMETER}/
+    DISPPARM = /;\s*(?:#{REGULAR_PARAMETER}|#{EXTENDED_PARAMETER})\s*/
+    RFC2183 = /^#{CONDISP}(#{DISPPARM})+$/i
 
+    class Parser
       BUFSIZE = 1_048_576
       TEXT_PLAIN = "text/plain"
       TEMPFILE_FACTORY = lambda { |filename, content_type|
         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
@@ -39,16 +71,12 @@ module Rack
           if str
             @cursor += str.bytesize
           else
-            # Raise an error for mismatching Content-Length and actual contents
+            # Raise an error for mismatching content-length and actual contents
             raise EOFError, "bad content body"
           end
 
           str
         end
-
-        def rewind
-          @io.rewind
-        end
       end
 
       MultipartInfo = Struct.new :params, :tmp_files
@@ -67,18 +95,17 @@ module Rack
         boundary = parse_boundary content_type
         return EMPTY unless boundary
 
+        if boundary.length > 70
+          # RFC 1521 Section 7.2.1 imposes a 70 character maximum for the boundary.
+          # Most clients use no more than 55 characters.
+          raise Error, "multipart boundary size too large (#{boundary.length} characters)"
+        end
+
         io = BoundedIO.new(io, content_length) if content_length
-        outbuf = String.new
 
         parser = new(boundary, tmpfile, bufsize, qp)
-        parser.on_read io.read(bufsize, outbuf)
+        parser.parse(io)
 
-        loop do
-          break if parser.state == :DONE
-          parser.on_read io.read(bufsize, outbuf)
-        end
-
-        io.rewind
         parser.result
       end
 
@@ -178,32 +205,46 @@ module Rack
       def initialize(boundary, tempfile, bufsize, query_parser)
         @query_parser   = query_parser
         @params         = query_parser.make_params
-        @boundary       = "--#{boundary}"
         @bufsize        = bufsize
 
-        @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
+        @body_regex = /(?:#{EOL}|\A)--#{Regexp.quote(boundary)}(?:#{EOL}|--)/m
+        @rx_max_size = boundary.bytesize + 6 # (\r\n-- at start, either \r\n or -- at finish)
         @head_regex = /(.*?#{EOL})#{EOL}/m
       end
 
-      def on_read(content)
-        handle_empty_content!(content)
-        @sbuf.concat content
-        run_parser
+      def parse(io)
+        outbuf = String.new
+        read_data(io, outbuf)
+
+        loop do
+          status =
+            case @state
+            when :FAST_FORWARD
+              handle_fast_forward
+            when :CONSUME_TOKEN
+              handle_consume_token
+            when :MIME_HEAD
+              handle_mime_head
+            when :MIME_BODY
+              handle_mime_body
+            else # when :DONE
+              return
+            end
+
+          read_data(io, outbuf) if status == :want_read
+        end
       end
 
       def result
         @collector.each do |part|
           part.get_data do |data|
             tag_multipart_encoding(part.filename, part.content_type, part.name, data)
-            @query_parser.normalize_params(@params, part.name, data, @query_parser.param_depth_limit)
+            @query_parser.normalize_params(@params, part.name, data)
           end
         end
         MultipartInfo.new @params.to_params_hash, @collector.find_all(&:file?).map(&:body)
@@ -211,29 +252,38 @@ module Rack
 
       private
 
-      def run_parser
-        loop do
-          case @state
-          when :FAST_FORWARD
-            break if handle_fast_forward == :want_read
-          when :CONSUME_TOKEN
-            break if handle_consume_token == :want_read
-          when :MIME_HEAD
-            break if handle_mime_head == :want_read
-          when :MIME_BODY
-            break if handle_mime_body == :want_read
-          when :DONE
-            break
-          end
-        end
+      def dequote(str) # From WEBrick::HTTPUtils
+        ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
+        ret.gsub!(/\\(.)/, "\\1")
+        ret
       end
 
+      def read_data(io, outbuf)
+        content = io.read(@bufsize, outbuf)
+        handle_empty_content!(content)
+        @sbuf.concat(content)
+      end
+
+      # This handles the initial parser state.  We read until we find the starting
+      # boundary, then we can transition to the next state. If we find the ending
+      # boundary, this is an invalid multipart upload, but keep scanning for opening
+      # boundary in that case. If no boundary found, we need to keep reading data
+      # and retry. It's highly unlikely the initial read will not consume the
+      # boundary.  The client would have to deliberately craft a response
+      # with the opening boundary beyond the buffer size for that to happen.
       def handle_fast_forward
-        if consume_boundary
-          @state = :MIME_HEAD
-        else
-          raise EOFError, "bad content body" if @sbuf.rest_size >= @bufsize
-          :want_read
+        while true
+          case consume_boundary
+          when :BOUNDARY
+            # found opening boundary, transition to next state
+            @state = :MIME_HEAD
+            return
+          when :END_BOUNDARY
+            # invalid multipart upload, but retry for opening boundary
+          else
+            # no boundary found, keep reading data
+            return :want_read
+          end
         end
       end
 
@@ -252,7 +302,7 @@ module Rack
           head = @sbuf[1]
           content_type = head[MULTIPART_CONTENT_TYPE, 1]
           if name = head[MULTIPART_CONTENT_DISPOSITION, 1]
-            name = Rack::Auth::Digest::Params::dequote(name)
+            name = dequote(name)
           else
             name = head[MULTIPART_CONTENT_ID, 1]
           end
@@ -289,15 +339,16 @@ module Rack
         end
       end
 
-      def full_boundary; @full_boundary; end
-
+      # Scan until the we find the start or end of the boundary.
+      # If we find it, return the appropriate symbol for the start or
+      # end of the boundary.  If we don't find the start or end of the
+      # boundary, clear the buffer and return nil.
       def consume_boundary
-        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 @sbuf.eos?
+        if read_buffer = @sbuf.scan_until(@body_regex)
+          read_buffer.end_with?(EOL) ? :BOUNDARY : :END_BOUNDARY
+        else
+          @sbuf.terminate
+          nil
         end
       end
 
@@ -307,10 +358,10 @@ module Rack
         when RFC2183
           params = Hash[*head.scan(DISPPARM).flat_map(&:compact)]
 
-          if filename = params['filename']
-            filename = $1 if filename =~ /^"(.*)"$/
-          elsif filename = params['filename*']
+          if filename = params['filename*']
             encoding, _, filename = filename.split("'", 3)
+          elsif filename = params['filename']
+            filename = $1 if filename =~ /^"(.*)"$/
           end
         when BROKEN
           filename = $1
@@ -337,6 +388,7 @@ module Rack
       end
 
       CHARSET = "charset"
+      deprecate_constant :CHARSET
 
       def tag_multipart_encoding(filename, content_type, name, body)
         name = name.to_s
@@ -357,7 +409,13 @@ module Rack
               k.strip!
               v.strip!
               v = v[1..-2] if v.start_with?('"') && v.end_with?('"')
-              encoding = Encoding.find v if k == CHARSET
+              if k == "charset"
+                encoding = begin
+                  Encoding.find v
+                rescue ArgumentError
+                  Encoding::BINARY
+                end
+              end
             end
           end
         end
@@ -368,7 +426,7 @@ module Rack
 
       def handle_empty_content!(content)
         if content.nil? || content.empty?
-          raise EOFError
+          raise EmptyContentError
         end
       end
     end
diff --git a/lib/rack/multipart/uploaded_file.rb b/lib/rack/multipart/uploaded_file.rb
index 9eaf691277b1e98f0c2439b5ebe52cfa7bc94e21..2782e44c73afcb64d4a8dc7a0e6259740ee53288 100644
--- a/lib/rack/multipart/uploaded_file.rb
+++ b/lib/rack/multipart/uploaded_file.rb
@@ -1,8 +1,12 @@
 # frozen_string_literal: true
 
+require 'tempfile'
+require 'fileutils'
+
 module Rack
   module Multipart
     class UploadedFile
+
       # The filename, *not* including the path, of the "uploaded" file
       attr_reader :original_filename
 
diff --git a/lib/rack/null_logger.rb b/lib/rack/null_logger.rb
index 3eff73d683fd336dca8f436b9f3cb7a4a6eee5cd..52fc125c3dc4cf9e21e551cf15f0836d0b5c4dd3 100644
--- a/lib/rack/null_logger.rb
+++ b/lib/rack/null_logger.rb
@@ -1,5 +1,7 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+
 module Rack
   class NullLogger
     def initialize(app)
@@ -22,6 +24,11 @@ module Rack
     def warn? ;  end
     def error? ; end
     def fatal? ; end
+    def debug! ; end
+    def error! ; end
+    def fatal! ; end
+    def info! ; end
+    def warn! ; end
     def level ; end
     def progname ; end
     def datetime_format ; end
@@ -34,6 +41,8 @@ module Rack
     def sev_threshold=(sev_threshold); end
     def close ; end
     def add(severity, message = nil, progname = nil, &block); end
+    def log(severity, message = nil, progname = nil, &block); end
     def <<(msg); end
+    def reopen(logdev = nil); end
   end
 end
diff --git a/lib/rack/query_parser.rb b/lib/rack/query_parser.rb
index 1c3923c32ff794175c165b3cf2cfc578638ca567..9e8434cf3a9ccb743f6974a5b57fda4f8cef8a52 100644
--- a/lib/rack/query_parser.rb
+++ b/lib/rack/query_parser.rb
@@ -1,10 +1,10 @@
 # frozen_string_literal: true
 
+require 'uri'
+
 module Rack
   class QueryParser
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
-    DEFAULT_SEP = /[&;] */n
+    DEFAULT_SEP = /[&] */n
     COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
 
     # ParameterTypeError is the error that is raised when incoming structural
@@ -20,29 +20,35 @@ module Rack
     # nested over the specified limit.
     class ParamsTooDeepError < RangeError; end
 
-    def self.make_default(key_space_limit, param_depth_limit)
-      new Params, key_space_limit, param_depth_limit
+    def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
+      unless not_deprecated
+        warn("`first argument `key_space limit` is deprecated and no longer has an effect. Please call with only one argument, which will be required in a future version of Rack", uplevel: 1)
+      end
+
+      new Params, param_depth_limit
     end
 
-    attr_reader :key_space_limit, :param_depth_limit
+    attr_reader :param_depth_limit
+
+    def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
+      unless not_deprecated
+        warn("`second argument `key_space limit` is deprecated and no longer has an effect. Please call with only two arguments, which will be required in a future version of Rack", uplevel: 1)
+      end
 
-    def initialize(params_class, key_space_limit, param_depth_limit)
       @params_class = params_class
-      @key_space_limit = key_space_limit
       @param_depth_limit = param_depth_limit
     end
 
     # Stolen from Mongrel, with some small modifications:
-    # Parses a query string by breaking it up at the '&'
-    # and ';' characters.  You can also use this to parse
-    # cookies by changing the characters used in the second
-    # parameter (which defaults to '&;').
-    def parse_query(qs, d = nil, &unescaper)
+    # Parses a query string by breaking it up at the '&'.  You can also use this
+    # to parse cookies by changing the characters used in the second parameter
+    # (which defaults to '&').
+    def parse_query(qs, separator = nil, &unescaper)
       unescaper ||= method(:unescape)
 
       params = make_params
 
-      (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
+      (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
         next if p.empty?
         k, v = p.split('=', 2).map!(&unescaper)
 
@@ -65,14 +71,14 @@ module Rack
     # query strings with parameters of conflicting types, in this case a
     # ParameterTypeError is raised. Users are encouraged to return a 400 in this
     # case.
-    def parse_nested_query(qs, d = nil)
+    def parse_nested_query(qs, separator = nil)
       params = make_params
 
       unless qs.nil? || qs.empty?
-        (qs || '').split(d ? (COMMON_SEP[d] || /[#{d}] */n) : DEFAULT_SEP).each do |p|
+        (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
           k, v = p.split('=', 2).map! { |s| unescape(s) }
 
-          normalize_params(params, k, v, param_depth_limit)
+          _normalize_params(params, k, v, 0)
         end
       end
 
@@ -83,58 +89,87 @@ module Rack
 
     # normalize_params recursively expands parameters into structural types. If
     # the structural types represented by two different parameter names are in
-    # conflict, a ParameterTypeError is raised.
-    def normalize_params(params, name, v, depth)
-      raise ParamsTooDeepError if depth <= 0
-
-      name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
-      k = $1 || ''
-      after = $' || ''
+    # conflict, a ParameterTypeError is raised.  The depth argument is deprecated
+    # and should no longer be used, it is kept for backwards compatibility with
+    # earlier versions of rack.
+    def normalize_params(params, name, v, _depth=nil)
+      _normalize_params(params, name, v, 0)
+    end
 
-      if k.empty?
-        if !v.nil? && name == "[]"
-          return Array(v)
+    private def _normalize_params(params, name, v, depth)
+      raise ParamsTooDeepError if depth >= param_depth_limit
+
+      if !name
+        # nil name, treat same as empty string (required by tests)
+        k = after = ''
+      elsif depth == 0
+        # Start of parsing, don't treat [] or [ at start of string specially
+        if start = name.index('[', 1)
+          # Start of parameter nesting, use part before brackets as key
+          k = name[0, start]
+          after = name[start, name.length]
         else
-          return
+          # Plain parameter with no nesting
+          k = name
+          after = ''
         end
+      elsif name.start_with?('[]')
+        # Array nesting
+        k = '[]'
+        after = name[2, name.length]
+      elsif name.start_with?('[') && (start = name.index(']', 1))
+        # Hash nesting, use the part inside brackets as the key
+        k = name[1, start-1]
+        after = name[start+1, name.length]
+      else
+        # Probably malformed input, nested but not starting with [
+        # treat full name as key for backwards compatibility.
+        k = name
+        after = ''
       end
 
+      return if k.empty?
+
       if after == ''
-        params[k] = v
+        if k == '[]' && depth != 0
+          return [v]
+        else
+          params[k] = v
+        end
       elsif after == "["
         params[name] = v
       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
-      elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
-        child_key = $1
+      elsif after.start_with?('[]')
+        # Recognize x[][y] (hash inside array) parameters
+        unless after[2] == '[' && after.end_with?(']') && (child_key = after[3, after.length-4]) && !child_key.empty? && !child_key.index('[') && !child_key.index(']')
+          # Handle other nested array parameters
+          child_key = after[2, after.length]
+        end
         params[k] ||= []
         raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
         if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
-          normalize_params(params[k].last, child_key, v, depth - 1)
+          _normalize_params(params[k].last, child_key, v, depth + 1)
         else
-          params[k] << normalize_params(make_params, child_key, v, depth - 1)
+          params[k] << _normalize_params(make_params, child_key, v, depth + 1)
         end
       else
         params[k] ||= make_params
         raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
-        params[k] = normalize_params(params[k], after, v, depth - 1)
+        params[k] = _normalize_params(params[k], after, v, depth + 1)
       end
 
       params
     end
 
     def make_params
-      @params_class.new @key_space_limit
-    end
-
-    def new_space_limit(key_space_limit)
-      self.class.new @params_class, key_space_limit, param_depth_limit
+      @params_class.new
     end
 
     def new_depth_limit(param_depth_limit)
-      self.class.new @params_class, key_space_limit, param_depth_limit
+      self.class.new @params_class, param_depth_limit
     end
 
     private
@@ -155,13 +190,12 @@ module Rack
       true
     end
 
-    def unescape(s)
-      Utils.unescape(s)
+    def unescape(string, encoding = Encoding::UTF_8)
+      URI.decode_www_form_component(string, encoding)
     end
 
     class Params
-      def initialize(limit)
-        @limit  = limit
+      def initialize
         @size   = 0
         @params = {}
       end
@@ -171,8 +205,6 @@ module Rack
       end
 
       def []=(key, value)
-        @size += key.size if key && !@params.key?(key)
-        raise ParamsTooDeepError, 'exceeded available parameter key space' if @size > @limit
         @params[key] = value
       end
 
diff --git a/lib/rack/recursive.rb b/lib/rack/recursive.rb
index 6971cbfd69dd242ba5b0bb8156029057ffd26488..0945d3227814b952ddf4993590569c84f04ef8c6 100644
--- a/lib/rack/recursive.rb
+++ b/lib/rack/recursive.rb
@@ -2,6 +2,8 @@
 
 require 'uri'
 
+require_relative 'constants'
+
 module Rack
   # Rack::ForwardRequest gets caught by Rack::Recursive and redirects
   # the current request to the app at +url+.
diff --git a/lib/rack/reloader.rb b/lib/rack/reloader.rb
index 2f17f50b836099a44ba36b398edd82b60e60ba74..a15064acc498b2b02e29d0399b514f0182912e38 100644
--- a/lib/rack/reloader.rb
+++ b/lib/rack/reloader.rb
@@ -22,8 +22,6 @@ 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
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
     def initialize(app, cooldown = 10, backend = Stat)
       @app = app
       @cooldown = cooldown
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index fea984590be4f5f1d0ad047871ccb7f9a8df899b..99a33aa1ec48f74b8a7a1e399ea61ad591919210 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -1,5 +1,9 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'media_type'
+
 module Rack
   # Rack::Request provides a convenient interface to a Rack
   # environment.  It is stateless, the environment +env+ passed to the
@@ -10,22 +14,54 @@ module Rack
   #   req.params["data"]
 
   class Request
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
     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
+      # The priority when checking forwarded headers. The default
+      # is <tt>[:forwarded, :x_forwarded]</tt>, which means, check the
+      # +Forwarded+ header first, followed by the appropriate
+      # <tt>X-Forwarded-*</tt> header.  You can revert the priority by
+      # reversing the priority, or remove checking of either
+      # or both headers by removing elements from the array.
+      #
+      # This should be set as appropriate in your environment
+      # based on what reverse proxies are in use.  If you are not
+      # using reverse proxies, you should probably use an empty
+      # array.
+      attr_accessor :forwarded_priority
+
+      # The priority when checking either the <tt>X-Forwarded-Proto</tt>
+      # or <tt>X-Forwarded-Scheme</tt> header for the forwarded protocol.
+      # The default is <tt>[:proto, :scheme]</tt>, to try the
+      # <tt>X-Forwarded-Proto</tt> header before the
+      # <tt>X-Forwarded-Scheme</tt> header.  Rack 2 had behavior
+      # similar to <tt>[:scheme, :proto]</tt>.  You can remove either or
+      # both of the entries in array to ignore that respective header.
+      attr_accessor :x_forwarded_proto_priority
     end
 
+    @forwarded_priority = [:forwarded, :x_forwarded]
+    @x_forwarded_proto_priority = [:proto, :scheme]
+
+    valid_ipv4_octet = /\.(25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])/
+
+    trusted_proxies = Regexp.union(
+      /\A127#{valid_ipv4_octet}{3}\z/,                          # localhost IPv4 range 127.x.x.x, per RFC-3330
+      /\A::1\z/,                                                # localhost IPv6 ::1
+      /\Af[cd][0-9a-f]{2}(?::[0-9a-f]{0,4}){0,7}\z/i,           # private IPv6 range fc00 .. fdff
+      /\A10#{valid_ipv4_octet}{3}\z/,                           # private IPv4 range 10.x.x.x
+      /\A172\.(1[6-9]|2[0-9]|3[01])#{valid_ipv4_octet}{2}\z/,   # private IPv4 range 172.16.0.0 .. 172.31.255.255
+      /\A192\.168#{valid_ipv4_octet}{2}\z/,                     # private IPv4 range 192.168.x.x
+      /\Alocalhost\z|\Aunix(\z|:)/i,                            # localhost hostname, and unix domain sockets
+    )
+
+    self.ip_filter = lambda { |ip| trusted_proxies.match?(ip) }
+
+    ALLOWED_SCHEMES = %w(https http wss ws).freeze
+
     def initialize(env)
+      @env = env
       @params = nil
-      super(env)
     end
 
     def params
@@ -49,6 +85,8 @@ module Rack
 
       def initialize(env)
         @env = env
+        # This module is included at least in `ActionDispatch::Request`
+        # The call to `super()` allows additional mixed-in initializers are called
         super()
       end
 
@@ -135,6 +173,8 @@ module Rack
       # The contents of the host/:authority header sent to the proxy.
       HTTP_X_FORWARDED_HOST = 'HTTP_X_FORWARDED_HOST'
 
+      HTTP_FORWARDED          = 'HTTP_FORWARDED'
+
       # The value of the scheme sent to the proxy.
       HTTP_X_FORWARDED_SCHEME = 'HTTP_X_FORWARDED_SCHEME'
 
@@ -144,7 +184,7 @@ module Rack
       # The port used to connect to the proxy.
       HTTP_X_FORWARDED_PORT = 'HTTP_X_FORWARDED_PORT'
 
-      # Another way for specifing https scheme was used.
+      # Another way for specifying https scheme was used.
       HTTP_X_FORWARDED_SSL = 'HTTP_X_FORWARDED_SSL'
 
       def body;            get_header(RACK_INPUT)                         end
@@ -159,7 +199,6 @@ module Rack
       def content_length;  get_header('CONTENT_LENGTH')                   end
       def logger;          get_header(RACK_LOGGER)                        end
       def user_agent;      get_header('HTTP_USER_AGENT')                  end
-      def multithread?;    get_header(RACK_MULTITHREAD)                   end
 
       # the referer of the client
       def referer;         get_header('HTTP_REFERER')                     end
@@ -248,9 +287,7 @@ module Rack
       end
 
       def server_port
-        if port = get_header(SERVER_PORT)
-          Integer(port)
-        end
+        get_header(SERVER_PORT)
       end
 
       def cookies
@@ -307,44 +344,67 @@ module Rack
 
       def port
         if authority = self.authority
-          _, _, port = split_authority(self.authority)
-
-          if port
-            return port
-          end
+          _, _, port = split_authority(authority)
         end
 
-        if forwarded_port = self.forwarded_port
-          return forwarded_port.first
-        end
-
-        if scheme = self.scheme
-          if port = DEFAULT_PORTS[self.scheme]
-            return port
-          end
-        end
-
-        self.server_port
+        port || forwarded_port&.last || DEFAULT_PORTS[scheme] || server_port
       end
 
       def forwarded_for
-        if value = get_header(HTTP_X_FORWARDED_FOR)
-          split_header(value).map do |authority|
-            split_authority(wrap_ipv6(authority))[1]
+        forwarded_priority.each do |type|
+          case type
+          when :forwarded
+            if forwarded_for = get_http_forwarded(:for)
+              return(forwarded_for.map! do |authority|
+                split_authority(authority)[1]
+              end)
+            end
+          when :x_forwarded
+            if value = get_header(HTTP_X_FORWARDED_FOR)
+              return(split_header(value).map do |authority|
+                split_authority(wrap_ipv6(authority))[1]
+              end)
+            end
           end
         end
+
+        nil
       end
 
       def forwarded_port
-        if value = get_header(HTTP_X_FORWARDED_PORT)
-          split_header(value).map(&:to_i)
+        forwarded_priority.each do |type|
+          case type
+          when :forwarded
+            if forwarded = get_http_forwarded(:for)
+              return(forwarded.map do |authority|
+                split_authority(authority)[2]
+              end.compact)
+            end
+          when :x_forwarded
+            if value = get_header(HTTP_X_FORWARDED_PORT)
+              return split_header(value).map(&:to_i)
+            end
+          end
         end
+
+        nil
       end
 
       def forwarded_authority
-        if value = get_header(HTTP_X_FORWARDED_HOST)
-          wrap_ipv6(split_header(value).first)
+        forwarded_priority.each do |type|
+          case type
+          when :forwarded
+            if forwarded = get_http_forwarded(:host)
+              return forwarded.last
+            end
+          when :x_forwarded
+            if value = get_header(HTTP_X_FORWARDED_HOST)
+              return wrap_ipv6(split_header(value).last)
+            end
+          end
         end
+
+        nil
       end
 
       def ssl?
@@ -356,17 +416,15 @@ module Rack
         external_addresses = reject_trusted_ip_addresses(remote_addresses)
 
         unless external_addresses.empty?
-          return external_addresses.first
+          return external_addresses.last
         end
 
-        if forwarded_for = self.forwarded_for
-          unless forwarded_for.empty?
-            # The forwarded for addresses are ordered: client, proxy1, proxy2.
-            # So we reject all the trusted addresses (proxy*) and return the
-            # last client. Or if we trust everyone, we just return the first
-            # address.
-            return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
-          end
+        if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
+          # The forwarded for addresses are ordered: client, proxy1, proxy2.
+          # So we reject all the trusted addresses (proxy*) and return the
+          # last client. Or if we trust everyone, we just return the first
+          # address.
+          return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
         end
 
         # If all the addresses are trusted, and we aren't forwarded, just return
@@ -402,13 +460,13 @@ module Rack
       end
 
       # Determine whether the request body contains form-data by checking
-      # the request Content-Type for one of the media-types:
+      # the request content-type for one of the media-types:
       # "application/x-www-form-urlencoded" or "multipart/form-data". The
       # list of form-data media types can be modified through the
       # +FORM_DATA_MEDIA_TYPES+ array.
       #
       # A request body is also assumed to contain form-data when no
-      # Content-Type header is provided and the request_method is POST.
+      # content-type header is provided and the request_method is POST.
       def form_data?
         type = media_type
         meth = get_header(RACK_METHODOVERRIDE_ORIGINAL_METHOD) || get_header(REQUEST_METHOD)
@@ -427,7 +485,7 @@ module Rack
         if get_header(RACK_REQUEST_QUERY_STRING) == query_string
           get_header(RACK_REQUEST_QUERY_HASH)
         else
-          query_hash = parse_query(query_string, '&;')
+          query_hash = parse_query(query_string, '&')
           set_header(RACK_REQUEST_QUERY_STRING, query_string)
           set_header(RACK_REQUEST_QUERY_HASH, query_hash)
         end
@@ -438,27 +496,46 @@ module Rack
       # This method support both application/x-www-form-urlencoded and
       # multipart/form-data.
       def POST
-        if get_header(RACK_INPUT).nil?
-          raise "Missing rack.input"
-        elsif get_header(RACK_REQUEST_FORM_INPUT) == get_header(RACK_INPUT)
-          get_header(RACK_REQUEST_FORM_HASH)
-        elsif form_data? || parseable_data?
-          unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
-            form_vars = get_header(RACK_INPUT).read
-
-            # Fix for Safari Ajax postings that always append \0
-            # form_vars.sub!(/\0\z/, '') # performance replacement:
-            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, '&')
-
-            get_header(RACK_INPUT).rewind
+        if error = get_header(RACK_REQUEST_FORM_ERROR)
+          raise error.class, error.message, cause: error.cause
+        end
+
+        begin
+          rack_input = get_header(RACK_INPUT)
+
+          # If the form hash was already memoized:
+          if form_hash = get_header(RACK_REQUEST_FORM_HASH)
+            # And it was memoized from the same input:
+            if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
+              return form_hash
+            end
           end
-          set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
-          get_header RACK_REQUEST_FORM_HASH
-        else
-          {}
+
+          # Otherwise, figure out how to parse the input:
+          if rack_input.nil?
+            set_header RACK_REQUEST_FORM_INPUT, nil
+            set_header(RACK_REQUEST_FORM_HASH, {})
+          elsif form_data? || parseable_data?
+            unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
+              form_vars = get_header(RACK_INPUT).read
+
+              # Fix for Safari Ajax postings that always append \0
+              # form_vars.sub!(/\0\z/, '') # performance replacement:
+              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, '&')
+            end
+
+            set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+            get_header RACK_REQUEST_FORM_HASH
+          else
+            set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
+            set_header(RACK_REQUEST_FORM_HASH, {})
+          end
+        rescue => error
+          set_header(RACK_REQUEST_FORM_ERROR, error)
+          raise
         end
       end
 
@@ -530,9 +607,7 @@ module Rack
 
       # shortcut for <tt>request.params[key]</tt>
       def [](key)
-        if $VERBOSE
-          warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead")
-        end
+        warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
 
         params[key.to_s]
       end
@@ -541,9 +616,7 @@ module Rack
       #
       # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
       def []=(key, value)
-        if $VERBOSE
-          warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead")
-        end
+        warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
 
         params[key.to_s] = value
       end
@@ -582,6 +655,11 @@ module Rack
         end
       end
 
+      # Get an array of values set in the RFC 7239 `Forwarded` request header.
+      def get_http_forwarded(token)
+        Utils.forwarded_values(get_header(HTTP_FORWARDED))&.[](token)
+      end
+
       def query_parser
         Utils.default_query_parser
       end
@@ -598,58 +676,94 @@ module Rack
         value ? value.strip.split(/[,\s]+/) : []
       end
 
-      AUTHORITY = /^
-        # The host:
+      # ipv6 extracted from resolv stdlib, simplified
+      # to remove numbered match group creation.
+      ipv6 = Regexp.union(
+        /(?:[0-9A-Fa-f]{1,4}:){7}
+         [0-9A-Fa-f]{1,4}/x,
+        /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
+         (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?/x,
+        /(?:[0-9A-Fa-f]{1,4}:){6,6}
+         \d+\.\d+\.\d+\.\d+/x,
+        /(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
+         (?:[0-9A-Fa-f]{1,4}:)*
+         \d+\.\d+\.\d+\.\d+/x,
+        /[Ff][Ee]80
+         (?::[0-9A-Fa-f]{1,4}){7}
+         %[-0-9A-Za-z._~]+/x,
+        /[Ff][Ee]80:
+         (?:
+           (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)? ::
+           (?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?
+           |
+           :(?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?
+         )?
+         :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+/x)
+
+      AUTHORITY = /
+        \A
         (?<host>
-          # An IPv6 address:
-          (\[(?<ip6>.*)\])
-          |
-          # An IPv4 address:
-          (?<ip4>[\d\.]+)
+          # Match IPv6 as a string of hex digits and colons in square brackets
+          \[(?<address>#{ipv6})\]
           |
-          # A hostname:
-          (?<name>[a-zA-Z0-9\.\-]+)
+          # Match any other printable string (except square brackets) as a hostname
+          (?<address>[[[:graph:]&&[^\[\]]]]*?)
         )
-        # The optional port:
         (:(?<port>\d+))?
-      $/x
+        \z
+      /x
 
       private_constant :AUTHORITY
 
       def split_authority(authority)
-        if match = AUTHORITY.match(authority)
-          if address = match[:ip6]
-            return match[:host], address, match[:port]&.to_i
-          else
-            return match[:host], match[:host], match[:port]&.to_i
-          end
-        end
-
-        # Give up!
-        return authority, authority, nil
+        return [] if authority.nil?
+        return [] unless match = AUTHORITY.match(authority)
+        return match[:host], match[:address], match[:port]&.to_i
       end
 
       def reject_trusted_ip_addresses(ip_addresses)
         ip_addresses.reject { |ip| trusted_proxy?(ip) }
       end
 
+      FORWARDED_SCHEME_HEADERS = {
+        proto: HTTP_X_FORWARDED_PROTO,
+        scheme: HTTP_X_FORWARDED_SCHEME
+      }.freeze
+      private_constant :FORWARDED_SCHEME_HEADERS
       def forwarded_scheme
-        allowed_scheme(get_header(HTTP_X_FORWARDED_SCHEME)) ||
-        allowed_scheme(extract_proto_header(get_header(HTTP_X_FORWARDED_PROTO)))
+        forwarded_priority.each do |type|
+          case type
+          when :forwarded
+            if (forwarded_proto = get_http_forwarded(:proto)) &&
+               (scheme = allowed_scheme(forwarded_proto.last))
+              return scheme
+            end
+          when :x_forwarded
+            x_forwarded_proto_priority.each do |x_type|
+              if header = FORWARDED_SCHEME_HEADERS[x_type]
+                split_header(get_header(header)).reverse_each do |scheme|
+                  if allowed_scheme(scheme)
+                    return scheme
+                  end
+                end
+              end
+            end
+          end
+        end
+
+        nil
       end
 
       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
+      def forwarded_priority
+        Request.forwarded_priority
+      end
+
+      def x_forwarded_proto_priority
+        Request.x_forwarded_proto_priority
       end
     end
 
@@ -657,3 +771,7 @@ module Rack
     include Helpers
   end
 end
+
+# :nocov:
+require_relative 'multipart' unless defined?(Rack::Multipart)
+# :nocov:
diff --git a/lib/rack/response.rb b/lib/rack/response.rb
index fd6d2f5d5555064f819b5babf4a3f236a1c23b89..f24683bcb54be6831a413140fd6f00cc6af52223 100644
--- a/lib/rack/response.rb
+++ b/lib/rack/response.rb
@@ -2,6 +2,11 @@
 
 require 'time'
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'media_type'
+require_relative 'headers'
+
 module Rack
   # Rack::Response provides a convenient interface to create a Rack
   # response.
@@ -26,22 +31,45 @@ module Rack
     attr_accessor :length, :status, :body
     attr_reader :headers
 
-    # @deprecated Use {#headers} instead.
-    alias header headers
+    # Deprecated, use headers instead.
+    def header
+      warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1', uplevel: 1
+
+      headers
+    end
 
-    # Initialize the response object with the specified body, status
-    # and headers.
+    # Initialize the response object with the specified +body+, +status+
+    # and +headers+.
+    #
+    # If the +body+ is +nil+, construct an empty response object with internal
+    # buffering.
+    #
+    # If the +body+ responds to +to_str+, assume it's a string-like object and
+    # construct a buffered response object containing using that string as the
+    # initial contents of the buffer.
     #
-    # @param body [nil, #each, #to_str] the response body.
-    # @param status [Integer] the integer status as defined by the
-    # HTTP protocol RFCs.
-    # @param headers [#each] a list of key-value header pairs which
-    # conform to the HTTP protocol RFCs.
+    # Otherwise it is expected +body+ conforms to the normal requirements of a
+    # Rack response body, typically implementing one of +each+ (enumerable
+    # body) or +call+ (streaming body).
     #
-    # Providing a body which responds to #to_str is legacy behaviour.
+    # The +status+ defaults to +200+ which is the "OK" HTTP status code. You
+    # can provide any other valid status code.
+    #
+    # The +headers+ must be a +Hash+ of key-value header pairs which conform to
+    # the Rack specification for response headers. The key must be a +String+
+    # instance and the value can be either a +String+ or +Array+ instance.
     def initialize(body = nil, status = 200, headers = {})
       @status = status.to_i
-      @headers = Utils::HeaderHash[headers]
+
+      unless headers.is_a?(Hash)
+        warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1
+      end
+
+      @headers = Headers.new
+      # Convert headers input to a plain hash with lowercase keys.
+      headers.each do |k, v|
+        @headers[k] = v
+      end
 
       @writer = self.method(:append)
 
@@ -58,7 +86,7 @@ module Rack
         @length = body.to_str.bytesize
       else
         @body = body
-        @buffered = false
+        @buffered = nil # undetermined as of yet.
         @length = 0
       end
 
@@ -74,11 +102,16 @@ module Rack
       CHUNKED == get_header(TRANSFER_ENCODING)
     end
 
+    def no_entity_body?
+      # The response body is an enumerable body and it is not allowed to have an entity body.
+      @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
+    end
+    
     # Generate a response array consistent with the requirements of the SPEC.
     # @return [Array] a 3-tuple suitable of `[status, headers, body]`
     # which is suitable to be returned from the middleware `#call(env)` method.
     def finish(&block)
-      if STATUS_WITH_NO_ENTITY_BODY[status.to_i]
+      if no_entity_body?
         delete_header CONTENT_TYPE
         delete_header CONTENT_LENGTH
         close
@@ -105,7 +138,7 @@ module Rack
       end
     end
 
-    # Append to body and update Content-Length.
+    # Append to body and update content-length.
     #
     # NOTE: Do not mix #write and direct #body access!
     #
@@ -123,10 +156,22 @@ module Rack
       @block == nil && @body.empty?
     end
 
-    def has_header?(key);   headers.key? key;   end
-    def get_header(key);    headers[key];       end
-    def set_header(key, v); headers[key] = v;   end
-    def delete_header(key); headers.delete key; end
+    def has_header?(key)
+      raise ArgumentError unless key.is_a?(String)
+      @headers.key?(key)
+    end
+    def get_header(key)
+      raise ArgumentError unless key.is_a?(String)
+      @headers[key]
+    end
+    def set_header(key, value)
+      raise ArgumentError unless key.is_a?(String)
+      @headers[key] = value
+    end
+    def delete_header(key)
+      raise ArgumentError unless key.is_a?(String)
+      @headers.delete key
+    end
 
     alias :[] :get_header
     alias :[]= :set_header
@@ -150,31 +195,43 @@ module Rack
       def forbidden?;           status == 403;                        end
       def not_found?;           status == 404;                        end
       def method_not_allowed?;  status == 405;                        end
+      def not_acceptable?;      status == 406;                        end
+      def request_timeout?;     status == 408;                        end
       def precondition_failed?; status == 412;                        end
       def unprocessable?;       status == 422;                        end
 
       def redirect?;            [301, 302, 303, 307, 308].include? status; end
 
       def include?(header)
-        has_header? header
+        has_header?(header)
       end
 
       # Add a header that may have multiple values.
       #
       # Example:
-      #   response.add_header 'Vary', 'Accept-Encoding'
-      #   response.add_header 'Vary', 'Cookie'
+      #   response.add_header 'vary', 'accept-encoding'
+      #   response.add_header 'vary', 'cookie'
       #
-      #   assert_equal 'Accept-Encoding,Cookie', response.get_header('Vary')
+      #   assert_equal 'accept-encoding,cookie', response.get_header('vary')
       #
       # http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
-      def add_header(key, v)
-        if v.nil?
-          get_header key
-        elsif has_header? key
-          set_header key, "#{get_header key},#{v}"
+      def add_header(key, value)
+        raise ArgumentError unless key.is_a?(String)
+
+        if value.nil?
+          return get_header(key)
+        end
+
+        value = value.to_s
+
+        if header = get_header(key)
+          if header.is_a?(Array)
+            header << value
+          else
+            set_header(key, [header, value])
+          end
         else
-          set_header key, v
+          set_header(key, value)
         end
       end
 
@@ -202,36 +259,39 @@ module Rack
       end
 
       def location
-        get_header "Location"
+        get_header "location"
       end
 
       def location=(location)
-        set_header "Location", location
+        set_header "location", location
       end
 
       def set_cookie(key, value)
-        cookie_header = get_header SET_COOKIE
-        set_header SET_COOKIE, ::Rack::Utils.add_cookie_to_header(cookie_header, key, value)
+        add_header SET_COOKIE, Utils.set_cookie_header(key, value)
       end
 
       def delete_cookie(key, value = {})
-        set_header SET_COOKIE, ::Rack::Utils.add_remove_cookie_to_header(get_header(SET_COOKIE), key, value)
+        set_header(SET_COOKIE,
+          Utils.delete_set_cookie_header!(
+            get_header(SET_COOKIE), key, value
+          )
+        )
       end
 
       def set_cookie_header
         get_header SET_COOKIE
       end
 
-      def set_cookie_header=(v)
-        set_header SET_COOKIE, v
+      def set_cookie_header=(value)
+        set_header SET_COOKIE, value
       end
 
       def cache_control
         get_header CACHE_CONTROL
       end
 
-      def cache_control=(v)
-        set_header CACHE_CONTROL, v
+      def cache_control=(value)
+        set_header CACHE_CONTROL, value
       end
 
       # Specifies that the content shouldn't be cached. Overrides `cache!` if already called.
@@ -254,34 +314,38 @@ module Rack
         get_header ETAG
       end
 
-      def etag=(v)
-        set_header ETAG, v
+      def etag=(value)
+        set_header ETAG, value
       end
 
     protected
 
       def buffered_body!
-        return if @buffered
-
-        if @body.is_a?(Array)
-          # The user supplied body was an array:
-          @body = @body.compact
-          @body.each do |part|
-            @length += part.to_s.bytesize
+        if @buffered.nil?
+          if @body.is_a?(Array)
+            # The user supplied body was an array:
+            @body = @body.compact
+            @body.each do |part|
+              @length += part.to_s.bytesize
+            end
+          elsif @body.respond_to?(:each)
+            # Turn the user supplied body into a buffered array:
+            body = @body
+            @body = Array.new
+
+            body.each do |part|
+              @writer.call(part.to_s)
+            end
+
+            body.close if body.respond_to?(:close)
+
+            @buffered = true
+          else
+            @buffered = false
           end
-        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
-
-          body.close if body.respond_to?(:close)
         end
 
-        @buffered = true
+        return @buffered
       end
 
       def append(chunk)
@@ -309,10 +373,21 @@ module Rack
         @headers = headers
       end
 
-      def has_header?(key);   headers.key? key;   end
-      def get_header(key);    headers[key];       end
-      def set_header(key, v); headers[key] = v;   end
-      def delete_header(key); headers.delete key; end
+      def has_header?(key)
+        headers.key?(key)
+      end
+
+      def get_header(key)
+        headers[key]
+      end
+
+      def set_header(key, value)
+        headers[key] = value
+      end
+
+      def delete_header(key)
+        headers.delete(key)
+      end
     end
   end
 end
diff --git a/lib/rack/rewindable_input.rb b/lib/rack/rewindable_input.rb
index 91b9d1eb367e8758477fa570a24b215f3696445f..730c6a2851a17cf2f94db5a51a4cb9706e4b3468 100644
--- a/lib/rack/rewindable_input.rb
+++ b/lib/rack/rewindable_input.rb
@@ -3,17 +3,29 @@
 
 require 'tempfile'
 
+require_relative 'constants'
+
 module Rack
   # Class which can make any IO object rewindable, including non-rewindable ones. It does
   # this by buffering the data into a tempfile, which is rewindable.
   #
-  # rack.input is required to be rewindable, so if your input stream IO is non-rewindable
-  # by nature (e.g. a pipe or a socket) then you can wrap it in an object of this class
-  # to easily make it rewindable.
-  #
   # Don't forget to call #close when you're done. This frees up temporary resources that
   # RewindableInput uses, though it does *not* close the original IO object.
   class RewindableInput
+    # Makes rack.input rewindable, for compatibility with applications and middleware
+    # designed for earlier versions of Rack (where rack.input was required to be
+    # rewindable).
+    class Middleware
+      def initialize(app)
+        @app = app
+      end
+
+      def call(env)
+        env[RACK_INPUT] = RewindableInput.new(env[RACK_INPUT])
+        @app.call(env)
+      end
+    end
+
     def initialize(io)
       @io = io
       @rewindable_io = nil
@@ -40,6 +52,11 @@ module Rack
       @rewindable_io.rewind
     end
 
+    def size
+      make_rewindable unless @rewindable_io
+      @rewindable_io.size
+    end
+
     # Closes this RewindableInput object without closing the originally
     # wrapped IO object. Cleans up any temporary resources that this RewindableInput
     # has created.
@@ -66,12 +83,14 @@ module Rack
       # access it because we have the file handle open.
       @rewindable_io = Tempfile.new('RackRewindableInput')
       @rewindable_io.chmod(0000)
-      @rewindable_io.set_encoding(Encoding::BINARY) if @rewindable_io.respond_to?(:set_encoding)
+      @rewindable_io.set_encoding(Encoding::BINARY)
       @rewindable_io.binmode
+      # :nocov:
       if filesystem_has_posix_semantics?
         raise 'Unlink failed. IO closed.' if @rewindable_io.closed?
         @unlinked = true
       end
+      # :nocov:
 
       buffer = "".dup
       while @io.read(1024 * 4, buffer)
diff --git a/lib/rack/runtime.rb b/lib/rack/runtime.rb
index d9b2d8ed19982d6fb9be36aaadc062c4dd6a3616..a1bfa696e3fae923385777b6bbb953ec0c205395 100644
--- a/lib/rack/runtime.rb
+++ b/lib/rack/runtime.rb
@@ -1,7 +1,9 @@
 # frozen_string_literal: true
 
+require_relative 'utils'
+
 module Rack
-  # Sets an "X-Runtime" response header, indicating the response
+  # Sets an "x-runtime" response header, indicating the response
   # time of the request, in seconds
   #
   # You can put it right before the application to see the processing
@@ -9,18 +11,17 @@ module Rack
   # too.
   class Runtime
     FORMAT_STRING = "%0.6f" # :nodoc:
-    HEADER_NAME = "X-Runtime" # :nodoc:
+    HEADER_NAME = "x-runtime" # :nodoc:
 
     def initialize(app, name = nil)
       @app = app
       @header_name = HEADER_NAME
-      @header_name += "-#{name}" if name
+      @header_name += "-#{name.to_s.downcase}" if name
     end
 
     def call(env)
       start_time = Utils.clock_time
-      status, headers, body = @app.call(env)
-      headers = Utils::HeaderHash[headers]
+      _, headers, _ = response = @app.call(env)
 
       request_time = Utils.clock_time - start_time
 
@@ -28,7 +29,7 @@ module Rack
         headers[@header_name] = FORMAT_STRING % request_time
       end
 
-      [status, headers, body]
+      response
     end
   end
 end
diff --git a/lib/rack/sendfile.rb b/lib/rack/sendfile.rb
index 3d5e786ff76850438a4f2f9b2a4d9f17cdb34266..9c6e0c42fa8f91d314c6169d2528729b81716fbb 100644
--- a/lib/rack/sendfile.rb
+++ b/lib/rack/sendfile.rb
@@ -1,32 +1,36 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'body_proxy'
+
 module Rack
 
   # = Sendfile
   #
   # The Sendfile middleware intercepts responses whose body is being
-  # served from a file and replaces it with a server specific X-Sendfile
+  # served from a file and replaces it with a server specific x-sendfile
   # header. The web server is then responsible for writing the file contents
   # to the client. This can dramatically reduce the amount of work required
   # by the Ruby backend and takes advantage of the web server's optimized file
   # delivery code.
   #
   # 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
+  # respond to +to_path+ and the request must include an x-sendfile-type
   # 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
+  # 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
   #
   # === Nginx
   #
-  # Nginx supports the X-Accel-Redirect header. This is similar to X-Sendfile
+  # Nginx supports the x-accel-redirect header. This is similar to x-sendfile
   # but requires parts of the filesystem to be mapped into a private URL
   # hierarchy.
   #
   # The following example shows the Nginx configuration required to create
-  # a private "/files/" area, enable X-Accel-Redirect, and pass the special
-  # X-Sendfile-Type and X-Accel-Mapping headers to the backend:
+  # a private "/files/" area, enable x-accel-redirect, and pass the special
+  # x-sendfile-type and x-accel-mapping headers to the backend:
   #
   #   location ~ /files/(.*) {
   #     internal;
@@ -40,14 +44,14 @@ module Rack
   #     proxy_set_header   X-Real-IP           $remote_addr;
   #     proxy_set_header   X-Forwarded-For     $proxy_add_x_forwarded_for;
   #
-  #     proxy_set_header   X-Sendfile-Type     X-Accel-Redirect;
-  #     proxy_set_header   X-Accel-Mapping     /var/www/=/files/;
+  #     proxy_set_header   x-sendfile-type     x-accel-redirect;
+  #     proxy_set_header   x-accel-mapping     /var/www/=/files/;
   #
   #     proxy_pass         http://127.0.0.1:8080/;
   #   }
   #
-  # Note that the X-Sendfile-Type header must be set exactly as shown above.
-  # The X-Accel-Mapping header should specify the location on the file system,
+  # Note that the x-sendfile-type header must be set exactly as shown above.
+  # The x-accel-mapping header should specify the location on the file system,
   # followed by an equals sign (=), followed name of the private URL pattern
   # that it maps to. The middleware performs a simple substitution on the
   # resulting path.
@@ -56,8 +60,8 @@ module Rack
   #
   # === lighttpd
   #
-  # Lighttpd has supported some variation of the X-Sendfile header for some
-  # time, although only recent version support X-Sendfile in a reverse proxy
+  # Lighttpd has supported some variation of the x-sendfile header for some
+  # time, although only recent version support x-sendfile in a reverse proxy
   # configuration.
   #
   #   $HTTP["host"] == "example.com" {
@@ -71,7 +75,7 @@ module Rack
   #
   #      proxy-core.allow-x-sendfile = "enable"
   #      proxy-core.rewrite-request = (
-  #        "X-Sendfile-Type" => (".*" => "X-Sendfile")
+  #        "x-sendfile-type" => (".*" => "x-sendfile")
   #      )
   #    }
   #
@@ -79,21 +83,21 @@ module Rack
   #
   # === Apache
   #
-  # X-Sendfile is supported under Apache 2.x using a separate module:
+  # x-sendfile is supported under Apache 2.x using a separate module:
   #
   # https://tn123.org/mod_xsendfile/
   #
   # Once the module is compiled and installed, you can enable it using
   # XSendFile config directive:
   #
-  #   RequestHeader Set X-Sendfile-Type X-Sendfile
+  #   RequestHeader Set x-sendfile-type x-sendfile
   #   ProxyPassReverse / http://localhost:8001/
   #   XSendFile on
   #
   # === Mapping parameter
   #
   # The third parameter allows for an overriding extension of the
-  # X-Accel-Mapping header. Mappings should be provided in tuples of internal to
+  # x-accel-mapping header. Mappings should be provided in tuples of internal to
   # external. The internal values may contain regular expression syntax, they
   # will be matched with case indifference.
 
@@ -107,28 +111,29 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
+      _, headers, body = response = @app.call(env)
+
       if body.respond_to?(:to_path)
         case type = variation(env)
-        when 'X-Accel-Redirect'
+        when /x-accel-redirect/i
           path = ::File.expand_path(body.to_path)
           if url = map_accel_path(env, path)
             headers[CONTENT_LENGTH] = '0'
             # '?' must be percent-encoded because it is not query string but a part of path
-            headers[type] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
+            headers[type.downcase] = ::Rack::Utils.escape_path(url).gsub('?', '%3F')
             obody = body
-            body = Rack::BodyProxy.new([]) do
+            response[2] = Rack::BodyProxy.new([]) do
               obody.close if obody.respond_to?(:close)
             end
           else
-            env[RACK_ERRORS].puts "X-Accel-Mapping header missing"
+            env[RACK_ERRORS].puts "x-accel-mapping header missing"
           end
-        when 'X-Sendfile', 'X-Lighttpd-Send-File'
+        when /x-sendfile|x-lighttpd-send-file/i
           path = ::File.expand_path(body.to_path)
           headers[CONTENT_LENGTH] = '0'
-          headers[type] = path
+          headers[type.downcase] = path
           obody = body
-          body = Rack::BodyProxy.new([]) do
+          response[2] = Rack::BodyProxy.new([]) do
             obody.close if obody.respond_to?(:close)
           end
         when '', nil
@@ -136,7 +141,7 @@ module Rack
           env[RACK_ERRORS].puts "Unknown x-sendfile variation: '#{type}'.\n"
         end
       end
-      [status, headers, body]
+      response
     end
 
     private
diff --git a/lib/rack/server.rb b/lib/rack/server.rb
deleted file mode 100644
index c1f2f5caa321fc9bc4c06d8f1643c81db11445fb..0000000000000000000000000000000000000000
--- a/lib/rack/server.rb
+++ /dev/null
@@ -1,466 +0,0 @@
-# frozen_string_literal: true
-
-require 'optparse'
-require 'fileutils'
-
-module Rack
-
-  class Server
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
-    class Options
-      def parse!(args)
-        options = {}
-        opt_parser = OptionParser.new("", 24, '  ') do |opts|
-          opts.banner = "Usage: rackup [ruby options] [rack options] [rackup config]"
-
-          opts.separator ""
-          opts.separator "Ruby options:"
-
-          lineno = 1
-          opts.on("-e", "--eval LINE", "evaluate a LINE of code") { |line|
-            eval line, TOPLEVEL_BINDING, "-e", lineno
-            lineno += 1
-          }
-
-          opts.on("-d", "--debug", "set debugging flags (set $DEBUG to true)") {
-            options[:debug] = true
-          }
-          opts.on("-w", "--warn", "turn warnings on for your script") {
-            options[:warn] = true
-          }
-          opts.on("-q", "--quiet", "turn off logging") {
-            options[:quiet] = true
-          }
-
-          opts.on("-I", "--include PATH",
-                  "specify $LOAD_PATH (may be used more than once)") { |path|
-            (options[:include] ||= []).concat(path.split(":"))
-          }
-
-          opts.on("-r", "--require LIBRARY",
-                  "require the library, before executing your script") { |library|
-            (options[:require] ||= []) << library
-          }
-
-          opts.separator ""
-          opts.separator "Rack options:"
-          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
-          }
-
-          opts.on("-o", "--host HOST", "listen on HOST (default: localhost)") { |host|
-            options[:Host] = host
-          }
-
-          opts.on("-p", "--port PORT", "use PORT (default: 9292)") { |port|
-            options[:Port] = port
-          }
-
-          opts.on("-O", "--option NAME[=VALUE]", "pass VALUE to the server as option NAME. If no VALUE, sets it to true. Run '#{$0} -s SERVER -h' to get a list of options for SERVER") { |name|
-            name, value = name.split('=', 2)
-            value = true if value.nil?
-            options[name.to_sym] = value
-          }
-
-          opts.on("-E", "--env ENVIRONMENT", "use ENVIRONMENT for defaults (default: development)") { |e|
-            options[:environment] = e
-          }
-
-          opts.on("-D", "--daemonize", "run daemonized in the background") { |d|
-            options[:daemonize] = d ? true : false
-          }
-
-          opts.on("-P", "--pid FILE", "file to store PID") { |f|
-            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:"
-
-          opts.on_tail("-h", "-?", "--help", "Show this message") do
-            puts opts
-            puts handler_opts(options)
-
-            exit
-          end
-
-          opts.on_tail("--version", "Show version") do
-            puts "Rack #{Rack.version} (Release: #{Rack.release})"
-            exit
-          end
-        end
-
-        begin
-          opt_parser.parse! args
-        rescue OptionParser::InvalidOption => e
-          warn e.message
-          abort opt_parser.to_s
-        end
-
-        options[:config] = args.last if args.last && !args.last.empty?
-        options
-      end
-
-      def handler_opts(options)
-        begin
-          info = []
-          server = Rack::Handler.get(options[:server]) || Rack::Handler.default
-          if server && server.respond_to?(:valid_options)
-            info << ""
-            info << "Server-specific options for #{server.name}:"
-
-            has_options = false
-            server.valid_options.each do |name, description|
-              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
-            return "" if !has_options
-          end
-          info.join("\n")
-        rescue NameError, LoadError
-          return "Warning: Could not find handler specified (#{options[:server] || 'default'}) to determine handler-specific options"
-        end
-      end
-    end
-
-    # Start a new rack server (like running rackup). This will parse ARGV and
-    # provide standard ARGV rackup options, defaulting to load 'config.ru'.
-    #
-    # Providing an options hash will prevent ARGV parsing and will not include
-    # any default options.
-    #
-    # This method can be used to very easily launch a CGI application, for
-    # example:
-    #
-    #  Rack::Server.start(
-    #    :app => lambda do |e|
-    #      [200, {'Content-Type' => 'text/html'}, ['hello world']]
-    #    end,
-    #    :server => 'cgi'
-    #  )
-    #
-    # Further options available here are documented on Rack::Server#initialize
-    def self.start(options = nil)
-      new(options).start
-    end
-
-    attr_writer :options
-
-    # Options may include:
-    # * :app
-    #     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
-    #     this selects the middleware that will be wrapped around
-    #     your application. Default options available are:
-    #       - development: CommonLogger, ShowExceptions, and Lint
-    #       - deployment: CommonLogger
-    #       - none: no extra middleware
-    #     note: when the server is a cgi server, CommonLogger is not included.
-    # * :server
-    #     choose a specific Rack::Handler, e.g. cgi, fcgi, webrick
-    # * :daemonize
-    #     if true, the server will daemonize itself (fork, detach, etc)
-    # * :pid
-    #     path to write a pid file after daemonize
-    # * :Host
-    #     the host address to bind to (used by supporting Rack::Handler)
-    # * :Port
-    #     the port to bind to (used by supporting Rack::Handler)
-    # * :AccessLog
-    #     webrick access log options (or supporting Rack::Handler)
-    # * :debug
-    #     turn on debug output ($DEBUG = true)
-    # * :warn
-    #     turn on warnings ($-w = true)
-    # * :include
-    #     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 = []
-
-      if options
-        @use_default_options = false
-        @options = options
-        @app = options[:app] if options[:app]
-      else
-        argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV
-        @use_default_options = true
-        @options = parse_options(argv)
-      end
-    end
-
-    def options
-      merged_options = @use_default_options ? default_options.merge(@options) : @options
-      merged_options.reject { |k, v| @ignore_options.include?(k) }
-    end
-
-    def default_options
-      environment  = ENV['RACK_ENV'] || 'development'
-      default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
-
-      {
-        environment: environment,
-        pid: nil,
-        Port: 9292,
-        Host: default_host,
-        AccessLog: [],
-        config: "config.ru"
-      }
-    end
-
-    def app
-      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
-    end
-
-    class << self
-      def logging_middleware
-        lambda { |server|
-          /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["deployment"] = [
-          [Rack::ContentLength],
-          logging_middleware,
-          [Rack::TempfileReaper]
-        ]
-        m["development"] = [
-          [Rack::ContentLength],
-          logging_middleware,
-          [Rack::ShowExceptions],
-          [Rack::Lint],
-          [Rack::TempfileReaper]
-        ]
-
-        m
-      end
-
-      def middleware
-        default_middleware_by_environment
-      end
-    end
-
-    def middleware
-      self.class.middleware
-    end
-
-    def start(&block)
-      if options[:warn]
-        $-w = true
-      end
-
-      if includes = options[:include]
-        $LOAD_PATH.unshift(*includes)
-      end
-
-      Array(options[:require]).each do |library|
-        require library
-      end
-
-      if options[:debug]
-        $DEBUG = true
-        require 'pp'
-        p options[:server]
-        pp wrapped_app
-        pp app
-      end
-
-      check_pid! if options[:pid]
-
-      # Touch the wrapped app, so that the config.ru is loaded before
-      # daemonization (i.e. before chdir, etc).
-      handle_profiling(options[:heapfile], options[:profile_mode], options[:profile_file]) do
-        wrapped_app
-      end
-
-      daemonize_app if options[:daemonize]
-
-      write_pid if options[:pid]
-
-      trap(:INT) do
-        if server.respond_to?(:shutdown)
-          server.shutdown
-        else
-          exit
-        end
-      end
-
-      server.run(wrapped_app, **options, &block)
-    end
-
-    def server
-      @_server ||= Rack::Handler.get(options[:server])
-
-      unless @_server
-        @_server = Rack::Handler.default
-
-        # We already speak FastCGI
-        @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
-      end
-
-      @_server
-    end
-
-    private
-      def build_app_and_options_from_config
-        if !::File.exist? options[:config]
-          abort "configuration #{options[:config]} not found"
-        end
-
-        app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
-        @options.merge!(options) { |key, old, new| old }
-        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
-
-      def parse_options(args)
-        # Don't evaluate CGI ISINDEX parameters.
-        # http://www.meb.uni-bonn.de/docs/cgi/cl.html
-        args.clear if ENV.include?(REQUEST_METHOD)
-
-        @options = opt_parser.parse!(args)
-        @options[:config] = ::File.expand_path(options[:config])
-        ENV["RACK_ENV"] = options[:environment]
-        @options
-      end
-
-      def opt_parser
-        Options.new
-      end
-
-      def build_app(app)
-        middleware[options[:environment]].reverse_each do |middleware|
-          middleware = middleware.call(self) if middleware.respond_to?(:call)
-          next unless middleware
-          klass, *args = middleware
-          app = klass.new(app, *args)
-        end
-        app
-      end
-
-      def wrapped_app
-        @wrapped_app ||= build_app app
-      end
-
-      def daemonize_app
-        # Cannot be covered as it forks
-        # :nocov:
-        Process.daemon
-        # :nocov:
-      end
-
-      def write_pid
-        ::File.open(options[:pid], ::File::CREAT | ::File::EXCL | ::File::WRONLY ){ |f| f.write("#{Process.pid}") }
-        at_exit { ::FileUtils.rm_f(options[:pid]) }
-      rescue Errno::EEXIST
-        check_pid!
-        retry
-      end
-
-      def check_pid!
-        case pidfile_process_status
-        when :running, :not_owned
-          $stderr.puts "A server is already running. Check #{options[:pid]}."
-          exit(1)
-        when :dead
-          ::File.delete(options[:pid])
-        end
-      end
-
-      def pidfile_process_status
-        return :exited unless ::File.exist?(options[:pid])
-
-        pid = ::File.read(options[:pid]).to_i
-        return :dead if pid == 0
-
-        Process.kill(0, pid)
-        :running
-      rescue Errno::ESRCH
-        :dead
-      rescue Errno::EPERM
-        :not_owned
-      end
-
-  end
-
-end
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
deleted file mode 100644
index 638bd3b3b08eea45c44de5aaca0fbdba4b47f12a..0000000000000000000000000000000000000000
--- a/lib/rack/session/abstract/id.rb
+++ /dev/null
@@ -1,523 +0,0 @@
-# frozen_string_literal: true
-
-# AUTHOR: blink <blinketje@gmail.com>; blink#ruby-lang@irc.freenode.net
-# bugrep: Andreas Zehnder
-
-require_relative '../../../rack'
-require 'time'
-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
-        include Enumerable
-        attr_writer :id
-
-        Unspecified = Object.new
-
-        def self.find(req)
-          req.get_header RACK_SESSION
-        end
-
-        def self.set(req, session)
-          req.set_header RACK_SESSION, session
-        end
-
-        def self.set_options(req, options)
-          req.set_header RACK_SESSION_OPTIONS, options.dup
-        end
-
-        def initialize(store, req)
-          @store = store
-          @req = req
-          @loaded = false
-        end
-
-        def id
-          return @id if @loaded or instance_variable_defined?(:@id)
-          @id = @store.send(:extract_session_id, @req)
-        end
-
-        def options
-          @req.session_options
-        end
-
-        def each(&block)
-          load_for_read!
-          @data.each(&block)
-        end
-
-        def [](key)
-          load_for_read!
-          @data[key.to_s]
-        end
-
-        def dig(key, *keys)
-          load_for_read!
-          @data.dig(key.to_s, *keys)
-        end
-
-        def fetch(key, default = Unspecified, &block)
-          load_for_read!
-          if default == Unspecified
-            @data.fetch(key.to_s, &block)
-          else
-            @data.fetch(key.to_s, default, &block)
-          end
-        end
-
-        def has_key?(key)
-          load_for_read!
-          @data.has_key?(key.to_s)
-        end
-        alias :key? :has_key?
-        alias :include? :has_key?
-
-        def []=(key, value)
-          load_for_write!
-          @data[key.to_s] = value
-        end
-        alias :store :[]=
-
-        def clear
-          load_for_write!
-          @data.clear
-        end
-
-        def destroy
-          clear
-          @id = @store.send(:delete_session, @req, id, options)
-        end
-
-        def to_hash
-          load_for_read!
-          @data.dup
-        end
-
-        def update(hash)
-          load_for_write!
-          @data.update(stringify_keys(hash))
-        end
-        alias :merge! :update
-
-        def replace(hash)
-          load_for_write!
-          @data.replace(stringify_keys(hash))
-        end
-
-        def delete(key)
-          load_for_write!
-          @data.delete(key.to_s)
-        end
-
-        def inspect
-          if loaded?
-            @data.inspect
-          else
-            "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
-          end
-        end
-
-        def exists?
-          return @exists if instance_variable_defined?(:@exists)
-          @data = {}
-          @exists = @store.send(:session_exists?, @req)
-        end
-
-        def loaded?
-          @loaded
-        end
-
-        def empty?
-          load_for_read!
-          @data.empty?
-        end
-
-        def keys
-          load_for_read!
-          @data.keys
-        end
-
-        def values
-          load_for_read!
-          @data.values
-        end
-
-      private
-
-        def load_for_read!
-          load! if !loaded? && exists?
-        end
-
-        def load_for_write!
-          load! unless loaded?
-        end
-
-        def load!
-          @id, session = @store.send(:load_session, @req)
-          @data = stringify_keys(session)
-          @loaded = true
-        end
-
-        def stringify_keys(other)
-          # Use transform_keys after dropping Ruby 2.4 support
-          hash = {}
-          other.to_hash.each do |key, value|
-            hash[key.to_s] = value
-          end
-          hash
-        end
-      end
-
-      # ID sets up a basic framework for implementing an id based sessioning
-      # service. Cookies sent to the client for maintaining sessions will only
-      # contain an id reference. Only #find_session, #write_session and
-      # #delete_session are required to be overwritten.
-      #
-      # All parameters are optional.
-      # * :key determines the name of the cookie, by default it is
-      #   'rack.session'
-      # * :path, :domain, :expire_after, :secure, and :httponly set the related
-      #   cookie options as by Rack::Response#set_cookie
-      # * :skip will not a set a cookie in the response nor update the session state
-      # * :defer will not set a cookie in the response but still update the session
-      #   state if it is used with a backend
-      # * :renew (implementation dependent) will prompt the generation of a new
-      #   session id, and migration of data to be referenced at the new id. If
-      #   :defer is set, it will be overridden and the cookie will be set.
-      # * :sidbits sets the number of bits in length that a generated session
-      #   id will be.
-      #
-      # These options can be set on a per request basis, at the location of
-      # <tt>env['rack.session.options']</tt>. Additionally the id of the
-      # session can be found within the options hash at the key :id. It is
-      # highly not recommended to change its value.
-      #
-      # Is Rack::Utils::Context compatible.
-      #
-      # Not included by default; you must require 'rack/session/abstract/id'
-      # to use.
-
-      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
-        }.freeze
-
-        attr_reader :key, :default_options, :sid_secure
-
-        def initialize(app, options = {})
-          @app = app
-          @default_options = self.class::DEFAULT_OPTIONS.merge(options)
-          @key = @default_options.delete(:key)
-          @cookie_only = @default_options.delete(:cookie_only)
-          @same_site = @default_options.delete(:same_site)
-          initialize_sid
-        end
-
-        def call(env)
-          context(env)
-        end
-
-        def context(env, app = @app)
-          req = make_request env
-          prepare_session(req)
-          status, headers, body = app.call(req.env)
-          res = Rack::Response::Raw.new status, headers
-          commit_session(req, res)
-          [status, headers, body]
-        end
-
-        private
-
-        def make_request(env)
-          Rack::Request.new env
-        end
-
-        def initialize_sid
-          @sidbits = @default_options[:sidbits]
-          @sid_secure = @default_options[:secure_random]
-          @sid_length = @sidbits / 4
-        end
-
-        # Generate a new session id using Ruby #rand.  The size of the
-        # session id is controlled by the :sidbits option.
-        # Monkey patch this to use custom methods for session id generation.
-
-        def generate_sid(secure = @sid_secure)
-          if secure
-            secure.hex(@sid_length)
-          else
-            "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
-          end
-        rescue NotImplementedError
-          generate_sid(false)
-        end
-
-        # Sets the lazy session at 'rack.session' and places options and session
-        # metadata into 'rack.session.options'.
-
-        def prepare_session(req)
-          session_was               = req.get_header RACK_SESSION
-          session                   = session_class.new(self, req)
-          req.set_header RACK_SESSION, session
-          req.set_header RACK_SESSION_OPTIONS, @default_options.dup
-          session.merge! session_was if session_was
-        end
-
-        # Extracts the session id from provided cookies and passes it and the
-        # environment to #find_session.
-
-        def load_session(req)
-          sid = current_session_id(req)
-          sid, session = find_session(req, sid)
-          [sid, session || {}]
-        end
-
-        # Extract session id from request object.
-
-        def extract_session_id(request)
-          sid = request.cookies[@key]
-          sid ||= request.params[@key] unless @cookie_only
-          sid
-        end
-
-        # Returns the current session id from the SessionHash.
-
-        def current_session_id(req)
-          req.get_header(RACK_SESSION).id
-        end
-
-        # Check if the session exists or not.
-
-        def session_exists?(req)
-          value = current_session_id(req)
-          value && !value.empty?
-        end
-
-        # Session should be committed if it was loaded, any of specific options like :renew, :drop
-        # or :expire_after was given and the security permissions match. Skips if skip is given.
-
-        def commit_session?(req, session, options)
-          if options[:skip]
-            false
-          else
-            has_session = loaded_session?(session) || forced_session_update?(session, options)
-            has_session && security_matches?(req, options)
-          end
-        end
-
-        def loaded_session?(session)
-          !session.is_a?(session_class) || session.loaded?
-        end
-
-        def forced_session_update?(session, options)
-          force_options?(options) && session && !session.empty?
-        end
-
-        def force_options?(options)
-          options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
-        end
-
-        def security_matches?(request, options)
-          return true unless options[:secure]
-          request.ssl?
-        end
-
-        # Acquires the session from the environment and the session id from
-        # the session options and passes them to #write_session. If successful
-        # and the :defer option is not true, a cookie will be added to the
-        # response with the session's id.
-
-        def commit_session(req, res)
-          session = req.get_header RACK_SESSION
-          options = session.options
-
-          if options[:drop] || options[:renew]
-            session_id = delete_session(req, session.id || generate_sid, options)
-            return unless session_id
-          end
-
-          return unless commit_session?(req, session, options)
-
-          session.send(:load!) unless loaded_session?(session)
-          session_id ||= session.id
-          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.")
-          elsif options[:defer] and not options[:renew]
-            req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
-          else
-            cookie = Hash.new
-            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]
-
-            if @same_site.respond_to? :call
-              cookie[:same_site] = @same_site.call(req, res)
-            else
-              cookie[:same_site] = @same_site
-            end
-            set_cookie(req, res, cookie.merge!(options))
-          end
-        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.
-
-        def set_cookie(request, res, cookie)
-          if request.cookies[@key] != cookie[:value] || cookie[:expires]
-            res.set_cookie_header =
-              Utils.add_cookie_to_header(res.set_cookie_header, @key, cookie)
-          end
-        end
-
-        # Allow subclasses to prepare_session for different Session classes
-
-        def session_class
-          SessionHash
-        end
-
-        # All thread safety and session retrieval procedures should occur here.
-        # Should return [session_id, session].
-        # If nil is provided as the session id, generation of a new valid id
-        # should occur within.
-
-        def find_session(env, sid)
-          raise '#find_session not implemented.'
-        end
-
-        # All thread safety and session storage procedures should occur here.
-        # Must return the session id if the session was saved successfully, or
-        # false if the session could not be saved.
-
-        def write_session(req, sid, session, options)
-          raise '#write_session not implemented.'
-        end
-
-        # All thread safety and session destroy procedures should occur here.
-        # Should return a new session id or nil if options[:drop]
-
-        def delete_session(req, sid, options)
-          raise '#delete_session not implemented'
-        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 }
-          unless k.instance_variable_defined?(:"@_rack_warned")
-            warn "#{klass} is inheriting from #{ID}.  Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
-            k.instance_variable_set(:"@_rack_warned", true)
-          end
-          super
-        end
-
-        # All thread safety and session retrieval procedures should occur here.
-        # Should return [session_id, session].
-        # If nil is provided as the session id, generation of a new valid id
-        # should occur within.
-
-        def find_session(req, sid)
-          get_session req.env, sid
-        end
-
-        # All thread safety and session storage procedures should occur here.
-        # Must return the session id if the session was saved successfully, or
-        # false if the session could not be saved.
-
-        def write_session(req, sid, session, options)
-          set_session req.env, sid, session, options
-        end
-
-        # All thread safety and session destroy procedures should occur here.
-        # Should return a new session id or nil if options[:drop]
-
-        def delete_session(req, sid, options)
-          destroy_session req.env, sid, options
-        end
-      end
-    end
-  end
-end
diff --git a/lib/rack/session/cookie.rb b/lib/rack/session/cookie.rb
deleted file mode 100644
index bb541396f7b0ef9f8b57dee2446a28646a1b9c07..0000000000000000000000000000000000000000
--- a/lib/rack/session/cookie.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-# frozen_string_literal: true
-
-require 'openssl'
-require 'zlib'
-require_relative 'abstract/id'
-require 'json'
-require 'base64'
-
-module Rack
-
-  module Session
-
-    # Rack::Session::Cookie provides simple cookie based session management.
-    # By default, the session is a Ruby Hash stored as base64 encoded marshalled
-    # data set to :key (default: rack.session).  The object that encodes the
-    # session data is configurable and must respond to +encode+ and +decode+.
-    # Both methods must take a string and return a string.
-    #
-    # When the secret key is set, cookie data is checked for data integrity.
-    # The old secret key is also accepted and allows graceful secret rotation.
-    #
-    # Example:
-    #
-    #     use Rack::Session::Cookie, :key => 'rack.session',
-    #                                :domain => 'foo.com',
-    #                                :path => '/',
-    #                                :expire_after => 2592000,
-    #                                :secret => 'change_me',
-    #                                :old_secret => 'also_change_me'
-    #
-    #     All parameters are optional.
-    #
-    # Example of a cookie with no encoding:
-    #
-    #   Rack::Session::Cookie.new(application, {
-    #     :coder => Rack::Session::Cookie::Identity.new
-    #   })
-    #
-    # Example of a cookie with custom encoding:
-    #
-    #   Rack::Session::Cookie.new(application, {
-    #     :coder => Class.new {
-    #       def encode(str); str.reverse; end
-    #       def decode(str); str.reverse; end
-    #     }.new
-    #   })
-    #
-
-    class Cookie < Abstract::PersistedSecure
-      # Encode session cookies as Base64
-      class Base64
-        def encode(str)
-          ::Base64.strict_encode64(str)
-        end
-
-        def decode(str)
-          ::Base64.decode64(str)
-        end
-
-        # Encode session cookies as Marshaled Base64 data
-        class Marshal < Base64
-          def encode(str)
-            super(::Marshal.dump(str))
-          end
-
-          def decode(str)
-            return unless str
-            ::Marshal.load(super(str)) rescue nil
-          end
-        end
-
-        # N.B. Unlike other encoding methods, the contained objects must be a
-        # valid JSON composite type, either a Hash or an Array.
-        class JSON < Base64
-          def encode(obj)
-            super(::JSON.dump(obj))
-          end
-
-          def decode(str)
-            return unless str
-            ::JSON.parse(super(str)) rescue nil
-          end
-        end
-
-        class ZipJSON < Base64
-          def encode(obj)
-            super(Zlib::Deflate.deflate(::JSON.dump(obj)))
-          end
-
-          def decode(str)
-            return unless str
-            ::JSON.parse(Zlib::Inflate.inflate(super(str)))
-          rescue
-            nil
-          end
-        end
-      end
-
-      # Use no encoding for session cookies
-      class Identity
-        def encode(str); str; end
-        def decode(str); str; end
-      end
-
-      attr_reader :coder
-
-      def initialize(app, options = {})
-        @secrets = options.values_at(:secret, :old_secret).compact
-        @hmac = options.fetch(:hmac, OpenSSL::Digest::SHA1)
-
-        warn <<-MSG unless secure?(options)
-        SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
-        This poses a security threat. It is strongly recommended that you
-        provide a secret to prevent exploits that may be possible from crafted
-        cookies. This will not be supported in future versions of Rack, and
-        future versions will even invalidate your existing user cookies.
-
-        Called from: #{caller[0]}.
-        MSG
-        @coder = options[:coder] ||= Base64::Marshal.new
-        super(app, options.merge!(cookie_only: true))
-      end
-
-      private
-
-      def find_session(req, sid)
-        data = unpacked_cookie_data(req)
-        data = persistent_session_id!(data)
-        [data["session_id"], data]
-      end
-
-      def extract_session_id(request)
-        unpacked_cookie_data(request)["session_id"]
-      end
-
-      def unpacked_cookie_data(request)
-        request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
-          session_data = request.cookies[@key]
-
-          if @secrets.size > 0 && session_data
-            session_data, _, digest = session_data.rpartition('--')
-            session_data = nil unless digest_match?(session_data, digest)
-          end
-
-          request.set_header(k, coder.decode(session_data) || {})
-        end
-      end
-
-      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)
-
-        if @secrets.first
-          session_data << "--#{generate_hmac(session_data, @secrets.first)}"
-        end
-
-        if session_data.size > (4096 - @key.size)
-          req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
-          nil
-        else
-          SessionId.new(session_id, session_data)
-        end
-      end
-
-      def delete_session(req, session_id, options)
-        # Nothing to do here, data is in the client
-        generate_sid unless options[:drop]
-      end
-
-      def digest_match?(data, digest)
-        return unless data && digest
-        @secrets.any? do |secret|
-          Rack::Utils.secure_compare(digest, generate_hmac(data, secret))
-        end
-      end
-
-      def generate_hmac(data, secret)
-        OpenSSL::HMAC.hexdigest(@hmac.new, secret, data)
-      end
-
-      def secure?(options)
-        @secrets.size >= 1 ||
-        (options[:coder] && options[:let_coder_handle_secure_encoding])
-      end
-
-    end
-  end
-end
diff --git a/lib/rack/session/memcache.rb b/lib/rack/session/memcache.rb
deleted file mode 100644
index 6a601174075b257a061b754b4b12748efbda32d5..0000000000000000000000000000000000000000
--- a/lib/rack/session/memcache.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-require 'rack/session/dalli'
-
-module Rack
-  module Session
-    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
deleted file mode 100644
index 4885605f5d294ea867164758c206c00cccce592c..0000000000000000000000000000000000000000
--- a/lib/rack/session/pool.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# 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
-#   sergio, threadiness and bugreps
-
-require_relative 'abstract/id'
-require 'thread'
-
-module Rack
-  module Session
-    # Rack::Session::Pool provides simple cookie based session management.
-    # Session data is stored in a hash held by @pool.
-    # In the context of a multithreaded environment, sessions being
-    # committed to the pool is done in a merging manner.
-    #
-    # The :drop option is available in rack.session.options if you wish to
-    # explicitly remove the session from the session cache.
-    #
-    # Example:
-    #   myapp = MyRackApp.new
-    #   sessioned = Rack::Session::Pool.new(myapp,
-    #     :domain => 'foo.com',
-    #     :expire_after => 2592000
-    #   )
-    #   Rack::Handler::WEBrick.run sessioned
-
-    class Pool < Abstract::PersistedSecure
-      attr_reader :mutex, :pool
-      DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge drop: false
-
-      def initialize(app, options = {})
-        super
-        @pool = Hash.new
-        @mutex = Mutex.new
-      end
-
-      def generate_sid
-        loop do
-          sid = super
-          break sid unless @pool.key? sid.private_id
-        end
-      end
-
-      def find_session(req, sid)
-        with_lock(req) do
-          unless sid and session = get_session_with_fallback(sid)
-            sid, session = generate_sid, {}
-            @pool.store sid.private_id, session
-          end
-          [sid, session]
-        end
-      end
-
-      def write_session(req, session_id, new_session, options)
-        with_lock(req) do
-          @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.public_id)
-          @pool.delete(session_id.private_id)
-          generate_sid unless options[:drop]
-        end
-      end
-
-      def with_lock(req)
-        @mutex.lock if req.multithread?
-        yield
-      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 07e60388069f7926eb98c91e310217b5acaa0fb9..ca090a5048a27b759590a37e123a987cbbf87a29 100644
--- a/lib/rack/show_exceptions.rb
+++ b/lib/rack/show_exceptions.rb
@@ -3,6 +3,10 @@
 require 'ostruct'
 require 'erb'
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'request'
+
 module Rack
   # Rack::ShowExceptions catches all exceptions raised from the app it
   # wraps.  It shows a useful backtrace with the sourcefile and
@@ -55,7 +59,12 @@ module Rack
     private :accepts_html?
 
     def dump_exception(exception)
-      string = "#{exception.class}: #{exception.message}\n".dup
+      if exception.respond_to?(:detailed_message)
+        message = exception.detailed_message(highlight: false)
+      else
+        message = exception.message
+      end
+      string = "#{exception.class}: #{message}\n".dup
       string << exception.backtrace.map { |l| "\t#{l}" }.join("\n")
       string
     end
@@ -159,7 +168,7 @@ module Rack
           div.commands { margin-left: 40px; }
           div.commands a { color:black; text-decoration:none; }
           #summary { background: #ffc; }
-          #summary h2 { font-weight: normal; color: #666; }
+          #summary h2 { font-family: monospace; font-weight: normal; color: #666; white-space: pre-wrap; }
           #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
           #summary ul#quicklinks li { float: left; padding: 0 1em; }
           #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
@@ -227,7 +236,11 @@ module Rack
 
       <div id="summary">
         <h1><%=h exception.class %> at <%=h path %></h1>
+      <% if exception.respond_to?(:detailed_message) %>
+        <h2><%=h exception.detailed_message(highlight: false) %></h2>
+      <% else %>
         <h2><%=h exception.message %></h2>
+      <% end %>
         <table><tr>
           <th>Ruby</th>
           <td>
diff --git a/lib/rack/show_status.rb b/lib/rack/show_status.rb
index a99bdaf33aa7e5ec388444f50e90ba32aaf0f71a..b6f75a016e9682dca1a3fdfe4d8a06b9e075fbd5 100644
--- a/lib/rack/show_status.rb
+++ b/lib/rack/show_status.rb
@@ -2,6 +2,11 @@
 
 require 'erb'
 
+require_relative 'constants'
+require_relative 'utils'
+require_relative 'request'
+require_relative 'body_proxy'
+
 module Rack
   # Rack::ShowStatus catches all empty responses and replaces them
   # with a site explaining the error.
@@ -17,8 +22,7 @@ module Rack
     end
 
     def call(env)
-      status, headers, body = @app.call(env)
-      headers = Utils::HeaderHash[headers]
+      status, headers, body = response = @app.call(env)
       empty = headers[CONTENT_LENGTH].to_i <= 0
 
       # client or server error, or explicit message
@@ -33,12 +37,18 @@ module Rack
         # Yes, it is dumb, but I don't like Ruby yelling at me.
         detail = detail = env[RACK_SHOWSTATUS_DETAIL] || message
 
-        body = @template.result(binding)
-        size = body.bytesize
-        [status, headers.merge(CONTENT_TYPE => "text/html", CONTENT_LENGTH => size.to_s), [body]]
-      else
-        [status, headers, body]
+        html = @template.result(binding)
+        size = html.bytesize
+
+        response[2] = Rack::BodyProxy.new([html]) do
+          body.close if body.respond_to?(:close)
+        end
+
+        headers[CONTENT_TYPE] = "text/html"
+        headers[CONTENT_LENGTH] = size.to_s
       end
+
+      response
     end
 
     def h(obj)                  # :nodoc:
diff --git a/lib/rack/static.rb b/lib/rack/static.rb
index 8cb58b2fd7342fe530ff7e4508cf132fb2abdf00..5c9b6760ffce346dac17d04ff9695dfcbeea67f4 100644
--- a/lib/rack/static.rb
+++ b/lib/rack/static.rb
@@ -1,5 +1,9 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'files'
+require_relative 'mime'
+
 module Rack
 
   # The Rack::Static middleware intercepts requests for static files
@@ -78,16 +82,14 @@ module Rack
   #         :header_rules => [
   #           # Cache all static files in public caches (e.g. Rack::Cache)
   #           #  as well as in the browser
-  #           [:all, {'Cache-Control' => 'public, max-age=31536000'}],
+  #           [:all, {'cache-control' => 'public, max-age=31536000'}],
   #
   #           # Provide web fonts with cross-origin access-control-headers
   #           #  Firefox requires this when serving assets using a Content Delivery Network
-  #           [:fonts, {'Access-Control-Allow-Origin' => '*'}]
+  #           [:fonts, {'access-control-allow-origin' => '*'}]
   #         ]
   #
   class Static
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
     def initialize(app, options = {})
       @app = app
       @urls = options[:urls] || ["/favicon.ico"]
@@ -137,10 +139,8 @@ module Rack
           elsif response[0] == 304
             # Do nothing, leave headers as is
           else
-            if mime_type = Mime.mime_type(::File.extname(path), 'text/plain')
-              response[1][CONTENT_TYPE] = mime_type
-            end
-            response[1]['Content-Encoding'] = 'gzip'
+            response[1][CONTENT_TYPE] = Mime.mime_type(::File.extname(path), 'text/plain')
+            response[1]['content-encoding'] = 'gzip'
           end
         end
 
diff --git a/lib/rack/tempfile_reaper.rb b/lib/rack/tempfile_reaper.rb
index 9b04fefc2441402ea1862a1780bc7611ffd27fa7..0b94cc73a157b52de72fb9706079c3c2947d972b 100644
--- a/lib/rack/tempfile_reaper.rb
+++ b/lib/rack/tempfile_reaper.rb
@@ -1,5 +1,8 @@
 # frozen_string_literal: true
 
+require_relative 'constants'
+require_relative 'body_proxy'
+
 module Rack
 
   # Middleware tracks and cleans Tempfiles created throughout a request (i.e. Rack::Multipart)
@@ -12,11 +15,19 @@ module Rack
 
     def call(env)
       env[RACK_TEMPFILES] ||= []
-      status, headers, body = @app.call(env)
-      body_proxy = BodyProxy.new(body) do
-        env[RACK_TEMPFILES].each(&:close!) unless env[RACK_TEMPFILES].nil?
+
+      begin
+        _, _, body = response = @app.call(env)
+      rescue Exception
+        env[RACK_TEMPFILES]&.each(&:close!)
+        raise
       end
-      [status, headers, body_proxy]
+
+      response[2] = BodyProxy.new(body) do
+        env[RACK_TEMPFILES]&.each(&:close!)
+      end
+
+      response
     end
   end
 end
diff --git a/lib/rack/urlmap.rb b/lib/rack/urlmap.rb
index 8462f92067d4c3397378cff6766d21b2ac5a9123..99c4d82365a298bfe709b8dab0f8edb2e7756ddc 100644
--- a/lib/rack/urlmap.rb
+++ b/lib/rack/urlmap.rb
@@ -2,6 +2,8 @@
 
 require 'set'
 
+require_relative 'constants'
+
 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
@@ -74,7 +76,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 c8e61ea1806c615bfdad47f0f7c14916d6b60694..99d696dbf47c707c2d69acf54240f4bd51d5a00e 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -8,29 +8,29 @@ require 'tempfile'
 require 'time'
 
 require_relative 'query_parser'
+require_relative 'mime'
+require_relative 'headers'
+require_relative 'constants'
 
 module Rack
   # Rack::Utils contains a grab-bag of useful methods for writing web
   # applications adopted from all kinds of Ruby libraries.
 
   module Utils
-    (require_relative 'core_ext/regexp'; using ::Rack::RegexpExtensions) if RUBY_VERSION < '2.4'
-
     ParameterTypeError = QueryParser::ParameterTypeError
     InvalidParameterError = QueryParser::InvalidParameterError
+    ParamsTooDeepError = QueryParser::ParamsTooDeepError
     DEFAULT_SEP = QueryParser::DEFAULT_SEP
     COMMON_SEP = QueryParser::COMMON_SEP
     KeySpaceConstrainedParams = QueryParser::Params
 
-    RFC2822_DAY_NAME = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
-    RFC2822_MONTH_NAME = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]
-
     class << self
       attr_accessor :default_query_parser
     end
-    # The default number of bytes to allow parameter keys to take up.
-    # This helps prevent a rogue client from flooding a Request.
-    self.default_query_parser = QueryParser.make_default(65536, 100)
+    # The default amount of nesting to allowed by hash parameters.
+    # This helps prevent a rogue client from triggering a possible stack overflow
+    # when parsing parameters.
+    self.default_query_parser = QueryParser.make_default(32)
 
     module_function
 
@@ -86,11 +86,12 @@ module Rack
     end
 
     def self.key_space_limit
-      default_query_parser.key_space_limit
+      warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
+      65536
     end
 
     def self.key_space_limit=(v)
-      self.default_query_parser = self.default_query_parser.new_space_limit(v)
+      warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
     end
 
     if defined?(Process::CLOCK_MONOTONIC)
@@ -131,13 +132,13 @@ module Rack
         }.join("&")
       when Hash
         value.map { |k, v|
-          build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
+          build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k)
         }.delete_if(&:empty?).join('&')
       when nil
-        prefix
+        escape(prefix)
       else
         raise ArgumentError, "value must be a Hash" if prefix.nil?
-        "#{prefix}=#{escape(value)}"
+        "#{escape(prefix)}=#{escape(value)}"
       end
     end
 
@@ -152,6 +153,19 @@ module Rack
       end
     end
 
+    def forwarded_values(forwarded_header)
+      return nil unless forwarded_header
+      forwarded_header = forwarded_header.to_s.gsub("\n", ";")
+
+      forwarded_header.split(/\s*;\s*/).each_with_object({}) do |field, values|
+        field.split(/\s*,\s*/).each do |pair|
+          return nil unless pair =~ /\A\s*(by|for|host|proto)\s*=\s*"?([^"]+)"?\s*\Z/i
+          (values[$1.downcase.to_sym] ||= []) << $2
+        end
+      end
+    end
+    module_function :forwarded_values
+
     # Return best accept value to use, based on the algorithm
     # in RFC 2616 Section 14.  If there are multiple best
     # matches (same specificity and quality), the value returned
@@ -166,7 +180,7 @@ module Rack
       end.compact.sort_by do |match, quality|
         (match.split('/', 2).count('*') * -10) + quality
       end.last
-      matches && matches.first
+      matches&.first
     end
 
     ESCAPE_HTML = {
@@ -217,17 +231,20 @@ module Rack
       (encoding_candidates & available_encodings)[0]
     end
 
-    def parse_cookies(env)
-      parse_cookies_header env[HTTP_COOKIE]
-    end
+    # :call-seq:
+    #   parse_cookies_header(value) -> hash
+    #
+    # Parse cookies from the provided header +value+ according to RFC6265. The
+    # syntax for cookie headers only supports semicolons. Returns a map of
+    # cookie +key+ to cookie +value+.
+    #
+    #   parse_cookies_header('myname=myvalue; max-age=0')
+    #   # => {"myname"=>"myvalue", "max-age"=>"0"}
+    #
+    def parse_cookies_header(value)
+      return {} unless value
 
-    def parse_cookies_header(header)
-      # According to RFC 6265:
-      # The syntax for cookie headers only supports semicolons
-      # User Agent -> Server ==
-      # Cookie: SID=31d4d96e407aad42; lang=en-US
-      return {} unless header
-      header.split(/[;] */n).each_with_object({}) do |cookie, cookies|
+      value.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)
@@ -235,14 +252,66 @@ module Rack
     end
 
     def add_cookie_to_header(header, key, value)
+      warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
+
+      case header
+      when nil, ''
+        return set_cookie_header(key, value)
+      when String
+        [header, set_cookie_header(key, value)]
+      when Array
+        header + [set_cookie_header(key, value)]
+      else
+        raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
+      end
+    end
+
+    # :call-seq:
+    #   parse_cookies(env) -> hash
+    #
+    # Parse cookies from the provided request environment using
+    # parse_cookies_header. Returns a map of cookie +key+ to cookie +value+.
+    #
+    #   parse_cookies({'HTTP_COOKIE' => 'myname=myvalue'})
+    #   # => {'myname' => 'myvalue'}
+    #
+    def parse_cookies(env)
+      parse_cookies_header env[HTTP_COOKIE]
+    end
+
+    # :call-seq:
+    #   set_cookie_header(key, value) -> encoded string
+    #
+    # Generate an encoded string using the provided +key+ and +value+ suitable
+    # for the +set-cookie+ header according to RFC6265. The +value+ may be an
+    # instance of either +String+ or +Hash+.
+    #
+    # If the cookie +value+ is an instance of +Hash+, it considers the following
+    # cookie attribute keys: +domain+, +max_age+, +expires+ (must be instance
+    # of +Time+), +secure+, +http_only+, +same_site+ and +value+. For more
+    # details about the interpretation of these fields, consult
+    # [RFC6265 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6265#section-5.2).
+    #
+    # An extra cookie attribute +escape_key+ can be provided to control whether
+    # or not the cookie key is URL encoded. If explicitly set to +false+, the
+    # cookie key name will not be url encoded (escaped). The default is +true+.
+    #
+    #   set_cookie_header("myname", "myvalue")
+    #   # => "myname=myvalue"
+    #
+    #   set_cookie_header("myname", {value: "myvalue", max_age: 10})
+    #   # => "myname=myvalue; max-age=10"
+    #
+    def set_cookie_header(key, value)
       case value
       when Hash
+        key = escape(key) unless value[:escape_key] == false
         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]
         expires = "; expires=#{value[:expires].httpdate}" if value[:expires]
         secure = "; secure"  if value[:secure]
-        httponly = "; HttpOnly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
+        httponly = "; httponly" if (value.key?(:httponly) ? value[:httponly] : value[:http_only])
         same_site =
           case value[:same_site]
           when false, nil
@@ -257,100 +326,109 @@ module Rack
             raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
           end
         value = value[:value]
+      else
+        key = escape(key)
       end
+
       value = [value] unless Array === value
 
-      cookie = "#{escape(key)}=#{value.map { |v| escape v }.join('&')}#{domain}" \
+      return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
         "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
+    end
 
-      case header
-      when nil, ''
-        cookie
-      when String
-        [header, cookie].join("\n")
-      when Array
-        (header + [cookie]).join("\n")
+    # :call-seq:
+    #   set_cookie_header!(headers, key, value) -> header value
+    #
+    # Append a cookie in the specified headers with the given cookie +key+ and
+    # +value+ using set_cookie_header.
+    #
+    # If the headers already contains a +set-cookie+ key, it will be converted
+    # to an +Array+ if not already, and appended to.
+    def set_cookie_header!(headers, key, value)
+      if header = headers[SET_COOKIE]
+        if header.is_a?(Array)
+          header << set_cookie_header(key, value)
+        else
+          headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
+        end
       else
-        raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
+        headers[SET_COOKIE] = set_cookie_header(key, value)
       end
     end
 
-    def set_cookie_header!(header, key, value)
-      header[SET_COOKIE] = add_cookie_to_header(header[SET_COOKIE], key, value)
-      nil
+    # :call-seq:
+    #   delete_set_cookie_header(key, value = {}) -> encoded string
+    #
+    # Generate an encoded string based on the given +key+ and +value+ using
+    # set_cookie_header for the purpose of causing the specified cookie to be
+    # deleted. The +value+ may be an instance of +Hash+ and can include
+    # attributes as outlined by set_cookie_header. The encoded cookie will have
+    # a +max_age+ of 0 seconds, an +expires+ date in the past and an empty
+    # +value+. When used with the +set-cookie+ header, it will cause the client
+    # to *remove* any matching cookie.
+    #
+    #   delete_set_cookie_header("myname")
+    #   # => "myname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    #
+    def delete_set_cookie_header(key, value = {})
+      set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
     end
 
     def make_delete_cookie_header(header, key, value)
-      case header
-      when nil, ''
-        cookies = []
-      when String
-        cookies = header.split("\n")
-      when Array
-        cookies = header
-      end
-
-      key = escape(key)
-      domain = value[:domain]
-      path = value[:path]
-      regexp = if domain
-                 if path
-                   /\A#{key}=.*(?:domain=#{domain}(?:;|$).*path=#{path}(?:;|$)|path=#{path}(?:;|$).*domain=#{domain}(?:;|$))/
-                 else
-                   /\A#{key}=.*domain=#{domain}(?:;|$)/
-                 end
-               elsif path
-                 /\A#{key}=.*path=#{path}(?:;|$)/
-               else
-                 /\A#{key}=/
-               end
-
-      cookies.reject! { |cookie| regexp.match? cookie }
+      warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
 
-      cookies.join("\n")
+      delete_set_cookie_header!(header, key, value)
     end
 
-    def delete_cookie_header!(header, key, value = {})
-      header[SET_COOKIE] = add_remove_cookie_to_header(header[SET_COOKIE], key, value)
-      nil
+    def delete_cookie_header!(headers, key, value = {})
+      headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
+
+      return nil
     end
 
-    # Adds a cookie that will *remove* a cookie from the client.  Hence the
-    # strange method name.
     def add_remove_cookie_to_header(header, key, value = {})
-      new_header = make_delete_cookie_header(header, key, value)
+      warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
 
-      add_cookie_to_header(new_header, key,
-                 { value: '', path: nil, domain: nil,
-                   max_age: '0',
-                   expires: Time.at(0) }.merge(value))
+      delete_set_cookie_header!(header, key, value)
+    end
 
+    # :call-seq:
+    #   delete_set_cookie_header!(header, key, value = {}) -> header value
+    #
+    # Set an expired cookie in the specified headers with the given cookie
+    # +key+ and +value+ using delete_set_cookie_header. This causes
+    # the client to immediately delete the specified cookie.
+    #
+    #   delete_set_cookie_header!(nil, "mycookie")
+    #   # => "mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    #
+    # If the header is non-nil, it will be modified in place.
+    #
+    #   header = []
+    #   delete_set_cookie_header!(header, "mycookie")
+    #   # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
+    #   header
+    #   # => ["mycookie=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"]
+    #
+    def delete_set_cookie_header!(header, key, value = {})
+      if header
+        header = Array(header)
+        header << delete_set_cookie_header(key, value)
+      else
+        header = delete_set_cookie_header(key, value)
+      end
+
+      return header
     end
 
     def rfc2822(time)
       time.rfc2822
     end
 
-    # Modified version of stdlib time.rb Time#rfc2822 to use '%d-%b-%Y' instead
-    # of '% %b %Y'.
-    # It assumes that the time is in GMT to comply to the RFC 2109.
-    #
-    # NOTE: I'm not sure the RFC says it requires GMT, but is ambiguous enough
-    # that I'm certain someone implemented only that option.
-    # Do not use %a and %b from Time.strptime, it would use localized names for
-    # weekday and month.
-    #
-    def rfc2109(time)
-      wday = RFC2822_DAY_NAME[time.wday]
-      mon = RFC2822_MONTH_NAME[time.mon - 1]
-      time.strftime("#{wday}, %d-#{mon}-%Y %H:%M:%S GMT")
-    end
-
     # Parses the "Range:" header, if present, into an array of Range objects.
     # Returns nil if the header is missing or syntactically invalid.
     # Returns an empty array if none of the ranges are satisfiable.
     def byte_ranges(env, size)
-      warn "`byte_ranges` is deprecated, please use `get_byte_ranges`" if $VERBOSE
       get_byte_ranges env['HTTP_RANGE'], size
     end
 
@@ -383,20 +461,30 @@ module Rack
       ranges
     end
 
-    # Constant time string comparison.
-    #
-    # NOTE: the values compared should be of fixed length, such as strings
-    # that have already been processed by HMAC. This should not be used
-    # on variable length plaintext strings because it could leak length info
-    # via timing attacks.
-    def secure_compare(a, b)
-      return false unless a.bytesize == b.bytesize
+    # :nocov:
+    if defined?(OpenSSL.fixed_length_secure_compare)
+      # Constant time string comparison.
+      #
+      # NOTE: the values compared should be of fixed length, such as strings
+      # that have already been processed by HMAC. This should not be used
+      # on variable length plaintext strings because it could leak length info
+      # via timing attacks.
+      def secure_compare(a, b)
+        return false unless a.bytesize == b.bytesize
+
+        OpenSSL.fixed_length_secure_compare(a, b)
+      end
+    # :nocov:
+    else
+      def secure_compare(a, b)
+        return false unless a.bytesize == b.bytesize
 
-      l = a.unpack("C*")
+        l = a.unpack("C*")
 
-      r, i = 0, -1
-      b.each_byte { |v| r |= v ^ l[i += 1] }
-      r == 0
+        r, i = 0, -1
+        b.each_byte { |v| r |= v ^ l[i += 1] }
+        r == 0
+      end
     end
 
     # Context allows the use of a compatible middleware at different points
@@ -425,94 +513,32 @@ module Rack
       end
     end
 
-    # A case-insensitive Hash that preserves the original case of a
+    # A wrapper around Headers
     # header when set.
     #
     # @api private
     class HeaderHash < Hash # :nodoc:
       def self.[](headers)
-        if headers.is_a?(HeaderHash) && !headers.frozen?
+        warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
+        if headers.is_a?(Headers) && !headers.frozen?
           return headers
-        else
-          return self.new(headers)
         end
-      end
 
-      def initialize(hash = {})
-        super()
-        @names = {}
-        hash.each { |k, v| self[k] = v }
+        new_headers = Headers.new
+        headers.each{|k,v| new_headers[k] = v}
+        new_headers
       end
 
-      # on dup/clone, we need to duplicate @names hash
-      def initialize_copy(other)
-        super
-        @names = other.names.dup
+      def self.new(hash = {})
+        warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
+        headers = Headers.new
+        hash.each{|k,v| headers[k] = v}
+        headers
       end
 
-      # on clear, we need to clear @names hash
-      def clear
-        super
-        @names.clear
+      def self.allocate
+        raise TypeError, "cannot allocate HeaderHash"
       end
-
-      def each
-        super do |k, v|
-          yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
-        end
-      end
-
-      def to_hash
-        hash = {}
-        each { |k, v| hash[k] = v }
-        hash
-      end
-
-      def [](k)
-        super(k) || super(@names[k.downcase])
-      end
-
-      def []=(k, v)
-        canonical = k.downcase.freeze
-        delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
-        @names[canonical] = k
-        super k, v
-      end
-
-      def delete(k)
-        canonical = k.downcase
-        result = super @names.delete(canonical)
-        result
-      end
-
-      def include?(k)
-        super || @names.include?(k.downcase)
-      end
-
-      alias_method :has_key?, :include?
-      alias_method :member?, :include?
-      alias_method :key?, :include?
-
-      def merge!(other)
-        other.each { |k, v| self[k] = v }
-        self
-      end
-
-      def merge(other)
-        hash = dup
-        hash.merge! other
-      end
-
-      def replace(other)
-        clear
-        other.each { |k, v| self[k] = v }
-        self
-      end
-
-      protected
-        def names
-          @names
-        end
     end
 
     # Every standard HTTP code mapped to the appropriate message.
diff --git a/lib/rack/version.rb b/lib/rack/version.rb
index d451de434c390e2dca840332535bc6aa5dd9e4b6..e634f23aecee4cef06bcd755e33f28cb1c1c163c 100644
--- a/lib/rack/version.rb
+++ b/lib/rack/version.rb
@@ -13,14 +13,19 @@
 
 module Rack
   # The Rack protocol version number implemented.
-  VERSION = [1, 3]
+  VERSION = [1, 3].freeze
+  deprecate_constant :VERSION
 
-  # Return the Rack protocol version as a dotted string.
+  VERSION_STRING = "1.3".freeze
+  deprecate_constant :VERSION_STRING
+
+  # The Rack protocol version number implemented.
   def self.version
-    VERSION.join(".")
+    warn "Rack.version is deprecated and will be removed in Rack 3.1!", uplevel: 1
+    VERSION
   end
 
-  RELEASE = "2.2.6.4"
+  RELEASE = "3.0.8"
 
   # Return the Rack release as a dotted string.
   def self.release
diff --git a/rack.gemspec b/rack.gemspec
index 246ed7c639ba65a21e1ad70620a7e12ab2f49d2d..743804ebf23066ae59cea395338b8d02d7270cf9 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -17,30 +17,26 @@ Gem::Specification.new do |s|
     middleware) into a single method call.
   EOF
 
-  s.files = Dir['{bin/*,contrib/*,example/*,lib/**/*}'] +
-    %w(MIT-LICENSE rack.gemspec Rakefile README.rdoc SPEC.rdoc)
-
-  s.bindir = 'bin'
-  s.executables << 'rackup'
-  s.require_path = 'lib'
-  s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.md', 'CONTRIBUTING.md']
+  s.files = Dir['lib/**/*'] + %w(MIT-LICENSE README.md SPEC.rdoc)
+  s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'CONTRIBUTING.md']
 
   s.author = 'Leah Neukirchen'
   s.email = 'leah@vuxu.org'
 
   s.homepage = 'https://github.com/rack/rack'
 
-  s.required_ruby_version = '>= 2.3.0'
+  s.required_ruby_version = '>= 2.4.0'
 
   s.metadata = {
     "bug_tracker_uri" => "https://github.com/rack/rack/issues",
-    "changelog_uri" => "https://github.com/rack/rack/blob/master/CHANGELOG.md",
+    "changelog_uri" => "https://github.com/rack/rack/blob/main/CHANGELOG.md",
     "documentation_uri" => "https://rubydoc.info/github/rack/rack",
     "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 'bundler'
   s.add_development_dependency 'rake'
 end
diff --git a/test/builder/an_underscore_app.rb b/test/builder/an_underscore_app.rb
index f58a2be509f0e422202dfd35fcbb0e120a730aa1..4f6e87e50349a03914e95ef75b9d7ae16672fa7d 100644
--- a/test/builder/an_underscore_app.rb
+++ b/test/builder/an_underscore_app.rb
@@ -2,6 +2,6 @@
 
 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/bom.ru b/test/builder/bom.ru
index 5740f9a13826a4865da00ec40dcefd548d617caf..6f491f0f7fa3aaec4dbb5db1bbcb690ec38d9f71 100644
--- a/test/builder/bom.ru
+++ b/test/builder/bom.ru
@@ -1 +1 @@
-run -> (env) { [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
+run -> (env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
diff --git a/test/builder/comment.ru b/test/builder/comment.ru
index 894ba5d017927d96c19dcbeb82aa70711e82cce0..7d03db8bfaea2d922e37617aca9c72cfdca8d2e2 100644
--- a/test/builder/comment.ru
+++ b/test/builder/comment.ru
@@ -3,4 +3,4 @@
 =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 dd8d45a9255e0c59bcdb45065cec4e2a6ceb282c..f1dcf5660b619f1003786a2d9f464538769b5160 100644
--- a/test/builder/end.ru
+++ b/test/builder/end.ru
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] }
+run lambda { |env| [200, { 'content-type' => 'text/plain' }, ['OK']] }
 __END__
 Should not be evaluated
 Neither should
diff --git a/test/builder/frozen.ru b/test/builder/frozen.ru
index 5bad750f4f4250fc5209dfe922525f225df8e03d..71ccc54d7f0e7527d5b020fa3d48e2f769d906b3 100644
--- a/test/builder/frozen.ru
+++ b/test/builder/frozen.ru
@@ -3,5 +3,5 @@
 run lambda { |env|
   body = 'frozen'
   raise "Not frozen!" unless body.frozen?
-  [200, { 'Content-Type' => 'text/plain' }, [body]]
+  [200, { 'content-type' => 'text/plain' }, [body]]
 }
diff --git a/test/builder/line.ru b/test/builder/line.ru
index 9ad88986087ce02ab6776dc9dc49d9e5d5323bbe..03a8e017c5b1ff8d50157cee0b3d47081c491cf8 100644
--- a/test/builder/line.ru
+++ b/test/builder/line.ru
@@ -1,3 +1,3 @@
 # frozen_string_literal: true
 
-run lambda{ |env| [200, { 'Content-Type' => 'text/plain' }, [__LINE__.to_s]] }
+run lambda{ |env| [200, { 'content-type' => 'text/plain' }, [__LINE__.to_s]] }
diff --git a/test/builder/options.ru b/test/builder/options.ru
index dca48fd9190c5dd55d895322d72858f6771446e7..5b3b42b6b7175ac5cd5643a78ceb82f189d916da 100644
--- a/test/builder/options.ru
+++ b/test/builder/options.ru
@@ -1,4 +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/rackup_stub.rb b/test/cgi/rackup_stub.rb
old mode 100755
new mode 100644
diff --git a/test/cgi/sample_rackup.ru b/test/cgi/sample_rackup.ru
old mode 100755
new mode 100644
index c8e94c9f1527d859870b095ec22ab7974d3fe42c..ec154da3a078eda35e7be2c6920c92e125a1be5c
--- a/test/cgi/sample_rackup.ru
+++ b/test/cgi/sample_rackup.ru
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
-require '../testrequest'
+require '../test_request'
 
 run Rack::Lint.new(TestRequest.new)
diff --git a/test/cgi/test b/test/cgi/test
old mode 100755
new mode 100644
index a1de2fbe39a84cb7800adc392ffcfbacbe2d0658..546ad906194fbc9e43921e4b690f4b2db7bc54dd
--- a/test/cgi/test
+++ b/test/cgi/test
@@ -1,9 +1,5 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-$: << File.join(File.dirname(__FILE__), "..", "..", "lib")
-
-require 'rack'
-require '../testrequest'
-
-Rack::Handler::CGI.run(Rack::Lint.new(TestRequest.new))
+***** DO NOT MODIFY THIS FILE! *****
+If you modify this file, tests will break!!!
+The quick brown fox jumps over the ruby dog.
+The quick brown fox jumps over the lazy dog.
+***** DO NOT MODIFY THIS FILE! *****
diff --git a/test/cgi/test.ru b/test/cgi/test.ru
old mode 100755
new mode 100644
index 1263778df2d458483d78060efe49e4f9fe151bd3..d13f288fbda950c1d8f26a2b1fea96efa6af423d
--- a/test/cgi/test.ru
+++ b/test/cgi/test.ru
@@ -1,5 +1,5 @@
 #!../../bin/rackup
 # frozen_string_literal: true
 
-require '../testrequest'
+require '../test_request'
 run Rack::Lint.new(TestRequest.new)
diff --git a/test/helper.rb b/test/helper.rb
index 55799c8c65b4b92f868480450b2a3a462258a9c3..a2f569c39a1f83ab75ebd055204164d479397369 100644
--- a/test/helper.rb
+++ b/test/helper.rb
@@ -1,21 +1,41 @@
 # frozen_string_literal: true
 
 if ENV.delete('COVERAGE')
-  require 'coverage'
   require 'simplecov'
 
-  def SimpleCov.rack_coverage(**opts)
-    start do
-      add_filter "/test/"
-      add_filter "/lib/rack/handler"
-      add_group('Missing'){|src| src.covered_percent < 100}
-      add_group('Covered'){|src| src.covered_percent == 100}
-    end
+  SimpleCov.start do
+    enable_coverage :branch
+    add_filter "/test/"
+    add_filter "/lib/rack/handler"
+    add_group('Missing'){|src| src.covered_percent < 100}
+    add_group('Covered'){|src| src.covered_percent == 100}
+  end
+end
+
+if ENV['SEPARATE']
+  def self.separate_testing
+    yield
+  end
+else
+  $:.unshift(File.expand_path('../lib', __dir__))
+  require_relative '../lib/rack'
+
+  def self.separate_testing
   end
-  SimpleCov.rack_coverage
 end
 
-$:.unshift(File.expand_path('../lib', __dir__))
-require_relative '../lib/rack'
 require 'minitest/global_expectations/autorun'
 require 'stringio'
+
+class Minitest::Spec
+  def self.deprecated(*args, &block)
+    it(*args) do
+      begin
+        verbose, $VERBOSE = $VERBOSE, nil
+        instance_exec(&block)
+      ensure
+        $VERBOSE = verbose
+      end
+    end
+  end
+end
diff --git a/test/load/rack-test-a.rb b/test/load/rack-test-a.rb
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/test/load/rack-test-b.rb b/test/load/rack-test-b.rb
deleted file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000
diff --git a/test/multipart/bad_robots b/test/multipart/bad_robots
index 7e5bd418f6602ae7a5c205270d4a5be5e98cea3b..c4b8258f024d1d7ceff0bae2264bbc6b72f32644 100644
--- a/test/multipart/bad_robots
+++ b/test/multipart/bad_robots
@@ -1,5 +1,5 @@
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="bbbbbbbbbbbbbbb"
+content-disposition: form-data; name="bbbbbbbbbbbbbbb"
 
 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
@@ -208,52 +208,52 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
 aaaaaaaaaaaaaaaaaaaa
 
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="ccccccc"
+content-disposition: form-data; name="ccccccc"
 
 ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="file.name"
+content-disposition: form-data; name="file.name"
 
 INPUTMSG.gz
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="file.content_type"
+content-disposition: form-data; name="file.content_type"
 
 application/octet-stream
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="file.path"
+content-disposition: form-data; name="file.path"
 
 /var/tmp/uploads/4/0001728414
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="file.md5"
+content-disposition: form-data; name="file.md5"
 
 aa73198feb4b4c1c3186f5e7466cbbcc
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="file.size"
+content-disposition: form-data; name="file.size"
 
 13212
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="size"
+content-disposition: form-data; name="size"
 
 80892
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="mail_server_id"
+content-disposition: form-data; name="mail_server_id"
 
 <1111111111.22222222.3333333333333.JavaMail.app@ffff-aaaa.dddd>
 
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="addresses"
+content-disposition: form-data; name="addresses"
 
 {"campsy_programmer@pinkedum.com":{"domain":"pinkedum.com","name":"Campsy Programmer","type":["env_sender"],"mailbox":"campsy_programmer"},"tex@rapidcity.com":{"domain":"rapidcity.com","name":"Big Tex","type":["env_recipients","to"],"mailbox":"tex"},"group-digests@linkedin.com":{"domain":"linkedin.com","name":"Group Members","type":["from"],"mailbox":"group-digests"}}
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="received_on"
+content-disposition: form-data; name="received_on"
 
 2009-11-15T14:21:11Z
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="id"
+content-disposition: form-data; name="id"
 
 dbfd9804d26d11deab24e3037639bf77
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon
-Content-Disposition: form-data; name="ip_address"
+content-disposition: form-data; name="ip_address"
 
 127.0.0.1
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon--
diff --git a/test/multipart/binary b/test/multipart/binary
index a3bd67c4973fae2a6aaff0f69b50f2bde312d298..ee2e7de2dea352ab65ca72d14e6831c494cb0afa 100644
Binary files a/test/multipart/binary and b/test/multipart/binary differ
diff --git a/test/multipart/content_type_and_no_disposition b/test/multipart/content_type_and_no_disposition
index 8a07dacdff7762767b53b4b45a0c223d61014007..7b8fcb8c109b4357a6e6af92a55abd8f864e89be 100644
--- a/test/multipart/content_type_and_no_disposition
+++ b/test/multipart/content_type_and_no_disposition
@@ -1,5 +1,5 @@
 --AaB03x
-Content-Type: text/plain; charset=US-ASCII
+content-type: text/plain; charset=US-ASCII
 
 contents
 --AaB03x--
diff --git a/test/multipart/content_type_and_no_filename b/test/multipart/content_type_and_no_filename
index bd4c89b0defbd30fb540f63be7bb72c382df5e36..05ce5041b6cae8c5d901fe32512630e453ed0e56 100644
--- a/test/multipart/content_type_and_no_filename
+++ b/test/multipart/content_type_and_no_filename
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="text"
-Content-Type: text/plain; charset=US-ASCII
+content-disposition: form-data; name="text"
+content-type: text/plain; charset=US-ASCII
 
 contents
 --AaB03x--
diff --git a/test/multipart/content_type_and_unknown_charset b/test/multipart/content_type_and_unknown_charset
new file mode 100644
index 0000000000000000000000000000000000000000..cf9c14c7744b1fa0d8aa9567b44dceeeb98381be
--- /dev/null
+++ b/test/multipart/content_type_and_unknown_charset
@@ -0,0 +1,6 @@
+--AaB03x
+content-disposition: form-data; name="text"
+content-type: text/plain; charset=foo; bar=baz
+
+contents
+--AaB03x--
diff --git a/test/multipart/empty b/test/multipart/empty
index f0f79835c96f3f28742b544cd20675da3581c0c7..d0f22e578c2c00ebe261145c6fc2ad5da682b5b6 100644
--- a/test/multipart/empty
+++ b/test/multipart/empty
@@ -1,10 +1,10 @@
 --AaB03x
-Content-Disposition: form-data; name="submit-name"
+content-disposition: form-data; name="submit-name"
 
 Larry
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="file1.txt"
+content-type: text/plain
 
 
 --AaB03x--
diff --git a/test/multipart/end_boundary_first b/test/multipart/end_boundary_first
new file mode 100644
index 0000000000000000000000000000000000000000..282c7ff777248705a355f6560cac9a8679a65d85
--- /dev/null
+++ b/test/multipart/end_boundary_first
@@ -0,0 +1,8 @@
+--AaB03x--
+
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="foo"
+Content-Type: application/octet-stream
+
+contents
+--AaB03x--
diff --git a/test/multipart/fail_16384_nofile b/test/multipart/fail_16384_nofile
index bdcd3320f3f9266756f26c225b832b91a3783877..b10ac5bafe128dee35458c124eead7e7b9883168 100644
--- a/test/multipart/fail_16384_nofile
+++ b/test/multipart/fail_16384_nofile
@@ -1,813 +1,813 @@
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="_method"
+content-disposition: form-data; name="_method"
 
 put
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="authenticity_token"
+content-disposition: form-data; name="authenticity_token"
 
 XCUgSyYsZ+iHQunq/yCSKFzjeVmsXV/WcphHQ0J+05I=
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[SESE]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[SESE]"
 
 BooBar
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[BBBBBBBBB]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[BBBBBBBBB]"
 
 18
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[CCCCCCCCCCCCCCCCCCC]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[CCCCCCCCCCCCCCCCCCC]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[STARTFOO]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[STARTFOO]"
 
 2009-11-04
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ENDFOO]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ENDFOO]"
 
 2009-12-01
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[DDDDDDDD]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[EEEEEEEEEE]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[EEEEEEEEEE]"
 
 10000
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[FFFFFFFFF]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[FFFFFFFFF]"
 
 boskoizcool
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[GGGGGGGGGGG]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[YYYYYYYYYYYYYYY]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[YYYYYYYYYYYYYYY]"
 
 5.00
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ZZZZZZZZZZZZZ]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[ZZZZZZZZZZZZZ]"
 
 mille
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[XXXXXXXXXXXXXXXXXXXXX]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[XXXXXXXXXXXXXXXXXXXXX]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][1][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][2][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][3][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][4][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][5][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][6][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][9]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][9]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][10]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][10]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][11]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][11]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][12]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][12]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][13]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][13]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][14]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][14]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][15]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][15]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][16]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][16]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][17]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][17]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][18]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][18]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][19]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][19]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][20]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][20]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][21]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][21]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][22]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][22]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][23]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][23]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][0]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][0]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][1]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][2]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][2]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][3]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][3]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][4]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][4]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][5]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][5]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][6]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][6]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][7]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][7]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][8]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[VVVVVVVVVVVVVVVVVVVVVVV][0][8]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][ZEZE]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][ZEZE]"
 
 PLAPLAPLAINCINCINC
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][123412341234e]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][123412341234e]"
 
 SITE
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][12345678901]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[WWWWWWWWWWWWWWWWWWWWWWWWW][678][12345678901]"
 
 56
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_type]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_type]"
 
 none
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][has_hashashas_has]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][has_hashashas_has]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_freee]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_freee]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_frefre]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][frefrefre_fre_frefre]"
 
 forever
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][self_block]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][self_block]"
 
 0
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][COUCOUN]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][COUCOUN]"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][REGREG]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][REGREG]"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][c1c1]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_wizard][GGG_RULES][][c1c1]"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA_TARTARTAR_wizard_rule"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA_TARTARTAR_wizard_rule"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_rule]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[TARTARTAR_rule]"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[selection_selection]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[selection_selection]"
 
 R
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][selection_selection]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][selection_selection]"
 
 1
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][ba_unit_id]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-1][ba_unit_id]"
 
 1015
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][selection_selection]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][selection_selection]"
 
 2
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][ba_unit_id]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[PLAPLAPLA_MEMMEMMEMM_ATTRATTRER][new][-2][ba_unit_id]"
 
 1017
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo
-Content-Disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[tile_name]"
+content-disposition: form-data; name="AAAAAAAAAAAAAAAAAAA[tile_name]"
 
 
 ------WebKitFormBoundaryWsY0GnpbI5U7ztzo--
diff --git a/test/multipart/filename_and_modification_param b/test/multipart/filename_and_modification_param
index 20893f4240376bba7427d0c49923746443221206..e9af861846c74954d59103ef548ab3bc61570494 100644
--- a/test/multipart/filename_and_modification_param
+++ b/test/multipart/filename_and_modification_param
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Type: image/jpeg
-Content-Disposition: attachment; name="files"; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
+content-type: image/jpeg
+content-disposition: attachment; name="files"; filename=genome.jpeg; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
 Content-Description: a complete map of the human genome
 
 contents
diff --git a/test/multipart/filename_and_no_name b/test/multipart/filename_and_no_name
index 00d58153d9471c1f16d5e54de5e517e69b617c82..88953d3fe648c976463d0773792513824ad04e51 100644
--- a/test/multipart/filename_and_no_name
+++ b/test/multipart/filename_and_no_name
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; filename="file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; filename="file1.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
diff --git a/test/multipart/filename_multi b/test/multipart/filename_multi
new file mode 100644
index 0000000000000000000000000000000000000000..9ab5e1ef5f933f8dcab00b20eee8eed3cfd7db19
--- /dev/null
+++ b/test/multipart/filename_multi
@@ -0,0 +1,6 @@
+--AaB03x
+Content-Disposition: form-data; name="files"; filename="foo"; filename*=utf-8''bar
+Content-Type: application/octet-stream
+
+contents
+--AaB03x--
diff --git a/test/multipart/filename_with_encoded_words b/test/multipart/filename_with_encoded_words
index 0c89b02a25eb31c6dff9df42581b7f12bfb80897..a2747dfd65f8704619c2e09ac22b4b4788e49d71 100644
--- a/test/multipart/filename_with_encoded_words
+++ b/test/multipart/filename_with_encoded_words
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Type: image/jpeg
-Content-Disposition: attachment; name="files"; filename*=utf-8''%D1%84%D0%B0%D0%B9%D0%BB
+content-type: image/jpeg
+content-disposition: attachment; name="files"; filename*=utf-8''%D1%84%D0%B0%D0%B9%D0%BB
 Content-Description: a complete map of the human genome
 
 contents
diff --git a/test/multipart/filename_with_escaped_quotes b/test/multipart/filename_with_escaped_quotes
index 0a332df70f2a6919c530ed84fbd93dbe79f3a66e..f096db5feac6985cb6b09481cc7c476a07efe539 100644
--- a/test/multipart/filename_with_escaped_quotes
+++ b/test/multipart/filename_with_escaped_quotes
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="escape \"quotes"
-Content-Type: application/octet-stream
+content-disposition: form-data; name="files"; filename="escape \"quotes"
+content-type: application/octet-stream
 
 contents
 --AaB03x--
diff --git a/test/multipart/filename_with_escaped_quotes_and_modification_param b/test/multipart/filename_with_escaped_quotes_and_modification_param
index 929f6ad3f960bd1b51a1a7bc43beddcdb1d6fcda..a1b1ed0c006e6819b2c2e5dfc7631f2a2853d9fb 100644
--- a/test/multipart/filename_with_escaped_quotes_and_modification_param
+++ b/test/multipart/filename_with_escaped_quotes_and_modification_param
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Type: image/jpeg
-Content-Disposition: attachment; name="files"; filename="\"human\" genome.jpeg"; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
+content-type: image/jpeg
+content-disposition: attachment; name="files"; filename="\"human\" genome.jpeg"; modification-date="Wed, 12 Feb 1997 16:29:51 -0500";
 Content-Description: a complete map of the human genome
 
 contents
diff --git a/test/multipart/filename_with_null_byte b/test/multipart/filename_with_null_byte
index 961d44c48985dc759743f01ac1ba97f1b94d0a99..26e28674dbc99da29da9836f62cf0c23d8af1862 100644
--- a/test/multipart/filename_with_null_byte
+++ b/test/multipart/filename_with_null_byte
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Type: image/jpeg
-Content-Disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
+content-type: image/jpeg
+content-disposition: attachment; name="files"; filename="flowers.exe%00.jpg"
 Content-Description: a complete map of the human genome
 
 contents
diff --git a/test/multipart/filename_with_percent_escaped_quotes b/test/multipart/filename_with_percent_escaped_quotes
index 7db06413737c6351d96750d4f56d2fbe6e38e423..af2394b1bb92dfc05d2e8508aa136eb51642788b 100644
--- a/test/multipart/filename_with_percent_escaped_quotes
+++ b/test/multipart/filename_with_percent_escaped_quotes
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="escape %22quotes"
-Content-Type: application/octet-stream
+content-disposition: form-data; name="files"; filename="escape %22quotes"
+content-type: application/octet-stream
 
 contents
 --AaB03x--
diff --git a/test/multipart/filename_with_plus b/test/multipart/filename_with_plus
index aa75022b937827fd16a0c77bc1e230e2b436b8e2..e169a11ee6d47b576506b1b644dafbf40d15d6ce 100644
--- a/test/multipart/filename_with_plus
+++ b/test/multipart/filename_with_plus
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="foo+bar"
-Content-Type: application/octet-stream
+content-disposition: form-data; name="files"; filename="foo+bar"
+content-type: application/octet-stream
 
 contents
 --AaB03x--
diff --git a/test/multipart/filename_with_single_quote b/test/multipart/filename_with_single_quote
index f7220abeef9ab2d3c085770d72ccf2a0dae0e905..8412701b498bb691b704e4d0f11f74efaa9b867e 100644
--- a/test/multipart/filename_with_single_quote
+++ b/test/multipart/filename_with_single_quote
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Type: image/jpeg
-Content-Disposition: attachment; name="files"; filename="bob's flowers.jpg"
+content-type: image/jpeg
+content-disposition: attachment; name="files"; filename="bob's flowers.jpg"
 Content-Description: a complete map of the human genome
 
 contents
diff --git a/test/multipart/filename_with_unescaped_percentages b/test/multipart/filename_with_unescaped_percentages
index f63dd22804fbdf7adca9eddb371f7ae7aa703533..a5ba7aeaf1c001bf9ece013540e0d2c817ce6e1e 100644
--- a/test/multipart/filename_with_unescaped_percentages
+++ b/test/multipart/filename_with_unescaped_percentages
@@ -1,6 +1,6 @@
 ------WebKitFormBoundary2NHc7OhsgU68l3Al
-Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"
-Content-Type: image/jpeg
+content-disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"
+content-type: image/jpeg
 
 contents
 ------WebKitFormBoundary2NHc7OhsgU68l3Al--
diff --git a/test/multipart/filename_with_unescaped_percentages2 b/test/multipart/filename_with_unescaped_percentages2
index 83eac3652180efdcbb25351d49f148bdc4530d07..57023461a3382d42143eb2f73ac3ee6bc72a1e2b 100644
--- a/test/multipart/filename_with_unescaped_percentages2
+++ b/test/multipart/filename_with_unescaped_percentages2
@@ -1,6 +1,6 @@
 ------WebKitFormBoundary2NHc7OhsgU68l3Al
-Content-Disposition: form-data; name="document[attachment]"; filename="100%a"
-Content-Type: image/jpeg
+content-disposition: form-data; name="document[attachment]"; filename="100%a"
+content-type: image/jpeg
 
 contents
 ------WebKitFormBoundary2NHc7OhsgU68l3Al--
diff --git a/test/multipart/filename_with_unescaped_percentages3 b/test/multipart/filename_with_unescaped_percentages3
index 4dba3c8856d98747c0443d53c3cb0252975cd2fa..f8d9114e18f986df0b80c5ea741e166bcbb38344 100644
--- a/test/multipart/filename_with_unescaped_percentages3
+++ b/test/multipart/filename_with_unescaped_percentages3
@@ -1,6 +1,6 @@
 ------WebKitFormBoundary2NHc7OhsgU68l3Al
-Content-Disposition: form-data; name="document[attachment]"; filename="100%"
-Content-Type: image/jpeg
+content-disposition: form-data; name="document[attachment]"; filename="100%"
+content-type: image/jpeg
 
 contents
 ------WebKitFormBoundary2NHc7OhsgU68l3Al--
diff --git a/test/multipart/filename_with_unescaped_quotes b/test/multipart/filename_with_unescaped_quotes
index 9a291e8e08a2d6218fef84e41e7a30004f8f74ab..6cd7c0da1b847f0d340fef296eb5ca1c299ea18e 100644
--- a/test/multipart/filename_with_unescaped_quotes
+++ b/test/multipart/filename_with_unescaped_quotes
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="escape "quotes"
-Content-Type: application/octet-stream
+content-disposition: form-data; name="files"; filename="escape "quotes"
+content-type: application/octet-stream
 
 contents
 --AaB03x--
diff --git a/test/multipart/ie b/test/multipart/ie
index eae06ab5b4da7578f720781651093d26afe3aad5..ac8151b12f3ef5ad4bf5750b4f0cfefbadebe2e3 100644
--- a/test/multipart/ie
+++ b/test/multipart/ie
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
diff --git a/test/multipart/invalid_character b/test/multipart/invalid_character
index 82467181a77cf871ba054cffc30cd3556206fbfd..324e6400621f5d7b85093a93a334ee9441eff060 100644
--- a/test/multipart/invalid_character
+++ b/test/multipart/invalid_character
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="invalidÃ.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="invalidÃ.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
diff --git a/test/multipart/mixed_files b/test/multipart/mixed_files
index 624d8045bd5736b393262f684bdf6de4cd24a359..253ca74cbe02576852834bc03ee30f09c93d323b 100644
--- a/test/multipart/mixed_files
+++ b/test/multipart/mixed_files
@@ -1,20 +1,20 @@
 --AaB03x
-Content-Disposition: form-data; name="foo"
+content-disposition: form-data; name="foo"
 
 bar
 --AaB03x
-Content-Disposition: form-data; name="files"
-Content-Type: multipart/mixed, boundary=BbC04y
+content-disposition: form-data; name="files"
+content-type: multipart/mixed, boundary=BbC04y
 
 --BbC04y
-Content-Disposition: attachment; filename="file.txt"
-Content-Type: text/plain
+content-disposition: attachment; filename="file.txt"
+content-type: text/plain
 
 contents
 --BbC04y
-Content-Disposition: attachment; filename="flowers.jpg"
-Content-Type: image/jpeg
-Content-Transfer-Encoding: binary
+content-disposition: attachment; filename="flowers.jpg"
+content-type: image/jpeg
+content-transfer-encoding: binary
 
 contents
 --BbC04y--
diff --git a/test/multipart/nested b/test/multipart/nested
index 51978824452a2fc351c4d92d444d80879b2a76ca..054ad3d00279af18601b64f8696b4110fc22cda8 100644
--- a/test/multipart/nested
+++ b/test/multipart/nested
@@ -1,10 +1,10 @@
 --AaB03x
-Content-Disposition: form-data; name="foo[submit-name]"
+content-disposition: form-data; name="foo[submit-name]"
 
 Larry
 --AaB03x
-Content-Disposition: form-data; name="foo[files]"; filename="file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="foo[files]"; filename="file1.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
diff --git a/test/multipart/none b/test/multipart/none
index d66f4730f13c485ba8d86b546735b0fdebaa1c5f..121f85fc91be52541bd689d2f4854718ae0c9fd6 100644
--- a/test/multipart/none
+++ b/test/multipart/none
@@ -1,9 +1,9 @@
 --AaB03x
-Content-Disposition: form-data; name="submit-name"
+content-disposition: form-data; name="submit-name"
 
 Larry
 --AaB03x
-Content-Disposition: form-data; name="files"; filename=""
+content-disposition: form-data; name="files"; filename=""
 
 
 --AaB03x--
diff --git a/test/multipart/preceding_boundary b/test/multipart/preceding_boundary
new file mode 100644
index 0000000000000000000000000000000000000000..b65e647b67f615181ea4894218313d821f783f56
--- /dev/null
+++ b/test/multipart/preceding_boundary
@@ -0,0 +1,6 @@
+A--AaB03x
+Content-Disposition: form-data; name="files"; filename="foo"
+Content-Type: application/octet-stream
+
+contents
+--AaB03x--
diff --git a/test/multipart/quoted b/test/multipart/quoted
index cf4e9b64897096cda23d2aa3e1198b7db9e952d1..8593bc20a2ecfd80e2ff4530cd93c45354c88796 100644
--- a/test/multipart/quoted
+++ b/test/multipart/quoted
@@ -1,15 +1,15 @@
 --AaB:03x
-Content-Disposition: form-data; name="submit-name"
+content-disposition: form-data; name="submit-name"
 
 Larry
 --AaB:03x
-Content-Disposition: form-data; name="submit-name-with-content"
-Content-Type: text/plain
+content-disposition: form-data; name="submit-name-with-content"
+content-type: text/plain
 
 Berry
 --AaB:03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="file1.txt"
+content-type: text/plain
 
 contents
 --AaB:03x--
diff --git a/test/multipart/robust_field_separation b/test/multipart/robust_field_separation
index 34956b150c3263f6329bd620255375c77e23d895..9fca06350bc3c3594ae1ebd3e828bda7a1616e39 100644
--- a/test/multipart/robust_field_separation
+++ b/test/multipart/robust_field_separation
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data;name="text"
-Content-Type: text/plain
+content-disposition: form-data;name="text"
+content-type: text/plain
 
 contents
 --AaB03x--
diff --git a/test/multipart/semicolon b/test/multipart/semicolon
index 00fd68ab85084bdf44c177fd34fb9cac1c101e44..d3702a2ba1e01b32ebec90136a03582251cf6a4c 100644
--- a/test/multipart/semicolon
+++ b/test/multipart/semicolon
@@ -1,6 +1,6 @@
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="fi;le1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="fi;le1.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
\ No newline at end of file
diff --git a/test/multipart/space case.txt b/test/multipart/space case.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0839b2e9412b314cb8bb9a20f587aa13752ae310
--- /dev/null
+++ b/test/multipart/space case.txt	
@@ -0,0 +1 @@
+contents
\ No newline at end of file
diff --git a/test/multipart/text b/test/multipart/text
index 01376d02fe5c4fabe5f4dcceaebaa236fd05df75..ac340228cf65cc74ec7520a43d13c363bac811f4 100644
--- a/test/multipart/text
+++ b/test/multipart/text
@@ -1,15 +1,15 @@
 --AaB03x
-Content-Disposition: form-data; name="submit-name"
+content-disposition: form-data; name="submit-name"
 
 Larry
 --AaB03x
-Content-Disposition: form-data; name="submit-name-with-content"
-Content-Type: text/plain
+content-disposition: form-data; name="submit-name-with-content"
+content-type: text/plain
 
 Berry
 --AaB03x
-Content-Disposition: form-data; name="files"; filename="file1.txt"
-Content-Type: text/plain
+content-disposition: form-data; name="files"; filename="file1.txt"
+content-type: text/plain
 
 contents
 --AaB03x--
\ No newline at end of file
diff --git a/test/multipart/three_files_three_fields b/test/multipart/three_files_three_fields
index 40d88b56c5df2db7480a9d3552dade4864ecadfa..8917424eaad8f939ef71ac14aa4a6744c0600454 100644
--- a/test/multipart/three_files_three_fields
+++ b/test/multipart/three_files_three_fields
@@ -12,20 +12,20 @@ content-disposition: form-data; name="from"
 others
 --AaB03x
 content-disposition: form-data; name="fileupload1"; filename="file1.jpg"
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
+content-type: image/jpeg
+content-transfer-encoding: base64
 
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
 --AaB03x
 content-disposition: form-data; name="fileupload2"; filename="file2.jpg"
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
+content-type: image/jpeg
+content-transfer-encoding: base64
 
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
 --AaB03x
 content-disposition: form-data; name="fileupload3"; filename="file3.jpg"
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
+content-type: image/jpeg
+content-transfer-encoding: base64
 
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
 --AaB03x--
diff --git a/test/multipart/webkit b/test/multipart/webkit
index 1375af317bda6ae0c402162f2277d602a69930e1..044d4db375ef4b1740720432247ca4703dee0ea2 100644
--- a/test/multipart/webkit
+++ b/test/multipart/webkit
@@ -1,32 +1,32 @@
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="_method"
+content-disposition: form-data; name="_method"
 
 put
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="profile[blog]"
+content-disposition: form-data; name="profile[blog]"
 
 
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="profile[public_email]"
+content-disposition: form-data; name="profile[public_email]"
 
 
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="profile[interests]"
+content-disposition: form-data; name="profile[interests]"
 
 
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="profile[bio]"
+content-disposition: form-data; name="profile[bio]"
 
 hello
 
 "quote"
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="media"; filename=""
+content-disposition: form-data; name="media"; filename=""
 Content-Type: application/octet-stream
 
 
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR
-Content-Disposition: form-data; name="commit"
+content-disposition: form-data; name="commit"
 
 Save
 ------WebKitFormBoundaryWLHCs9qmcJJoyjKR--
diff --git a/test/rackup/config.ru b/test/rackup/config.ru
index fa9b6ecab50cbd3b7a1eabafa520d07710823738..267ffb506ab43ad4c6be9aa1a2f658b98526f86d 100644
--- a/test/rackup/config.ru
+++ b/test/rackup/config.ru
@@ -1,6 +1,6 @@
 # frozen_string_literal: true
 
-require "#{File.dirname(__FILE__)}/../testrequest"
+require_relative "../test_request"
 
 $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
deleted file mode 100644
index 21b6051676cb8ffe2cf2a3bb1e226accb30ae634..0000000000000000000000000000000000000000
--- a/test/registering_handler/rack/handler/registering_myself.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  module Handler
-    class RegisteringMyself
-    end
-
-    register :registering_myself, RegisteringMyself
-  end
-end
diff --git a/test/spec_auth_basic.rb b/test/spec_auth_basic.rb
index 7d39b195260deeb11828f447c6c4e2d7a6a8d7d7..ee7000495e6af27e690903d07235a0a38902a2e2 100644
--- a/test/spec_auth_basic.rb
+++ b/test/spec_auth_basic.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/auth/basic'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/lint'
+end
+
 describe Rack::Auth::Basic do
   def realm
     'WallysWorld'
@@ -9,7 +15,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
 
@@ -34,8 +40,8 @@ describe Rack::Auth::Basic do
   def assert_basic_auth_challenge(response)
     response.must_be :client_error?
     response.status.must_equal 401
-    response.must_include 'WWW-Authenticate'
-    response.headers['WWW-Authenticate'].must_match(/Basic realm="#{Regexp.escape(realm)}"/)
+    response.must_include 'www-authenticate'
+    response.headers['www-authenticate'].must_match(/Basic realm="#{Regexp.escape(realm)}"/)
     response.body.must_be :empty?
   end
 
@@ -62,7 +68,7 @@ describe Rack::Auth::Basic do
     request 'HTTP_AUTHORIZATION' => 'Digest params' do |response|
       response.must_be :client_error?
       response.status.must_equal 400
-      response.wont_include 'WWW-Authenticate'
+      response.wont_include 'www-authenticate'
     end
   end
 
@@ -70,7 +76,7 @@ describe Rack::Auth::Basic do
     request 'HTTP_AUTHORIZATION' => '' do |response|
       response.must_be :client_error?
       response.status.must_equal 400
-      response.wont_include 'WWW-Authenticate'
+      response.wont_include 'www-authenticate'
     end
   end
 
@@ -86,7 +92,7 @@ describe Rack::Auth::Basic do
     request 'HTTP_AUTHORIZATION' => auth do |response|
       response.must_be :client_error?
       response.status.must_equal 400
-      response.wont_include 'WWW-Authenticate'
+      response.wont_include 'www-authenticate'
     end
   end
 
diff --git a/test/spec_auth_digest.rb b/test/spec_auth_digest.rb
index 6e32152f401c377e1d9c07136e5327039b1a463b..3a8981c5d96b18faa025213575e06f1ddd9f6b0d 100644
--- a/test/spec_auth_digest.rb
+++ b/test/spec_auth_digest.rb
@@ -2,6 +2,16 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/auth/digest/md5'
+  require_relative '../lib/rack/auth/digest/nonce'
+  require_relative '../lib/rack/auth/digest/params'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/urlmap'
+  require_relative '../lib/rack/method_override'
+end
+
 describe Rack::Auth::Digest::MD5 do
   def realm
     'WallysWorld'
@@ -10,7 +20,7 @@ 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
 
@@ -81,7 +91,7 @@ describe Rack::Auth::Digest::MD5 do
       sleep wait
     end
 
-    challenge = response['WWW-Authenticate'].split(' ', 2).last
+    challenge = response['www-authenticate'].split(' ', 2).last
 
     params = Rack::Auth::Digest::Params.parse(challenge)
 
@@ -102,15 +112,15 @@ describe Rack::Auth::Digest::MD5 do
   def assert_digest_auth_challenge(response)
     response.must_be :client_error?
     response.status.must_equal 401
-    response.must_include 'WWW-Authenticate'
-    response.headers['WWW-Authenticate'].must_match(/^Digest /)
+    response.must_include 'www-authenticate'
+    response.headers['www-authenticate'].must_match(/^Digest /)
     response.body.must_be :empty?
   end
 
   def assert_bad_request(response)
     response.must_be :client_error?
     response.status.must_equal 400
-    response.wont_include 'WWW-Authenticate'
+    response.wont_include 'www-authenticate'
   end
 
   it 'challenge when no credentials are specified' do
@@ -160,7 +170,7 @@ describe Rack::Auth::Digest::MD5 do
       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/)
+        response.headers['www-authenticate'].wont_match(/\bstale=true\b/)
       end
     ensure
       Rack::Auth::Digest::Nonce.time_limit = nil
@@ -173,7 +183,7 @@ describe Rack::Auth::Digest::MD5 do
 
       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/)
+        response.headers['www-authenticate'].must_match(/\bstale=true\b/)
       end
     ensure
       Rack::Auth::Digest::Nonce.time_limit = nil
@@ -263,6 +273,7 @@ describe Rack::Auth::Digest::MD5 do
     req.respond_to?(:nonce).must_equal true
     req.respond_to?(:a).must_equal true
     req.a.must_equal 'b'
+    proc{req.missing}.must_raise NoMethodError
     lambda { req.a(2) }.must_raise ArgumentError
   end
 
@@ -270,4 +281,8 @@ describe Rack::Auth::Digest::MD5 do
     Rack::Auth::Digest::Nonce.new.fresh?.must_equal true
     Rack::Auth::Digest::Nonce.new.stale?.must_equal false
   end
+
+  it 'Params.new can be called without a block' do
+    Rack::Auth::Digest::Params.new.must_be_instance_of(Rack::Auth::Digest::Params)
+  end
 end
diff --git a/test/spec_body_proxy.rb b/test/spec_body_proxy.rb
index 1199f2f18e42db06a53b943174b05417cf28c24b..91fcbd74e856ede0d1fc9cd5bf0ef9166058d6e8 100644
--- a/test/spec_body_proxy.rb
+++ b/test/spec_body_proxy.rb
@@ -2,6 +2,10 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/body_proxy'
+end
+
 describe Rack::BodyProxy do
   it 'call each on the wrapped body' do
     called = false
diff --git a/test/spec_builder.rb b/test/spec_builder.rb
index c0f59c1828cb14193b3ecfa3e60b72ca94eeaa8e..2cc7732c7fa6e2418021917a8e3964120b31568a 100644
--- a/test/spec_builder.rb
+++ b/test/spec_builder.rb
@@ -2,6 +2,15 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/builder'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/content_length'
+  require_relative '../lib/rack/show_exceptions'
+  require_relative '../lib/rack/auth/basic'
+end
+
 class NothingMiddleware
   def initialize(app, **)
     @app = app
@@ -25,13 +34,20 @@ describe Rack::Builder do
     Rack::Lint.new Rack::Builder.new(&block).to_app
   end
 
+  it "supports run with block" do
+    app = builder_to_app do
+      run {|env| [200, { "content-type" => "text/plain" }, ["OK"]]}
+    end
+    Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
+  end
+
   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'
@@ -42,13 +58,13 @@ describe Rack::Builder do
     app = builder_to_app do
       map '/sub' do
         use Rack::ContentLength
-        run lambda { |inner_env| [200, { "Content-Type" => "text/plain" }, ['sub']] }
+        run lambda { |inner_env| [200, { "content-type" => "text/plain" }, ['sub']] }
       end
       use Rack::ContentLength
-      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("/").headers['Content-Length'].must_equal '4'
-    Rack::MockRequest.new(app).get("/sub").headers['Content-Length'].must_equal '3'
+    Rack::MockRequest.new(app).get("/").headers['content-length'].must_equal '4'
+    Rack::MockRequest.new(app).get("/sub").headers['content-length'].must_equal '3'
   end
 
   it "doesn't dupe env even when mapping" do
@@ -57,7 +73,7 @@ describe Rack::Builder do
       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
@@ -68,7 +84,7 @@ describe Rack::Builder do
   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]] }
+        run lambda { |env|  [200, { "content-type" => "text/plain" }, [object_id.to_s]] }
       end
     end
 
@@ -107,7 +123,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("/")
@@ -135,9 +151,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'
@@ -174,7 +190,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
 
@@ -229,12 +245,11 @@ describe Rack::Builder do
       File.join(File.dirname(__FILE__), 'builder', name)
     end
 
-    it "parses commented options" do
-      app, options = Rack::Builder.parse_file config_file('options.ru')
-      options[:debug].must_equal true
-      options[:environment].must_equal 'test'
-      options[:Port].must_equal '2929'
-      Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
+    it "raises if parses commented options" do
+      proc do
+        Rack::Builder.parse_file config_file('options.ru')
+      end.must_raise(RuntimeError).
+       message.must_include('Parsing options from the first comment line is no longer supported')
     end
 
     it "removes __END__ before evaluating app" do
@@ -243,9 +258,8 @@ describe Rack::Builder do
     end
 
     it "supports multi-line comments" do
-      proc, env = Rack::Builder.parse_file(config_file('comment.ru'))
-      proc.must_be_kind_of Proc
-      env.must_equal({})
+      app = Rack::Builder.parse_file(config_file('comment.ru'))
+      app.must_be_kind_of(Proc)
     end
 
     it 'requires an_underscore_app not ending in .ru' do
@@ -263,11 +277,13 @@ describe Rack::Builder do
     it "strips leading unicode byte order mark when present" do
       enc = Encoding.default_external
       begin
+        verbose, $VERBOSE = $VERBOSE, nil
         Encoding.default_external = 'UTF-8'
         app, _ = Rack::Builder.parse_file config_file('bom.ru')
         Rack::MockRequest.new(app).get("/").body.to_s.must_equal 'OK'
       ensure
         Encoding.default_external = enc
+        $VERBOSE = verbose
       end
     end
 
@@ -283,7 +299,7 @@ describe Rack::Builder do
 
   describe 'new_from_string' do
     it "builds a rack app from string" do
-      app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'Content-Type' => 'text/plane'}, ['OK']] }"
+      app, = Rack::Builder.new_from_string "run lambda{|env| [200, {'content-type' => 'text/plane'}, ['OK']] }"
       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 8f1fd131ce915c935a0b3060e162fa86cb1bbafd..50dd2805b33c00108015c97a663b92342bd20ed8 100644
--- a/test/spec_cascade.rb
+++ b/test/spec_cascade.rb
@@ -2,6 +2,14 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/cascade'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/urlmap'
+  require_relative '../lib/rack/files'
+end
+
 describe Rack::Cascade do
   def cascade(*args)
     Rack::Lint.new Rack::Cascade.new(*args)
@@ -13,7 +21,7 @@ describe Rack::Cascade do
   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])
@@ -47,17 +55,17 @@ describe Rack::Cascade do
     res = app.call('/')
     s, h, body = res
     s.must_equal 404
-    h['Content-Type'].must_equal 'text/plain'
+    h['content-type'].must_equal 'text/plain'
     body.must_be_empty
 
     res[0] = 200
-    h['Content-Type'] = 'text/html'
+    h['content-type'] = 'text/html'
     body << "a"
 
     res = app.call('/')
     s, h, body = res
     s.must_equal 404
-    h['Content-Type'].must_equal 'text/plain'
+    h['content-type'].must_equal 'text/plain'
     body.must_be_empty
   end
 
diff --git a/test/spec_chunked.rb b/test/spec_chunked.rb
index ceb7bdfb2e1df2e03b4a2fb77e25ccb3474347c0..4ba4eefbc5aa81770d3e1471c4600ca2dd3d1427 100644
--- a/test/spec_chunked.rb
+++ b/test/spec_chunked.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/chunked'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Chunked do
   def chunked(app)
     proc do |env|
@@ -24,33 +30,49 @@ describe Rack::Chunked do
     end
 
     def trailers
-      { "Expires" => "tomorrow" }
+      { "expires" => "tomorrow" }
     end
   end
 
   it 'yields trailer headers after the response' do
     app = lambda { |env|
-      [200, { "Content-Type" => "text/plain", "Trailer" => "Expires" }, TrailerBody.new]
+      [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"
+    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!']] }
+  it 'chunk responses with no content-length' do
+    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'
+    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\n\r\n"
   end
 
+  it 'avoid empty chunks' do
+    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'
+    response.body.must_equal "5\r\nHello\r\n6\r\nWorld!\r\n0\r\n\r\n"
+  end
+
+  it 'handles unclosable bodies' do
+    app = lambda { |env| [200, { "content-type" => "text/plain" }, ['Hello', '', 'World!']] }
+    response = Rack::MockResponse.new(*Rack::Chunked.new(app).call(@env))
+    response.headers.wont_include 'content-length'
+    response.headers['transfer-encoding'].must_equal 'chunked'
+    response.body.must_equal "5\r\nHello\r\n6\r\nWorld!\r\n0\r\n\r\n"
+  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'
+    response.headers.wont_include 'content-length'
+    response.headers['transfer-encoding'].must_equal 'chunked'
     response.body.must_equal "0\r\n\r\n"
   end
 
@@ -59,68 +81,65 @@ describe Rack::Chunked do
     closed = false
     def obj.each; yield 's' end
     obj.define_singleton_method(:close) { closed = true }
-    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, obj] }
+    app = lambda { |env| [200, { "content-type" => "text/plain" }, obj] }
     response = Rack::MockRequest.new(Rack::Chunked.new(app)).get('/', @env)
-    response.headers.wont_include 'Content-Length'
-    response.headers['Transfer-Encoding'].must_equal 'chunked'
+    response.headers.wont_include 'content-length'
+    response.headers['transfer-encoding'].must_equal 'chunked'
     response.body.must_equal "1\r\ns\r\n0\r\n\r\n"
     closed.must_equal true
   end
 
   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.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".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
+  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
-    headers.wont_include 'Transfer-Encoding'
-    headers.must_include 'Content-Length'
+    headers.wont_include 'transfer-encoding'
+    headers.must_include 'content-length'
     body.join.must_equal 'Hello World!'
   end
 
   it 'not modify response when client is HTTP/1.0' do
-    app = lambda { |env| [200, { "Content-Type" => "text/plain" }, ['Hello', ' ', 'World!']] }
+    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'
+    headers.wont_include 'transfer-encoding'
     body.join.must_equal 'Hello World!'
   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
-      headers.wont_include 'Transfer-Encoding'
+      headers.wont_include 'transfer-encoding'
       body.join.must_equal 'Hello World!'
     end
 
-    @env.delete('SERVER_PROTOCOL') # unicorn will do this on pre-HTTP/1.0 requests
-    check.call
-
     @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
+  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
-    headers['Transfer-Encoding'].must_equal 'identity'
+    headers['transfer-encoding'].must_equal 'identity'
     body.join.must_equal 'Hello World!'
   end
 
@@ -129,7 +148,7 @@ describe Rack::Chunked do
       app = lambda { |env| [status_code, {}, []] }
       status, headers, _ = chunked(app).call(@env)
       status.must_equal status_code
-      headers.wont_include 'Transfer-Encoding'
+      headers.wont_include 'transfer-encoding'
     end
   end
 end
diff --git a/test/spec_common_logger.rb b/test/spec_common_logger.rb
index 4ddb5f03d374214e78467587e65b100f6f0d93f2..595debd47a1d0f9e41c0a47a3670ce25d580dafa 100644
--- a/test/spec_common_logger.rb
+++ b/test/spec_common_logger.rb
@@ -3,21 +3,27 @@
 require_relative 'helper'
 require 'logger'
 
+separate_testing do
+  require_relative '../lib/rack/common_logger'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::CommonLogger do
   obj = 'foobar'
   length = obj.size
 
   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" },
      []]}
   app_without_lint = lambda { |env|
     [200,
@@ -28,14 +34,14 @@ describe Rack::CommonLogger do
     res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/")
 
     res.errors.wont_be :empty?
-    res.errors.must_match(/"GET \/ " 200 #{length} /)
+    res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
   end
 
   it "log to anything with +write+" do
     log = StringIO.new
     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
 
-    log.string.must_match(/"GET \/ " 200 #{length} /)
+    log.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
   end
 
   it "work with standard library logger" do
@@ -43,21 +49,35 @@ describe Rack::CommonLogger do
     log = Logger.new(logdev)
     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
 
-    logdev.string.must_match(/"GET \/ " 200 #{length} /)
+    logdev.string.must_match(/"GET \/ HTTP\/1\.1" 200 #{length} /)
   end
 
   it "log - content length if header is missing" do
     res = Rack::MockRequest.new(Rack::CommonLogger.new(app_without_length)).get("/")
 
     res.errors.wont_be :empty?
-    res.errors.must_match(/"GET \/ " 200 - /)
+    res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /)
   end
 
   it "log - content length if header is zero" do
     res = Rack::MockRequest.new(Rack::CommonLogger.new(app_with_zero_length)).get("/")
 
     res.errors.wont_be :empty?
-    res.errors.must_match(/"GET \/ " 200 - /)
+    res.errors.must_match(/"GET \/ HTTP\/1\.1" 200 - /)
+  end
+
+  it "log - records host from X-Forwarded-For header" do
+    res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_X_FORWARDED_FOR' => '203.0.113.0')
+
+    res.errors.wont_be :empty?
+    res.errors.must_match(/203\.0\.113\.0 - /)
+  end
+
+  it "log - records host from RFC 7239 forwarded for header" do
+    res = Rack::MockRequest.new(Rack::CommonLogger.new(app)).get("/", 'HTTP_FORWARDED' => 'for=203.0.113.0')
+
+    res.errors.wont_be :empty?
+    res.errors.must_match(/203\.0\.113\.0 - /)
   end
 
   def with_mock_time(t = 0)
@@ -75,10 +95,10 @@ describe Rack::CommonLogger do
   it "log in common log format" do
     log = StringIO.new
     with_mock_time do
-      Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/")
+      Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/", 'QUERY_STRING' => 'foo=bar')
     end
 
-    md = /- - - \[([^\]]+)\] "(\w+) \/ " (\d{3}) \d+ ([\d\.]+)/.match(log.string)
+    md = /- - - \[([^\]]+)\] "(\w+) \/\?foo=bar HTTP\/1\.1" (\d{3}) \d+ ([\d\.]+)/.match(log.string)
     md.wont_equal nil
     time, method, status, duration = *md.captures
     time.must_equal Time.at(0).strftime("%d/%b/%Y:%H:%M:%S %z")
@@ -90,9 +110,9 @@ describe Rack::CommonLogger do
   it "escapes non printable characters except newline" do
     logdev = StringIO.new
     log = Logger.new(logdev)
-    Rack::MockRequest.new(Rack::CommonLogger.new(app_without_lint, log)).request("GET\b", "/hello")
+    Rack::MockRequest.new(Rack::CommonLogger.new(app_without_lint, log)).request("GET\x1f", "/hello")
 
-    logdev.string.must_match(/GET\\x8 \/hello/)
+    logdev.string.must_match(/GET\\x1f \/hello HTTP\/1\.1/)
   end
 
   it "log path with PATH_INFO" do
@@ -100,7 +120,7 @@ describe Rack::CommonLogger do
     log = Logger.new(logdev)
     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/hello")
 
-    logdev.string.must_match(/"GET \/hello " 200 #{length} /)
+    logdev.string.must_match(/"GET \/hello HTTP\/1\.1" 200 #{length} /)
   end
 
   it "log path with SCRIPT_NAME" do
@@ -108,7 +128,15 @@ describe Rack::CommonLogger do
     log = Logger.new(logdev)
     Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", script_name: "/script")
 
-    logdev.string.must_match(/"GET \/script\/path " 200 #{length} /)
+    logdev.string.must_match(/"GET \/script\/path HTTP\/1\.1" 200 #{length} /)
+  end
+
+  it "log path with SERVER_PROTOCOL" do
+    logdev = StringIO.new
+    log = Logger.new(logdev)
+    Rack::MockRequest.new(Rack::CommonLogger.new(app, log)).get("/path", http_version: "HTTP/1.0")
+
+    logdev.string.must_match(/"GET \/path HTTP\/1\.0" 200 #{length} /)
   end
 
   def length
diff --git a/test/spec_conditional_get.rb b/test/spec_conditional_get.rb
index 5d517be4dad6276d94bbf8ee5d38f7339d2e1c01..d028b3dec60bfe521743d10e747d86928c1df535 100644
--- a/test/spec_conditional_get.rb
+++ b/test/spec_conditional_get.rb
@@ -3,15 +3,21 @@
 require_relative 'helper'
 require 'time'
 
+separate_testing do
+  require_relative '../lib/rack/conditional_get'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ConditionalGet do
   def conditional_get(app)
     Rack::Lint.new Rack::ConditionalGet.new(app)
   end
 
-  it "set a 304 status and truncate body when If-Modified-Since hits" 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)
@@ -20,20 +26,36 @@ describe Rack::ConditionalGet do
     response.body.must_be :empty?
   end
 
-  it "set a 304 status and truncate body when If-Modified-Since hits and is higher than current time" 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']] })
+
+    response = Rack::MockRequest.new(app).
+      get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
+
+    response.status.must_equal 304
+    response.body.must_be :empty?
+  end
+
+  it "closes bodies" do
+    body = Object.new
+    def body.each; yield 'TEST' end
+    closed = false
+    body.define_singleton_method(:close){closed = true}
     app = conditional_get(lambda { |env|
-      [200, { 'Last-Modified' => (Time.now - 3600).httpdate }, ['TEST']] })
+      [200, { 'last-modified' => (Time.now - 3600).httpdate }, body] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
 
     response.status.must_equal 304
     response.body.must_be :empty?
+    closed.must_equal true
   end
 
-  it "set a 304 status and truncate body when If-None-Match hits" 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')
@@ -42,9 +64,9 @@ describe Rack::ConditionalGet do
     response.body.must_be :empty?
   end
 
-  it "set a 304 status and truncate body when If-None-Match hits but If-Modified-Since is after Last-Modified" do
+  it "set a 304 status and truncate body when if-none-match hits but if-modified-since is after last-modified" do
     app = conditional_get(lambda { |env|
-      [200, { 'Last-Modified' => (Time.now + 3600).httpdate, 'Etag' => '1234', 'Content-Type' => 'text/plain' }, ['TEST']] })
+      [200, { 'last-modified' => (Time.now + 3600).httpdate, 'etag' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
 
     response = Rack::MockRequest.new(app).
       get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate, 'HTTP_IF_NONE_MATCH' => '1234')
@@ -53,10 +75,21 @@ describe Rack::ConditionalGet do
     response.body.must_be :empty?
   end
 
-  it "not set a 304 status if If-Modified-Since hits but Etag does not" do
+  it "not set a 304 status if last-modified is too short" do
+    app = conditional_get(lambda { |env|
+      [200, { 'last-modified' => '1234', 'content-type' => 'text/plain' }, ['TEST']] })
+
+    response = Rack::MockRequest.new(app).
+      get("/", 'HTTP_IF_MODIFIED_SINCE' => Time.now.httpdate)
+
+    response.status.must_equal 200
+    response.body.must_equal 'TEST'
+  end
+
+  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')
@@ -65,10 +98,10 @@ describe Rack::ConditionalGet do
     response.body.must_equal 'TEST'
   end
 
-  it "set a 304 status and truncate body when both If-None-Match and If-Modified-Since hits" 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')
@@ -79,7 +112,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')
@@ -90,7 +123,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')
@@ -102,7 +135,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 304ef8bf715a67026821b03f717927b090b5511c..ce5f777776342aed616135bfe52f1042bcbc9474 100644
--- a/test/spec_config.rb
+++ b/test/spec_config.rb
@@ -2,6 +2,13 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/config'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/builder'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Config do
   it "accept a block that modifies the environment" do
     app = Rack::Builder.new do
@@ -10,7 +17,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 07a4c56e72ce7f5bd81160d9f7851fe494f826e6..cffe8cb45957b613994dd53b0c9f81df23c4d580 100644
--- a/test/spec_content_length.rb
+++ b/test/spec_content_length.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/content_length'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ContentLength do
   def content_length(app)
     Rack::Lint.new Rack::ContentLength.new(app)
@@ -11,60 +17,58 @@ describe Rack::ContentLength do
     Rack::MockRequest.env_for
   end
 
-  it "set Content-Length on Array bodies if none is set" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
+  it "set content-length on Array bodies if none is set" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
     response = content_length(app).call(request)
-    response[1]['Content-Length'].must_equal '13'
+    response[1]['content-length'].must_equal '13'
   end
 
-  it "set Content-Length on variable length bodies" do
+  it "not set content-length on variable length bodies" 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_equal '12'
+    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!"] }
+  it "not change content-length if it is already set" do
+    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'
+    response[1]['content-length'].must_equal '1'
   end
 
-  it "not set Content-Length on 304 responses" do
+  it "not set content-length on 304 responses" do
     app = lambda { |env| [304, {}, []] }
     response = content_length(app).call(request)
-    response[1]['Content-Length'].must_be_nil
+    response[1]['content-length'].must_be_nil
   end
 
-  it "not set Content-Length when Transfer-Encoding is chunked" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Transfer-Encoding' => 'chunked' }, []] }
+  it "not set content-length when transfer-encoding is chunked" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain', 'transfer-encoding' => 'chunked' }, []] }
     response = content_length(app).call(request)
-    response[1]['Content-Length'].must_be_nil
+    response[1]['content-length'].must_be_nil
   end
 
   # Using "Connection: close" for this is fairly contended. It might be useful
   # to have some other way to signal this.
   #
-  # should "not force a Content-Length when Connection:close" do
+  # should "not force a content-length when Connection:close" do
   #   app = lambda { |env| [200, {'Connection' => 'close'}, []] }
   #   response = content_length(app).call({})
-  #   response[1]['Content-Length'].must_be_nil
+  #   response[1]['content-length'].must_be_nil
   # end
 
   it "close bodies that need to be closed" do
     body = Struct.new(:body) do
       attr_reader :closed
-      def each; body.join; end
+      def each; body.each {|b| yield b}; close; end
       def close; @closed = true; end
-      def to_ary; end
+      def to_ary; enum_for.to_a; end
     end.new(%w[one two three])
 
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
-    response = content_length(app).call(request)
-    body.closed.must_be_nil
-    response[2].close
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
+    content_length(app).call(request)
     body.closed.must_equal true
   end
 
@@ -73,13 +77,13 @@ describe Rack::ContentLength do
       def each
         yield body.shift until body.empty?
       end
-      def to_ary; end
+      def to_ary; enum_for.to_a; 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
+    response[1]['content-length'].must_equal expected.join.size.to_s
     response[2].to_enum.to_a.must_equal expected
   end
 end
diff --git a/test/spec_content_type.rb b/test/spec_content_type.rb
index 4cfc32231f84e15f2af996eaa541fcb6c4a8cdca..fff687080f536312c1e4d7ddbc8c814127615367 100644
--- a/test/spec_content_type.rb
+++ b/test/spec_content_type.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/content_type'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ContentType do
   def content_type(app, *args)
     Rack::Lint.new Rack::ContentType.new(app, *args)
@@ -11,45 +17,30 @@ describe Rack::ContentType do
     Rack::MockRequest.env_for
   end
 
-  it "set Content-Type to default text/html if none is set" do
+  it "set content-type to default text/html if none is set" do
     app = lambda { |env| [200, {}, "Hello, World!"] }
     headers = content_type(app).call(request)[1]
-    headers['Content-Type'].must_equal 'text/html'
+    headers['content-type'].must_equal 'text/html'
   end
 
-  it "set Content-Type to chosen default if none is set" do
+  it "set content-type to chosen default if none is set" do
     app = lambda { |env| [200, {}, "Hello, World!"] }
     headers =
       content_type(app, 'application/octet-stream').call(request)[1]
-    headers['Content-Type'].must_equal 'application/octet-stream'
-  end
-
-  it "not change Content-Type if it is already set" do
-    app = lambda { |env| [200, { 'Content-Type' => 'foo/bar' }, "Hello, World!"] }
-    headers = content_type(app).call(request)[1]
-    headers['Content-Type'].must_equal 'foo/bar'
+    headers['content-type'].must_equal 'application/octet-stream'
   end
 
-  it "detect Content-Type case insensitive" do
-    app = lambda { |env| [200, { 'CONTENT-Type' => 'foo/bar' }, "Hello, World!"] }
+  it "not change content-type if it is already set" do
+    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['content-type'].must_equal '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
-
-  ['100', '204', '304'].each do |code|
-    it "not set Content-Type on #{code} responses if status is a string" do
+    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
+      response[1]['content-type'].must_be_nil
     end
   end
 end
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index ed9cffeca63f362c9e9c51054bdded6a0b7912cd..9d6e81f50f30798c6d31f2478c0ee1b1a5aabc77 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -4,13 +4,19 @@ require_relative 'helper'
 require 'time'  # for Time#httpdate
 require 'zlib'
 
+separate_testing do
+  require_relative '../lib/rack/deflater'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Deflater do
 
   def build_response(status, body, accept_encoding, options = {})
     body = [body] if body.respond_to? :to_str
     app = lambda do |env|
       res = [status, options['response_headers'] || {}, body]
-      res[1]['Content-Type'] = 'text/plain' unless res[0] == 304
+      res[1]['content-type'] = 'text/plain' unless res[0] == 304
       res
     end
 
@@ -25,7 +31,7 @@ describe Rack::Deflater do
   #
   # [expected_status] expected response status, e.g. 200, 304
   # [expected_body] expected response body
-  # [accept_encoing] what Accept-Encoding header to send and expect, e.g.
+  # [accept_encoding] what Accept-Encoding header to send and expect, e.g.
   #                  'deflate' - accepts and expects deflate encoding in response
   #                  { 'gzip' => nil } - accepts gzip but expects no encoding in response
   # [options] hash of request options, i.e.
@@ -68,7 +74,7 @@ describe Rack::Deflater do
         io = StringIO.new(body_text)
         gz = Zlib::GzipReader.new(io)
         mtime = gz.mtime.to_i
-        if last_mod = headers['Last-Modified']
+        if last_mod = headers['last-modified']
           Time.httpdate(last_mod).to_i.must_equal mtime
         else
           mtime.must_be(:<=, Time.now.to_i)
@@ -86,6 +92,7 @@ describe Rack::Deflater do
 
     # yield full response verification
     yield(status, headers, body) if block_given?
+    body.close if body.respond_to?(:close)
   end
 
   # automatic gzip detection (streamable)
@@ -103,22 +110,34 @@ describe Rack::Deflater do
 
     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'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
     end
   end
 
+  it 'should not update vary response header if it includes * or accept-encoding' do
+    verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => 'Accept-Encoding' } ) do |status, headers, body|
+      headers['vary'].must_equal 'Accept-Encoding'
+    end
+    verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => '*' } ) do |status, headers, body|
+      headers['vary'].must_equal '*'
+    end
+    verify(200, 'foobar', deflate_or_gzip, 'response_headers' => { 'vary' => 'Do-Not-Accept-Encoding' } ) do |status, headers, body|
+      headers['vary'].must_equal 'Do-Not-Accept-Encoding,Accept-Encoding'
+    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'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
     end
   end
@@ -129,9 +148,9 @@ describe Rack::Deflater do
 
     verify(200, app_body, deflate_or_gzip, { 'skip_body_verify' => true }) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
 
       buf = []
@@ -149,9 +168,9 @@ describe Rack::Deflater do
     opts = { 'skip_body_verify' => true }
     verify(200, app_body, 'gzip', opts) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
 
       buf = []
@@ -173,9 +192,9 @@ describe Rack::Deflater do
   it 'be able to deflate String bodies' do
     verify(200, 'Hello world!', deflate_or_gzip) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
     end
   end
@@ -186,9 +205,19 @@ describe Rack::Deflater do
 
     verify(200, 'foobar', 'gzip', { 'app_body' => app_body }) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
+      })
+    end
+  end
+
+  it 'be able to gzip files' do
+    verify(200, File.binread(__FILE__), 'gzip', { 'app_body' => File.open(__FILE__)}) do |status, headers, body|
+      headers.must_equal({
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
     end
   end
@@ -199,9 +228,9 @@ describe Rack::Deflater do
 
     verify(200, app_body, 'gzip', { 'skip_body_verify' => true }) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
 
       buf = []
@@ -216,8 +245,8 @@ describe Rack::Deflater do
   it 'be able to fallback to no deflation' do
     verify(200, 'Hello world!', 'superzip') do |status, headers, body|
       headers.must_equal({
-        'Vary' => 'Accept-Encoding',
-        'Content-Type' => 'text/plain'
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
     end
   end
@@ -247,67 +276,86 @@ describe Rack::Deflater do
       }
     }
 
+    app_body3 = [app_body]
+    closed = false
+    app_body3.define_singleton_method(:close){closed = true}
+    options3 = {
+      'app_status' => 200,
+      'app_body' => app_body3,
+      'request_headers' => {
+        'PATH_INFO' => '/'
+      }
+    }
+
     verify(406, not_found_body1, 'identity;q=0', options1) do |status, headers, body|
       headers.must_equal({
-        'Content-Type' => 'text/plain',
-        'Content-Length' => not_found_body1.length.to_s
+        'content-type' => 'text/plain',
+        'content-length' => not_found_body1.length.to_s
       })
     end
 
     verify(406, not_found_body2, 'identity;q=0', options2) do |status, headers, body|
       headers.must_equal({
-        'Content-Type' => 'text/plain',
-        'Content-Length' => not_found_body2.length.to_s
+        'content-type' => 'text/plain',
+        'content-length' => not_found_body2.length.to_s
       })
     end
+
+    verify(406, not_found_body1, 'identity;q=0', options3) do |status, headers, body|
+      headers.must_equal({
+        'content-type' => 'text/plain',
+        'content-length' => not_found_body1.length.to_s
+      })
+    end
+    closed.must_equal true
   end
 
-  it 'handle gzip response with Last-Modified header' do
+  it 'handle gzip response with last-modified header' do
     last_modified = Time.now.httpdate
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain',
-        'Last-Modified' => last_modified
+        'content-type' => 'text/plain',
+        'last-modified' => last_modified
       }
     }
 
     verify(200, 'Hello World!', 'gzip', options) do |status, headers, body|
       headers.must_equal({
-        'Content-Encoding' => 'gzip',
-        'Vary' => 'Accept-Encoding',
-        'Last-Modified' => last_modified,
-        'Content-Type' => 'text/plain'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'last-modified' => last_modified,
+        'content-type' => 'text/plain'
       })
     end
   end
 
-  it 'do nothing when no-transform Cache-Control directive present' do
+  it 'do nothing when no-transform cache-control directive present' do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain',
-        'Cache-Control' => 'no-transform'
+        'content-type' => 'text/plain',
+        'cache-control' => 'no-transform'
       }
     }
     verify(200, 'Hello World!', { 'gzip' => nil }, options) do |status, headers, body|
-      headers.wont_include 'Content-Encoding'
+      headers.wont_include 'content-encoding'
     end
   end
 
-  it 'do nothing when Content-Encoding already present' do
+  it 'do nothing when content-encoding already present' do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain',
-        'Content-Encoding' => 'gzip'
+        'content-type' => 'text/plain',
+        'content-encoding' => 'gzip'
       }
     }
     verify(200, 'Hello World!', { 'gzip' => nil }, options)
   end
 
-  it 'deflate when Content-Encoding is identity' do
+  it 'deflate when content-encoding is identity' do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain',
-        'Content-Encoding' => 'identity'
+        'content-type' => 'text/plain',
+        'content-encoding' => 'identity'
       }
     }
     verify(200, 'Hello World!', deflate_or_gzip, options)
@@ -316,7 +364,7 @@ describe Rack::Deflater do
   it "deflate if content-type matches :include" do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain'
+        'content-type' => 'text/plain'
       },
       'deflater_options' => {
         include: %w(text/plain)
@@ -328,7 +376,7 @@ describe Rack::Deflater do
   it "deflate if content-type is included it :include" do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain; charset=us-ascii'
+        'content-type' => 'text/plain; charset=us-ascii'
       },
       'deflater_options' => {
         include: %w(text/plain)
@@ -349,7 +397,7 @@ describe Rack::Deflater do
   it "not deflate if content-type do not match :include" do
     options = {
       'response_headers' => {
-        'Content-Type' => 'text/plain'
+        'content-type' => 'text/plain'
       },
       'deflater_options' => {
         include: %w(text/json)
@@ -361,7 +409,7 @@ describe Rack::Deflater do
   it "not deflate if content-length is 0" do
     options = {
       'response_headers' => {
-        'Content-Length' => '0'
+        'content-length' => '0'
       },
     }
     verify(200, '', { 'gzip' => nil }, options)
@@ -385,16 +433,16 @@ describe Rack::Deflater do
     verify(200, 'Hello World!', { 'gzip' => nil }, options)
   end
 
-  it "check for Content-Length via :if" do
+  it "check for content-length via :if" do
     response = 'Hello World!'
     response_len = response.length
     options = {
       'response_headers' => {
-        'Content-Length' => response_len.to_s
+        'content-length' => response_len.to_s
       },
       'deflater_options' => {
         if: lambda { |env, status, headers, body|
-          headers['Content-Length'].to_i >= response_len
+          headers['content-length'].to_i >= response_len
         }
       }
     }
@@ -417,9 +465,9 @@ describe Rack::Deflater do
     }
     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'
+        'content-encoding' => 'gzip',
+        'vary' => 'Accept-Encoding',
+        'content-type' => 'text/plain'
       })
 
       buf = ''.dup
@@ -435,4 +483,43 @@ describe Rack::Deflater do
       raw_bytes.must_be(:<, expect.bytesize)
     end
   end
+
+  it 'will honor sync: false to avoid unnecessary flushing when deflating files' do
+    content = File.binread(__FILE__)
+    options = {
+      'deflater_options' => { sync: false },
+      'app_body' => File.open(__FILE__),
+      'skip_body_verify' => true,
+    }
+    verify(200, content, 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
+      buf.must_equal content
+      raw_bytes.must_be(:<, content.bytesize)
+    end
+  end
+
+  it 'does not close the response body prematurely' do
+    app_body = Class.new do
+      attr_reader :closed;
+      def each; yield('foo'); yield('bar'); end;
+      def close; @closed = true; end;
+    end.new
+
+    verify(200, 'foobar', deflate_or_gzip, { 'app_body' => app_body }) do |status, headers, body|
+      assert_nil app_body.closed
+    end
+  end
 end
diff --git a/test/spec_directory.rb b/test/spec_directory.rb
index 0e4d501fbcecbfa1ef22757e9c8fdc8a83f98396..fcd1f16cad06143bf7d458ab7a450d45ee207acd 100644
--- a/test/spec_directory.rb
+++ b/test/spec_directory.rb
@@ -4,9 +4,17 @@ require_relative 'helper'
 require 'tempfile'
 require 'fileutils'
 
+separate_testing do
+  require_relative '../lib/rack/directory'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/utils'
+  require_relative '../lib/rack/builder'
+end
+
 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
 
@@ -32,12 +40,34 @@ describe Rack::Directory do
     end
   end
 
+  it "serve root directory index" do
+    res = Rack::MockRequest.new(Rack::Lint.new(app)).
+      get("/")
+
+    res.must_be :ok?
+    assert_includes(res.body, '<html><head>')
+    assert_includes(res.body, "href='cgi")
+  end
+
   it "serve directory indices" do
     res = Rack::MockRequest.new(Rack::Lint.new(app)).
       get("/cgi/")
 
     res.must_be :ok?
-    assert_match(res, /<html><head>/)
+    assert_includes(res.body, '<html><head>')
+    assert_includes(res.body, "rackup_stub.rb")
+  end
+
+  it "return 404 for pipes" do
+    begin
+      File.mkfifo('test/cgi/fifo')
+      res = Rack::MockRequest.new(Rack::Lint.new(app)).
+        get("/cgi/fifo")
+
+      res.status.must_equal 404
+    ensure
+      File.delete('test/cgi/fifo')
+    end
   end
 
   it "serve directory indices with bad symlinks" do
@@ -119,6 +149,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")
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index 77c2dfc32002d7fdf49189d715ee609f7dec6481..e1670dfa577c60ca23c6f59d39e720ed3f24ef07 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -3,6 +3,12 @@
 require_relative 'helper'
 require 'time'
 
+separate_testing do
+  require_relative '../lib/rack/etag'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ETag do
   def etag(app, *args)
     Rack::Lint.new Rack::ETag.new(app, *args)
@@ -13,93 +19,91 @@ describe Rack::ETag do
   end
 
   def sendfile_body
-    res = ['Hello World']
-    def res.to_path ; "/tmp/hello.txt" ; end
-    res
+    File.new(File::NULL)
   end
 
-  it "set ETag if none is set if status is 200" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
+  it "set etag if none is set if status is 200" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
+    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!"]] }
+  it "set etag if none is set if status is 201" do
+    app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
+    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!"]] }
+  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!"]] }
     response = etag(app).call(request)
-    response[1]['Cache-Control'].must_equal 'max-age=0, private, must-revalidate'
+    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!"]] }
+  it "set cache-control to chosen one if none is set" do
+    app = lambda { |env| [201, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
     response = etag(app, nil, 'public').call(request)
-    response[1]['Cache-Control'].must_equal 'public'
+    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' }, []] }
+  it "set a given cache-control even if digest could not be calculated" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, []] }
     response = etag(app, 'no-cache').call(request)
-    response[1]['Cache-Control'].must_equal 'no-cache'
+    response[1]['cache-control'].must_equal 'no-cache'
   end
 
-  it "does not set a cache-control if it is already set" do
-    app = lambda { |env| [201, { 'Content-Type' => 'text/plain', 'cache-control' => 'public' }, ["Hello, World!"]] }
+  it "not set cache-control if it is already set" do
+    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 it is already set" do
-    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'
+  it "not set cache-control if directive isn't present" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello, World!"]] }
+    response = etag(app, nil, nil).call(request)
+    response[1]['cache-control'].must_be_nil
   end
 
-  it "not set Cache-Control if directive isn't present" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ["Hello, World!"]] }
-    response = etag(app, nil, nil).call(request)
-    response[1]['Cache-Control'].must_be_nil
+  it "not change etag if it is already set" do
+    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 change ETag if it is already set" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'ETag' => '"abc"' }, ["Hello, World!"]] }
+  it "not set etag if body is empty" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain', 'last-modified' => Time.now.httpdate }, []] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_equal "\"abc\""
+    response[1]['etag'].must_be_nil
   end
 
-  it "not set ETag if body is empty" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Last-Modified' => Time.now.httpdate }, []] }
+  it "set handle empty body parts" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, ["Hello", "", ", World!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_be_nil
+    response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
   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!"]] }
+  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!"]] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_be_nil
+    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] }
+  it "not set etag if a sendfile_body is given" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, sendfile_body] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_be_nil
+    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.']] }
+  it "not set etag if a status is not 200 or 201" do
+    app = lambda { |env| [401, { 'content-type' => 'text/plain' }, ['Access denied.']] }
     response = etag(app).call(request)
-    response[1]['ETag'].must_be_nil
+    response[1]['etag'].must_be_nil
   end
 
-  it "set ETag even if no-cache is given" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain', 'Cache-Control' => 'no-cache, must-revalidate' }, ['Hello, World!']] }
+  it "set etag even if no-cache is given" do
+    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_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
+    response[1]['etag'].must_equal "W/\"dffd6021bb2bd5b0af676290809ec3a5\""
   end
 
   it "close the original body" do
diff --git a/test/spec_events.rb b/test/spec_events.rb
index e2077984d0f9c76861e174e42e83bd8d0c45bc23..e40d72be26fad02aa10c4ad6c43e7bd7db54c871 100644
--- a/test/spec_events.rb
+++ b/test/spec_events.rb
@@ -2,6 +2,10 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/events'
+end
+
 module Rack
   class TestEvents < Minitest::Test
     class EventMiddleware
@@ -58,7 +62,7 @@ module Rack
       app = lambda { |env| events << [app, :call]; ret }
       se = EventMiddleware.new events
       e = Events.new app, [se]
-      triple = e.call({})
+      e.call({})
       assert_equal [[se, :on_start],
                     [app, :call],
                     [se, :on_commit],
@@ -118,7 +122,6 @@ module Rack
 
     def test_finish_is_called_if_there_is_an_exception
       events = []
-      ret = [200, {}, []]
       app = lambda { |env| raise }
       se = EventMiddleware.new events
       e = Events.new app, [se]
diff --git a/test/spec_files.rb b/test/spec_files.rb
index 898b0d9095b19d4c31812f3b087ad15befef901d..5b5a09d8004613349b55d75f34860822a4f713f8 100644
--- a/test/spec_files.rb
+++ b/test/spec_files.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/files'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Files do
   DOCROOT = File.expand_path(File.dirname(__FILE__)) unless defined? DOCROOT
 
@@ -19,16 +25,7 @@ describe Rack::Files do
     )
 
     file_path = File.expand_path("cgi/test", __dir__)
-    status, headers, body = app.serving(request, file_path)
-    assert_equal 200, status
-  end
-
-  it 'raises if you attempt to define response_body in subclass' do
-    c = Class.new(Rack::Files)
-
-    lambda do
-      c.send(:define_method, :response_body){}
-    end.must_raise RuntimeError
+    assert_equal 200, app.serving(request, file_path)[0]
   end
 
   it 'serves files with + in the file name' do
@@ -58,13 +55,13 @@ describe Rack::Files do
     res.status.must_equal 404
   end
 
-  it "set Last-Modified header" do
+  it "set last-modified header" do
     res = Rack::MockRequest.new(files(DOCROOT)).get("/cgi/test")
 
     path = File.join(DOCROOT, "/cgi/test")
 
     res.must_be :ok?
-    res["Last-Modified"].must_equal File.mtime(path).httpdate
+    res["last-modified"].must_equal File.mtime(path).httpdate
   end
 
   it "return 304 if file isn't modified since last serve" do
@@ -190,9 +187,23 @@ describe Rack::Files do
     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/208"
-    res.body.must_equal "frozen_strin"
+    res["content-length"].must_equal "12"
+    res["content-range"].must_equal "bytes 22-33/209"
+    res.body.must_equal "IS FILE! ***"
+  end
+
+  it "handle case where file is truncated during request" do
+    env = Rack::MockRequest.env_for("/cgi/test")
+    env["HTTP_RANGE"] = "bytes=0-3300"
+    files = Class.new(Rack::Files) do
+      def filesize(_); 10000 end
+    end.new(DOCROOT)
+
+    res = Rack::MockResponse.new(*files.call(env))
+
+    res.status.must_equal 206
+    res["content-length"].must_equal "209"
+    res["content-range"].must_equal "bytes 0-3300/10000"
   end
 
   it "return correct multiple byte ranges in body" do
@@ -201,20 +212,20 @@ describe Rack::Files do
     res = Rack::MockResponse.new(*files(DOCROOT).call(env))
 
     res.status.must_equal 206
-    res["Content-Length"].must_equal "191"
-    res["Content-Type"].must_equal "multipart/byteranges; boundary=AaB03x"
+    res["content-length"].must_equal "191"
+    res["content-type"].must_equal "multipart/byteranges; boundary=AaB03x"
     expected_body = <<-EOF
 \r
 --AaB03x\r
-Content-Type: text/plain\r
-Content-Range: bytes 22-33/208\r
+content-type: text/plain\r
+content-range: bytes 22-33/209\r
 \r
-frozen_strin\r
+IS FILE! ***\r
 --AaB03x\r
-Content-Type: text/plain\r
-Content-Range: bytes 60-80/208\r
+content-type: text/plain\r
+content-range: bytes 60-80/209\r
 \r
-e.join(File.dirname(_\r
+, tests will break!!!\r
 --AaB03x--\r
     EOF
 
@@ -227,17 +238,17 @@ e.join(File.dirname(_\r
     res = Rack::MockResponse.new(*files(DOCROOT).call(env))
 
     res.status.must_equal 416
-    res["Content-Range"].must_equal "bytes */208"
+    res["content-range"].must_equal "bytes */209"
   end
 
   it "support custom http headers" do
     env = Rack::MockRequest.env_for("/cgi/test")
-    status, heads, _ = files(DOCROOT, 'Cache-Control' => 'public, max-age=38',
-     'Access-Control-Allow-Origin' => '*').call(env)
+    status, heads, _ = files(DOCROOT, 'cache-control' => 'public, max-age=38',
+     'access-control-allow-origin' => '*').call(env)
 
     status.must_equal 200
-    heads['Cache-Control'].must_equal 'public, max-age=38'
-    heads['Access-Control-Allow-Origin'].must_equal '*'
+    heads['cache-control'].must_equal 'public, max-age=38'
+    heads['access-control-allow-origin'].must_equal '*'
   end
 
   it "support not add custom http headers if none are supplied" do
@@ -245,8 +256,8 @@ e.join(File.dirname(_\r
     status, heads, _ = files(DOCROOT).call(env)
 
     status.must_equal 200
-    heads['Cache-Control'].must_be_nil
-    heads['Access-Control-Allow-Origin'].must_be_nil
+    heads['cache-control'].must_be_nil
+    heads['access-control-allow-origin'].must_be_nil
   end
 
   it "only support GET, HEAD, and OPTIONS requests" do
@@ -257,7 +268,7 @@ e.join(File.dirname(_\r
       res = req.send(method, "/cgi/test")
       res.must_be :client_error?
       res.must_be :method_not_allowed?
-      res.headers['Allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS)
+      res.headers['allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS)
     end
 
     allowed = %w[get head options]
@@ -271,36 +282,36 @@ e.join(File.dirname(_\r
     req = Rack::MockRequest.new(files(DOCROOT))
     res = req.options('/cgi/test')
     res.must_be :successful?
-    res.headers['Allow'].wont_equal nil
-    res.headers['Allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS)
+    res.headers['allow'].wont_equal nil
+    res.headers['allow'].split(/, */).sort.must_equal %w(GET HEAD OPTIONS)
   end
 
-  it "set Content-Length correctly for HEAD requests" do
+  it "set content-length correctly for HEAD requests" do
     req = Rack::MockRequest.new(Rack::Lint.new(Rack::Files.new(DOCROOT)))
     res = req.head "/cgi/test"
     res.must_be :successful?
-    res['Content-Length'].must_equal "208"
+    res['content-length'].must_equal "209"
   end
 
   it "default to a mime type of text/plain" do
     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"
+    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::Files.new(DOCROOT, nil, 'application/octet-stream')))
     res = req.get "/cgi/test"
     res.must_be :successful?
-    res['Content-Type'].must_equal "application/octet-stream"
+    res['content-type'].must_equal "application/octet-stream"
   end
 
-  it "not set Content-Type if the mime type is not set" do
+  it "not set content-type if the mime type is not set" do
     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
+    res['content-type'].must_be_nil
   end
 
   it "return error when file not found for head request" do
diff --git a/test/spec_handler.rb b/test/spec_handler.rb
deleted file mode 100644
index d6d9cccec38b39848c3e0a87c1994b4c53da1e57..0000000000000000000000000000000000000000
--- a/test/spec_handler.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-class Rack::Handler::Lobster; end
-class RockLobster; end
-
-describe Rack::Handler do
-  it "has registered default handlers" do
-    Rack::Handler.get('cgi').must_equal Rack::Handler::CGI
-    Rack::Handler.get('webrick').must_equal Rack::Handler::WEBrick
-
-    begin
-      Rack::Handler.get('fastcgi').must_equal Rack::Handler::FastCGI
-    rescue LoadError
-    end
-  end
-
-  it "raise LoadError if handler doesn't exist" do
-    lambda {
-      Rack::Handler.get('boom')
-    }.must_raise(LoadError)
-
-    lambda {
-      Rack::Handler.get('Object')
-    }.must_raise(LoadError)
-  end
-
-  it "get unregistered, but already required, handler by name" do
-    Rack::Handler.get('Lobster').must_equal Rack::Handler::Lobster
-  end
-
-  it "register custom handler" do
-    Rack::Handler.register('rock_lobster', 'RockLobster')
-    Rack::Handler.get('rock_lobster').must_equal RockLobster
-  end
-
-  it "not need registration for properly coded handlers even if not already required" do
-    begin
-      $LOAD_PATH.push File.expand_path('../unregistered_handler', __FILE__)
-      Rack::Handler.get('Unregistered').must_equal Rack::Handler::Unregistered
-      lambda { Rack::Handler.get('UnRegistered') }.must_raise LoadError
-      Rack::Handler.get('UnregisteredLongOne').must_equal Rack::Handler::UnregisteredLongOne
-    ensure
-      $LOAD_PATH.delete File.expand_path('../unregistered_handler', __FILE__)
-    end
-  end
-
-  it "allow autoloaded handlers to be registered properly while being loaded" do
-    path = File.expand_path('../registering_handler', __FILE__)
-    begin
-      $LOAD_PATH.push path
-      Rack::Handler.get('registering_myself').must_equal Rack::Handler::RegisteringMyself
-    ensure
-      $LOAD_PATH.delete path
-    end
-  end
-end
diff --git a/test/spec_head.rb b/test/spec_head.rb
index d2dedd28167051033c69c207bacfe6d382458ff7..4ab90c2efd486937fe1f400094e749bb466eb51c 100644
--- a/test/spec_head.rb
+++ b/test/spec_head.rb
@@ -2,12 +2,18 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/head'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 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)
@@ -20,7 +26,7 @@ describe Rack::Head do
       resp, _ = test_response("REQUEST_METHOD" => type)
 
       resp[0].must_equal 200
-      resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3"
+      resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
       resp[2].to_enum.to_a.must_equal ["foo"]
     end
   end
@@ -29,14 +35,14 @@ describe Rack::Head do
     resp, _ = test_response("REQUEST_METHOD" => "HEAD")
 
     resp[0].must_equal 200
-    resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3"
+    resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
     resp[2].to_enum.to_a.must_equal []
   end
 
   it "close the body when it is removed" do
     resp, body = test_response("REQUEST_METHOD" => "HEAD")
     resp[0].must_equal 200
-    resp[1].must_equal "Content-type" => "test/plain", "Content-length" => "3"
+    resp[1].must_equal "content-type" => "test/plain", "content-length" => "3"
     resp[2].to_enum.to_a.must_equal []
     body.wont_be :closed?
     resp[2].close
diff --git a/test/spec_headers.rb b/test/spec_headers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f22680b77b9500c6ff348c41d433ce91e6691a0e
--- /dev/null
+++ b/test/spec_headers.rb
@@ -0,0 +1,520 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+
+separate_testing do
+  require_relative '../lib/rack/headers'
+end
+
+class RackHeadersTest < Minitest::Spec
+  before do
+    @h = Rack::Headers.new
+    @fh = Rack::Headers['AB'=>'1', 'cd'=>'2', '3'=>'4']
+  end
+
+  def test_public_interface
+    headers_methods = Rack::Headers.public_instance_methods.sort
+    hash_methods = Hash.public_instance_methods.sort
+    assert_empty(headers_methods - hash_methods)
+    assert_empty(hash_methods - headers_methods)
+  end
+
+  def test_class_aref
+    assert_equal Hash[], Rack::Headers[]
+    assert_equal Hash['a'=>'2'], Rack::Headers['A'=>'2']
+    assert_equal Hash['a'=>'2', 'b'=>'4'], Rack::Headers['A'=>'2', 'B'=>'4']
+    assert_equal Hash['a','2','b','4'], Rack::Headers['A','2','B','4']
+    assert_raises(ArgumentError){Rack::Headers['A']}
+    assert_raises(ArgumentError){Rack::Headers['A',2,'B']}
+  end
+
+  def test_default_values
+    h, ch = Hash.new, Rack::Headers.new
+    assert_equal h, ch
+    h, ch = Hash.new('1'), Rack::Headers.new('1')
+    assert_equal h, ch
+    assert_equal h['3'], ch['3']
+    h['a'], ch['A'] = ['2', '2']
+    assert_equal h['a'], ch['a']
+    h, ch = Hash.new{|h,k| k*2}, Rack::Headers.new{|h,k| k*2}
+    assert_equal h['3'], ch['3']
+    h['c'], ch['C'] = ['2', '2']
+    assert_equal h['c'], ch['c']
+    assert_raises(ArgumentError){Rack::Headers.new('1'){|hash,k| key}}
+
+    assert_nil @fh.default
+    assert_nil @fh.default_proc
+    assert_nil @fh['55']
+    assert_equal '3', Rack::Headers.new('3').default
+    assert_nil Rack::Headers.new('3').default_proc
+    assert_equal '3', Rack::Headers.new('3')['1']
+
+    @fh.default = '4'
+    assert_equal '4', @fh.default
+    assert_nil  @fh.default_proc
+    assert_equal '4', @fh['55']
+
+    h = Rack::Headers.new('5')
+    assert_equal '5', h.default
+    assert_nil  h.default_proc
+    assert_equal '5', h['55']
+
+    h = Rack::Headers.new{|hash, key| '1234'}
+    assert_nil  h.default
+    refute_equal nil, h.default_proc
+    assert_equal '1234', h['55']
+
+    h = Rack::Headers.new{|hash, key| hash[key] = '1234'; nil}
+    assert_nil  h.default
+    refute_equal nil, h.default_proc
+    assert_nil  h['Ac']
+    assert_equal '1234', h['aC']
+  end
+
+  def test_store_and_retrieve
+    assert_nil  @h['a']
+    @h['A'] = '2'
+    assert_equal '2', @h['a']
+    assert_equal '2', @h['A']
+    @h['a'] = '3'
+    assert_equal '3', @h['a']
+    assert_equal '3', @h['A']
+    @h['AB'] = '5'
+    assert_equal '5', @h['ab']
+    assert_equal '5', @h['AB']
+    assert_equal '5', @h['aB']
+    assert_equal '5', @h['Ab']
+    @h.store('C', '8')
+    assert_equal '8', @h['c']
+    assert_equal '8', @h['C']
+  end
+
+  def test_clear
+    assert_equal 3, @fh.length
+    @fh.clear
+    assert_equal Hash[], @fh
+    assert_equal 0, @fh.length
+  end
+
+  def test_delete
+    assert_equal 3, @fh.length
+    assert_equal '1', @fh.delete('aB')
+    assert_equal 2, @fh.length
+    assert_nil @fh.delete('Ab')
+    assert_equal 2, @fh.length
+  end
+
+  def test_delete_if_and_reject
+    assert_equal 3, @fh.length
+    hash = @fh.reject{|key, value| key == 'ab' || key == 'cd'}
+    assert_equal 1, hash.length
+    assert_equal Hash['3'=>'4'], hash
+    assert_equal 3, @fh.length
+    hash = @fh.delete_if{|key, value| key == 'ab' || key == 'cd'}
+    assert_equal 1, hash.length
+    assert_equal Hash['3'=>'4'], hash
+    assert_equal 1, @fh.length
+    assert_equal Hash['3'=>'4'], @fh
+    assert_nil  @fh.reject!{|key, value| key == 'ab' || key == 'cd'}
+    hash = @fh.reject!{|key, value| key == '3'}
+    assert_equal 0, hash.length
+    assert_equal Hash[], hash
+    assert_equal 0, @fh.length
+    assert_equal Hash[], @fh
+  end
+
+  def test_dup_and_clone
+    def @h.foo; 1; end
+    h2 = @h.dup
+    h3 = @h.clone
+    h2['A'] = '2'
+    h3['B'] = '3'
+    assert_equal Rack::Headers[], @h
+    assert_raises NoMethodError do h2.foo end
+    assert_equal 1, h3.foo
+    assert_equal '2', h2['a']
+    assert_equal '3', h3['b']
+  end
+
+  def test_each
+    i = 0
+    @h.each{i+=1}
+    assert_equal 0, i
+    items = [['ab','1'], ['cd','2'], ['3','4']]
+    @fh.each do |k,v|
+      assert items.include?([k,v])
+      items -= [[k,v]]
+    end
+    assert_equal [], items
+  end
+
+  def test_each_key
+    i = 0
+    @h.each{i+=1}
+    assert_equal 0, i
+    keys = ['ab', 'cd', '3']
+    @fh.each_key do |k|
+      assert keys.include?(k)
+      assert k.frozen?
+      keys -= [k]
+    end
+    assert_equal [], keys
+  end
+
+  def test_each_value
+    i = 0
+    @h.each{i+=1}
+    assert_equal 0, i
+    values = ['1', '2', '4']
+    @fh.each_value do |v|
+      assert values.include?(v)
+      values -= [v]
+    end
+    assert_equal [], values
+  end
+
+  def test_empty
+    assert @h.empty?
+    assert !@fh.empty?
+  end
+
+  def test_fetch
+    assert_raises(ArgumentError){@h.fetch(1,2,3)}
+    assert_raises(ArgumentError){@h.fetch(1,2,3){4}}
+    assert_raises(IndexError){@h.fetch(1)}
+    @h.default = '33'
+    assert_raises(IndexError){@h.fetch(1)}
+    @h['1'] = '8'
+    assert_equal '8', @h.fetch('1')
+    assert_equal '3', @h.fetch(2, '3')
+    assert_equal '222', @h.fetch('2'){|k| k*3}
+    assert_equal '1', @fh.fetch('Ab')
+    assert_equal '2', @fh.fetch('cD', '3')
+    assert_equal '4', @fh.fetch("3", 3)
+    assert_equal '4', @fh.fetch("3"){|k| k*3}
+    assert_raises(IndexError){Rack::Headers.new{34}.fetch(1)}
+  end
+
+  def test_has_key
+    %i'include? has_key? key? member?'.each do |meth|
+      assert !@h.send(meth,1)
+      assert @fh.send(meth,'Ab')
+      assert @fh.send(meth,'cD')
+      assert @fh.send(meth,'3')
+      assert @fh.send(meth,'ab')
+      assert @fh.send(meth,'CD')
+      assert @fh.send(meth,'3')
+      assert !@fh.send(meth,1)
+    end
+  end
+
+  def test_has_value
+    %i'value? has_value?'.each do |meth|
+      assert !@h.send(meth,'1')
+      assert @fh.send(meth,'1')
+      assert @fh.send(meth,'2')
+      assert @fh.send(meth,'4')
+      assert !@fh.send(meth,'3')
+    end
+  end
+
+  def test_inspect
+    %i'inspect to_s'.each do |meth|
+      assert_equal '{}', @h.send(meth)
+      assert_equal '{"ab"=>"1", "cd"=>"2", "3"=>"4"}', @fh.send(meth)
+    end
+  end
+
+  def test_invert
+    assert_kind_of(Rack::Headers, @h.invert)
+    assert_equal({}, @h.invert)
+    assert_equal({"1"=>"ab", "2"=>"cd", "4"=>"3"}, @fh.invert)
+    assert_equal({'cd'=>'ab'}, Rack::Headers['AB'=>'CD'].invert)
+    assert_equal({'cd'=>'xy'}, Rack::Headers['AB'=>'Cd', 'xY'=>'cD'].invert)
+  end
+
+  def test_keys
+    assert_equal [], @h.keys
+    assert_equal %w'ab cd 3', @fh.keys
+  end
+
+  def test_length
+    %i'length size'.each do |meth|
+      assert_equal 0, @h.send(meth)
+      assert_equal 3, @fh.send(meth)
+    end
+  end
+
+  def test_merge_and_update
+    assert_equal @h, @h.merge({})
+    assert_equal @fh, @fh.merge({})
+    assert_equal Rack::Headers['ab'=>'55'], @h.merge({'ab'=>'55'})
+    assert_equal Rack::Headers[], @h
+    assert_equal Rack::Headers['ab'=>'55'], @h.update({'ab'=>'55'})
+    assert_equal Rack::Headers['ab'=>'55'], @h
+    assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'55'})
+    assert_equal Rack::Headers['ab'=>'1', 'cd'=>'2', '3'=>'4'], @fh
+    assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'55'})
+    assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh
+    assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.merge({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+    assert_equal Rack::Headers['ab'=>'55', 'cd'=>'2', '3'=>'4'], @fh
+    assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh.update({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+    assert_equal Rack::Headers['ab'=>'abss55', 'cd'=>'2', '3'=>'4'], @fh
+    assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh.merge!({'ab'=>'ss'}){|k,ov,nv| [k,nv,ov].join}
+    assert_equal Rack::Headers['ab'=>'abssabss55', 'cd'=>'2', '3'=>'4'], @fh
+  end
+
+  def test_replace
+    h = @h.dup
+    fh = @fh.dup
+    h1 = fh.replace(@h)
+    assert_equal @h, h1
+    assert_same fh, h1
+
+    h2 = h.replace(@fh)
+    assert_equal @fh, h2
+    assert_same h, h2
+
+    assert_equal @h, fh.replace({})
+    assert_equal @fh, h.replace('AB'=>'1', 'cd'=>'2', '3'=>'4')
+  end
+
+  def test_select
+    assert_equal({}, @h.select{true})
+    assert_equal({}, @h.select{false})
+    assert_equal({'3' => '4', "ab" => '1', 'cd' => '2'}, @fh.select{true})
+    assert_equal({}, @fh.select{false})
+    assert_equal({'cd' => '2'}, @fh.select{|k,v| k.start_with?('c')})
+    assert_equal({'3' => '4'}, @fh.select{|k,v| v == '4'})
+  end
+
+  def test_shift
+    assert_nil @h.shift
+    array = @fh.to_a
+    i = 3
+    while true
+      assert i >= 0
+      kv = @fh.shift
+      if kv.nil?
+        assert_equal [], array
+        break
+      else
+        i -= 1
+        assert array.include?(kv)
+        array -= [kv]
+      end
+    end
+    assert_equal [], array
+    assert_equal 0, i
+  end
+
+  def test_sort
+    assert_equal [], @h.sort
+    assert_equal [], @h.sort{|a,b| a.to_s<=>b.to_s}
+    assert_equal [['ab', '1'], ['cd', '4'], ['ef', '2']], Rack::Headers['CD','4','AB','1','EF','2'].sort
+    assert_equal [['3', '4'], ['ab', '1'], ['cd', '2']], @fh.sort{|(ak,av),(bk,bv)| ak.to_s<=>bk.to_s}
+  end
+
+  def test_to_a
+    assert_equal [], @h.to_a
+    assert_equal [['ab', '1'], ['cd', '2'], ['3', '4']], @fh.to_a
+  end
+
+  def test_to_hash
+    assert_equal Hash[], @h.to_hash
+    assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_hash
+  end
+
+  def test_values
+    assert_equal [], @h.values
+    assert_equal ['f', 'c'], Rack::Headers['aB','f','1','c'].values
+  end
+
+  def test_values_at
+    assert_equal [], @h.values_at()
+    assert_equal [nil], @h.values_at(1)
+    assert_equal [nil, nil], @h.values_at(1, 1)
+    assert_equal [], @fh.values_at()
+    assert_equal ['1'], @fh.values_at('AB')
+    assert_equal ['2', '1'], @fh.values_at('CD', 'Ab')
+    assert_equal ['2', nil, '1'], @fh.values_at('CD', 32, 'aB')
+    assert_equal ['4', '2', nil, '1'], @fh.values_at('3', 'CD', 32, 'ab')
+  end
+
+  def test_assoc
+    assert_nil  @h.assoc(1)
+    assert_equal ['ab', '1'], @fh.assoc('Ab')
+    assert_equal ['cd', '2'], @fh.assoc('CD')
+    assert_nil  @fh.assoc('4')
+    assert_equal ['3', '4'], @fh.assoc('3')
+  end
+
+  def test_default_proc=
+    @h.default_proc = proc{|h, k| k * 2}
+    assert_equal 'aa', @h['A']
+    @h['Ab'] = '2'
+    assert_equal '2', @h['aB']
+  end
+
+  def test_flatten
+    assert_equal [], @h.flatten
+    assert_equal ['ab', '1', 'cd', '2', '3', '4'], @fh.flatten
+    @fh['X'] = '56'
+    assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten
+    assert_equal ['ab', '1', 'cd', '2', '3', '4', 'x', '56'], @fh.flatten(2)
+  end
+
+  def test_keep_if
+    assert_equal @h, @h.keep_if{|k, v| true}
+    assert_equal @fh, @fh.keep_if{|k, v| true}
+    assert_equal @h, @fh.dup.keep_if{|k, v| false}
+    assert_equal Rack::Headers["AB"=>'1'], @fh.keep_if{|k, v| k == "ab"}
+  end
+
+  def test_key
+    assert_nil @h.key('1')
+    assert_nil @fh.key(1)
+    assert_equal 'ab', @fh.key('1')
+    assert_equal 'cd', @fh.key('2')
+    assert_nil @fh.key('3')
+    assert_equal '3', @fh.key('4')
+  end
+
+  def test_rassoc
+    assert_nil @h.rassoc('1')
+    assert_equal ['ab', '1'], @fh.rassoc('1')
+    assert_equal ['cd', '2'], @fh.rassoc('2')
+    assert_nil @fh.rassoc('3')
+    assert_equal ['3', '4'], @fh.rassoc('4')
+  end
+
+  def test_select!
+    assert_nil @h.select!{|k, v| true}
+    assert_nil @fh.select!{|k, v| true}
+    assert_equal @h, @fh.dup.select!{|k, v| false}
+    assert_equal Rack::Headers["AB"=>'1'], @fh.select!{|k, v| k == "ab"}
+  end
+
+  def test_compare_by_identity
+    assert_raises(TypeError){@fh.compare_by_identity}
+  end
+
+  def test_compare_by_identity?
+    assert_equal(false, @fh.compare_by_identity?)
+  end
+
+  def test_to_h
+    assert_equal Hash[], @h.to_h
+    assert_equal Hash['3','4','ab','1','cd','2'], @fh.to_h
+  end
+
+  def test_dig
+    assert_equal('1', @fh.dig('AB'))
+    assert_equal('2', @fh.dig('Cd'))
+    assert_equal('4', @fh.dig('3'))
+    assert_nil(@fh.dig('4'))
+
+    assert_raises(TypeError){@fh.dig('AB', 1)}
+    assert_raises(TypeError){@fh.dig('cd', 2)}
+    assert_raises(TypeError){@fh.dig('3', 3)}
+    assert_nil(@fh.dig('4', 5))
+  end
+
+  def test_fetch_values
+    assert_equal(['1'], @fh.fetch_values('AB'))
+    assert_equal(['1', '2', '4'], @fh.fetch_values('AB', 'Cd', '3'))
+    assert_raises(KeyError){@fh.fetch_values('AB', 'cD', '4')}
+  end
+
+  def test_to_proc
+    pr = @fh.to_proc
+    assert_equal('1', pr['AB'])
+    assert_equal('2', pr['cD'])
+    assert_equal('4', pr['3'])
+    assert_nil(pr['4'])
+  end
+
+  def test_compact
+    assert_equal(false, @fh.compact.equal?(@fh))
+    assert_equal(@fh, @fh.compact)
+    assert_equal(Rack::Headers['Ab'=>1], Rack::Headers['aB'=>1, 'cd'=>nil].compact)
+  end
+
+  def test_compact!
+    fh = @fh.dup
+    assert_nil(@fh.compact!)
+    assert_equal(fh, @fh)
+
+    h = Rack::Headers['Ab'=>1, 'cd'=>nil]
+    assert_equal(Rack::Headers['aB'=>1], h.compact!)
+    assert_equal(Rack::Headers['AB'=>1], h)
+  end
+
+  def test_transform_values
+    fh = @fh.transform_values{|v| v.to_s*2}
+    assert_equal('1', @fh['aB'])
+    assert_equal(Rack::Headers['AB'=>'11', 'cD'=>'22', '3'=>'44'], fh)
+    assert_equal('11', fh['Ab'])
+  end
+
+  def test_transform_values!
+    @fh.transform_values!{|v| v.to_s*2}
+    assert_equal('11', @fh['AB'])
+    assert_equal(Rack::Headers['Ab'=>'11', 'CD'=>'22', '3'=>'44'], @fh)
+    assert_equal('11', @fh['aB'])
+  end
+
+  if RUBY_VERSION >= '2.5'
+    def test_slice
+      assert_equal(Rack::Headers['Ab'=>'1', 'cD'=>'2', '3'=>'4'], @fh.slice('aB', 'Cd', '3'))
+      assert_equal(Rack::Headers['AB'=>'1', 'CD'=>'2'], @fh.slice('Ab', 'CD'))
+      assert_equal(Rack::Headers[], @fh.slice('ad'))
+      assert_equal('1', @fh.slice('AB', 'cd')['Ab'])
+    end
+
+    def test_transform_keys
+      map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'}
+      dh = @fh.dup
+      fh = @fh.transform_keys{|k| map[k]}
+      assert_equal(dh, @fh)
+      assert_equal('1', fh['xY'])
+      assert_equal('2', fh['Dc'])
+      assert_equal('4', fh['5'])
+    end
+
+    def test_transform_keys!
+      map = {'ab'=>'Xy', 'cd'=>'dC', '3'=>'5'}
+      dh = @fh.dup
+      @fh.transform_keys!{|k| map[k]}
+      assert_equal(false, dh == @fh)
+      assert_equal('1', @fh['xY'])
+      assert_equal('2', @fh['DC'])
+      assert_equal('4', @fh['5'])
+    end
+  end
+
+  if RUBY_VERSION >= '2.6'
+    def test_filter!
+      assert_nil @h.filter!{|k, v| true}
+      assert_nil @fh.filter!{|k, v| true}
+      assert_equal @h, @fh.dup.filter!{|k, v| false}
+      assert_equal Rack::Headers["AB"=>'1'], @fh.filter!{|k, v| k == "ab"}
+    end
+  end
+
+  if RUBY_VERSION >= '2.7'
+    def test_deconstruct_keys
+      assert_equal(@fh.to_hash, @fh.deconstruct_keys([]))
+      assert_equal(Rack::Headers, @fh.deconstruct_keys([]).class)
+    end
+  end
+
+  if RUBY_VERSION >= '3.0'
+    def test_except
+      @fh = Rack::Headers['AB'=>'1', 'Cd'=>'2', '3'=>'4']
+      assert_equal(@fh, @fh.except)
+      assert_equal(Rack::Headers['cD'=>'2', '3'=>'4'], @fh.except('AB', 5))
+      assert_equal(Rack::Headers['AB'=>'1'], @fh.except('cD', '3'))
+    end
+  end
+end
diff --git a/test/spec_lint.rb b/test/spec_lint.rb
old mode 100644
new mode 100755
index 5df61435d87210ce75c2caa42107a9ca3e349746..398c7719c9e0c1f1fed8b82b54083c7d565d1572
--- a/test/spec_lint.rb
+++ b/test/spec_lint.rb
@@ -3,15 +3,22 @@
 require_relative 'helper'
 require 'tempfile'
 
+separate_testing do
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Lint do
+  valid_app = lambda do |env|
+    [200, { "content-type" => "test/plain", "content-length" => "3" }, ["foo"]]
+  end
+
   def env(*args)
     Rack::MockRequest.env_for("/", *args)
   end
 
   it "pass valid request" do
-    Rack::Lint.new(lambda { |env|
-                     [200, { "Content-type" => "test/plain", "Content-length" => "3" }, ["foo"]]
-                   }).call(env({})).first.must_equal 200
+    Rack::Lint.new(valid_app).call(env({})).first.must_equal 200
   end
 
   it "notice fatal errors" do
@@ -26,7 +33,6 @@ describe Rack::Lint do
     lambda { Rack::Lint.new(nil).call({}.freeze) }.must_raise(Rack::Lint::LintError).
       message.must_match(/env should not be frozen, but is/)
 
-
     lambda {
       e = env
       e.delete("REQUEST_METHOD")
@@ -41,6 +47,26 @@ describe Rack::Lint do
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/missing required key SERVER_NAME/)
 
+    lambda {
+      e = env
+      e.delete("SERVER_PROTOCOL")
+      Rack::Lint.new(nil).call(e)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/missing required key SERVER_PROTOCOL/)
+
+    lambda {
+      e = env
+      e["SERVER_PROTOCOL"] = 'Foo'
+      Rack::Lint.new(nil).call(e)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/env\[SERVER_PROTOCOL\] does not match HTTP/)
+
+    lambda {
+      e = env
+      e["HTTP_VERSION"] = 'HTTP/1.0'
+      Rack::Lint.new(nil).call(e)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/env\[HTTP_VERSION\] does not equal env\[SERVER_PROTOCOL\]/)
 
     lambda {
       Rack::Lint.new(nil).call(env("HTTP_CONTENT_TYPE" => "text/plain"))
@@ -57,11 +83,6 @@ describe Rack::Lint do
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/non-string value/)
 
-    lambda {
-      Rack::Lint.new(nil).call(env("rack.version" => "0.2"))
-    }.must_raise(Rack::Lint::LintError).
-      message.must_match(/must be an Array/)
-
     lambda {
       Rack::Lint.new(nil).call(env("rack.url_scheme" => "gopher"))
     }.must_raise(Rack::Lint::LintError).
@@ -72,6 +93,8 @@ describe Rack::Lint do
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "session [] must respond to store and []="
 
+    Rack::Lint.new(valid_app).call(env("rack.session" => {}))[0].must_equal 200
+
     lambda {
       Rack::Lint.new(nil).call(env("rack.session" => {}.freeze))
     }.must_raise(Rack::Lint::LintError).
@@ -133,11 +156,16 @@ describe Rack::Lint do
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "logger [] must respond to fatal"
 
+    def obj.fatal(*) end
+    Rack::Lint.new(valid_app).call(env("rack.logger" => obj))[0].must_equal 200
+
     lambda {
       Rack::Lint.new(nil).call(env("rack.multipart.buffer_size" => 0))
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "rack.multipart.buffer_size must be an Integer > 0 if specified"
 
+    Rack::Lint.new(valid_app).call(env("rack.multipart.buffer_size" => 1))[0].must_equal 200
+
     lambda {
       Rack::Lint.new(nil).call(env("rack.multipart.tempfile_factory" => Tempfile))
     }.must_raise(Rack::Lint::LintError).
@@ -158,6 +186,21 @@ describe Rack::Lint do
     }.must_raise(Rack::Lint::LintError).
       message.must_equal "response array has 0 elements instead of 3"
 
+    lambda {
+      Rack::Lint.new(nil).call(env("SERVER_PORT" => "howdy"))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'env[SERVER_PORT] is not an Integer'
+
+    lambda {
+      Rack::Lint.new(nil).call(env("SERVER_NAME" => "\u1234"))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal "\u1234 must be a valid authority"
+
+    lambda {
+      Rack::Lint.new(nil).call(env("HTTP_HOST" => "\u1234"))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal "\u1234 must be a valid authority"
+
     lambda {
       Rack::Lint.new(nil).call(env("REQUEST_METHOD" => "FUCKUP?"))
     }.must_raise(Rack::Lint::LintError).
@@ -209,6 +252,16 @@ describe Rack::Lint do
       Rack::Lint.new(nil).call(env("SCRIPT_NAME" => "/"))
     }.must_raise(Rack::Lint::LintError).
       message.must_match(/cannot be .* make it ''/)
+
+    lambda {
+      Rack::Lint.new(nil).call(env("rack.response_finished" => "not a callable"))
+    }.must_raise(Rack::Lint::LintError).
+    message.must_match(/rack.response_finished must be an array of callable objects/)
+
+    lambda {
+      Rack::Lint.new(nil).call(env("rack.response_finished" => [-> (env) {}, "not a callable"]))
+    }.must_raise(Rack::Lint::LintError).
+    message.must_match(/rack.response_finished values must respond to call/)
   end
 
   it "notice input errors" do
@@ -271,26 +324,42 @@ describe Rack::Lint do
                        ["cc", {}, ""]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
-      message.must_match(/must be >=100 seen as integer/)
+      message.must_match(/must be an Integer >=100/)
 
     lambda {
       Rack::Lint.new(lambda { |env|
                        [42, {}, ""]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
-      message.must_match(/must be >=100 seen as integer/)
+      message.must_match(/must be an Integer >=100/)
+
+    lambda {
+      Rack::Lint.new(lambda { |env|
+                       ["200", {}, ""]
+                     }).call(env({}))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/must be an Integer >=100/)
   end
 
   it "notice header errors" do
+    obj = Object.new
+    def obj.each; end
     lambda {
       io = StringIO.new('a')
       io.binmode
       Rack::Lint.new(lambda { |env|
                        env['rack.input'].each{ |x| }
-                       [200, Object.new, []]
+                       [200, obj, []]
                      }).call(env({ "rack.input" => io }))
     }.must_raise(Rack::Lint::LintError).
-      message.must_equal "headers object should respond to #each, but doesn't (got Object as headers)"
+      message.must_equal "headers object should be a hash, but isn't (got Object as headers)"
+    lambda {
+      Rack::Lint.new(lambda { |env|
+                       [200, {}.freeze, []]
+                     }).call(env({}))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal "headers object should not be frozen, but is"
+
 
     lambda {
       Rack::Lint.new(lambda { |env|
@@ -301,17 +370,18 @@ describe Rack::Lint do
 
     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/)
+      message.must_match(/must not contain status/)
 
     # From RFC 7230:<F24><F25>
     # Most HTTP header field values are defined using common syntax
     # components (token, quoted-string, and comment) separated by
     # whitespace or specific delimiting characters.  Delimiters are chosen
     # from the set of US-ASCII visual characters not allowed in a token
-    # (DQUOTE and "(),/:;<=>?@[\]{}").
+    # (DQUOTE and "(),/:;<=>?@[\]{}"). Rack also doesn't allow uppercase
+    # ASCII (A-Z) in header keys.
     #
     #   token          = 1*tchar
     #
@@ -328,7 +398,15 @@ describe Rack::Lint do
       }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}").
       message.must_equal("invalid header name: #{invalid_header}")
     end
-    valid_headers = 0.upto(127).map(&:chr) - invalid_headers
+    ('A'..'Z').each do |invalid_header|
+      lambda {
+        Rack::Lint.new(lambda { |env|
+          [200, { invalid_header => "text/plain" }, []]
+        }).call(env({}))
+      }.must_raise(Rack::Lint::LintError, "on invalid header: #{invalid_header}").
+      message.must_equal("uppercase character in header name: #{invalid_header}")
+    end
+    valid_headers = 0.upto(127).map(&:chr) - invalid_headers - ('A'..'Z').to_a
     valid_headers.each do |valid_header|
       Rack::Lint.new(lambda { |env|
                        [200, { valid_header => "text/plain" }, []]
@@ -337,52 +415,42 @@ describe Rack::Lint do
 
     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"
+      message.must_equal "a header value must be a String or Array of Strings, but the value of 'foo' is a Object"
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, { "Foo" => [1, 2, 3] }, []]
+                       [200, { "foo-bar" => "text\000plain" }, []]
                      }).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"
-
+      message.must_match(/invalid header/)
 
     lambda {
       Rack::Lint.new(lambda { |env|
-                       [200, { "Foo-Bar" => "text\000plain" }, []]
+                     [200, [%w(content-type text/plain), %w(content-length 0)], []]
                      }).call(env({}))
     }.must_raise(Rack::Lint::LintError).
-      message.must_match(/invalid header/)
+      message.must_equal "headers object should be a hash, but isn't (got Array as headers)"
 
-    # 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" }, []]
-                   }).call(env({})).first.must_equal 200
-
-    # non-Hash header responses.must_be :allowed?
-    Rack::Lint.new(lambda { |env|
-                     [200, [%w(Content-Type text/plain), %w(Content-Length 0)], []]
-                   }).call(env({})).first.must_equal 200
   end
 
   it "notice content-type errors" do
     # lambda {
     #   Rack::Lint.new(lambda { |env|
-    #                    [200, {"Content-length" => "0"}, []]
+    #                    [200, {"content-length" => "0"}, []]
     #                  }).call(env({}))
     # }.must_raise(Rack::Lint::LintError).
-    #   message.must_match(/No Content-Type/)
+    #   message.must_match(/No content-type/)
 
     [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/)
+        message.must_match(/content-type header found/)
     end
   end
 
@@ -390,35 +458,186 @@ 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/)
+        message.must_match(/content-length header found/)
     end
 
     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/)
+      message.must_match(/content-length header was 1, but should be 0/)
+  end
+
+  it "responds to to_path" do
+    body = Object.new
+    def body.each; end
+    def body.to_path; __FILE__ end
+    app = lambda { |env| [200, {}, body] }
+
+    body = Rack::Lint.new(app).call(env({}))[2]
+    body.must_respond_to(:to_path)
+    body.to_path.must_equal __FILE__
   end
 
   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).
       message.must_match(/yielded non-string/)
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               [200, { "content-type" => "text/plain", "content-length" => "3" }, Object.new]
+                             }).call(env({}))[2]
+      body.respond_to?(:to_ary).must_equal false
+      body.each { |part| }
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'Enumerable Body must respond to each'
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               [200, { "content-type" => "text/plain", "content-length" => "0" }, []]
+                             }).call(env({}))[2]
+      body.each { |part| }
+      body.each { |part| }
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'Response body must only be invoked once (each)'
+
+    # Lint before and after the Rack middleware being tested.
+    def stacked_lint(app)
+      Rack::Lint.new(lambda do |env|
+        Rack::Lint.new(app).call(env).tap {|response| response[2] = yield response[2]}
+      end)
+    end
+
+    yielder_app = lambda do |_|
+      input = Object.new
+      def input.each; 10.times {yield 'foo'}; end
+      [200, {"content-type" => "text/plain", "content-length" => "30"}, input]
+    end
+
+    lambda {
+      body = stacked_lint(yielder_app) {|body|
+        new_body = Struct.new(:body) do
+          def each(&block)
+            body.each { |part| yield part.upcase }
+            body.close
+          end
+        end
+        new_body.new(body)
+      }.call(env({}))[2]
+      body.each {|part| part.must_equal 'FOO'}
+      body.close
+    }.call
+
+    lambda {
+      body = stacked_lint(yielder_app) { |body|
+        body.enum_for.to_a
+      }.call(env({}))[2]
+      body.each {}
+      body.close
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/Middleware must not call #each directly/)
+
+    lambda {
+      body = stacked_lint(yielder_app) { |body|
+        new_body = Struct.new(:body) do
+          def each(&block)
+            body.enum_for.each_slice(2) { |parts| yield parts.join }
+          end
+        end
+        new_body.new(body)
+      }.call(env({}))[2]
+      body.each {}
+      body.close
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/New body must yield at least once per iteration of old body/)
+
+    lambda {
+      body = stacked_lint(yielder_app) { |body|
+        Struct.new(:body) do
+          def each; body.each {|part| yield part} end
+        end.new(body)
+      }.call(env({}))[2]
+      body.each {}
+      body.close
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/Body has not been closed/)
+
+    static_app = lambda do |_|
+      input = ['foo'] * 10
+      [200, {"content-type" => "text/plain", "content-length" => "30"}, input]
+    end
+
+    lambda {
+      body = stacked_lint(static_app) { |body| body.to_ary}.call(env({}))[2]
+      body.each {}
+      body.close
+    }.call
+
+    array_mismatch = lambda do |_|
+      input = Object.new
+      def input.to_ary; ['bar'] * 10; end
+      def input.each; 10.times {yield 'foo'}; end
+      [200, {"content-type" => "text/plain", "content-length" => "30"}, input]
+    end
+
+    lambda {
+      body = stacked_lint(array_mismatch) { |body| body.to_ary}.call(env({}))[2]
+      body.each {}
+      body.close
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/#to_ary not identical to contents produced by calling #each/)
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               to_path = Object.new
+                               def to_path.each; end
+                               def to_path.to_path; 'non-existent' end
+                               [200, { "content-type" => "text/plain", "content-length" => "0" }, to_path]
+                             }).call(env({}))[2]
+      body.each { |part| }
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'The file identified by body.to_path does not exist'
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               [200, { "content-type" => "text/plain", "content-length" => "0" }, Object.new]
+                             }).call(env({}))[2]
+      body.call(nil)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'Streaming Body must respond to call'
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               [200, { "content-type" => "text/plain", "content-length" => "0" }, proc{}]
+                             }).call(env({}))[2]
+      body.call(StringIO.new)
+      body.call(nil)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'Response body must only be invoked once (call)'
+
+    lambda {
+      body = Rack::Lint.new(lambda { |env|
+                               [200, { "content-type" => "text/plain", "content-length" => "0" }, proc{}]
+                             }).call(env({}))[2]
+      body.call(nil)
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'Stream must respond to read'
   end
 
   it "notice input handling errors" 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/)
@@ -427,7 +646,7 @@ describe Rack::Lint do
       Rack::Lint.new(lambda { |env|
                        env["rack.input"].gets
                        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/)
@@ -435,7 +654,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/)
@@ -443,7 +662,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/)
@@ -451,7 +670,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/)
@@ -459,19 +678,11 @@ 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/)
 
-    lambda {
-      Rack::Lint.new(lambda { |env|
-                       env["rack.input"].rewind(0)
-                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
-                     }).call(env({}))
-    }.must_raise(Rack::Lint::LintError).
-      message.must_match(/rewind called with arguments/)
-
     weirdio = Object.new
     class << weirdio
       def gets
@@ -486,10 +697,6 @@ describe Rack::Lint do
         yield 23
         yield 42
       end
-
-      def rewind
-        raise Errno::ESPIPE, "Errno::ESPIPE"
-      end
     end
 
     eof_weirdio = Object.new
@@ -504,23 +711,28 @@ describe Rack::Lint do
 
       def each
       end
-
-      def rewind
-      end
     end
 
     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/)
 
+    lambda {
+      Rack::Lint.new(lambda { |env|
+                       env["rack.input"].each(1) { |x| }
+                       [201, { "content-type" => "text/plain", "content-length" => "0" }, []]
+                     }).call(env("rack.input" => weirdio))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_match(/rack.input#each called with arguments/)
+
     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/)
@@ -528,7 +740,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/)
@@ -536,34 +748,28 @@ 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/)
+  end
 
-    lambda {
-      Rack::Lint.new(lambda { |env|
-                       env["rack.input"].rewind
-                       [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/)
+  it "can call close" do
+    app = lambda do |env|
+      env["rack.input"].close
+      [201, {"content-type" => "text/plain", "content-length" => "0"}, []]
+    end
 
+    response = Rack::Lint.new(app).call(env({}))
 
-    lambda {
-      Rack::Lint.new(lambda { |env|
-                       env["rack.input"].close
-                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
-                     }).call(env({}))
-    }.must_raise(Rack::Lint::LintError).
-      message.must_match(/close must not be called/)
+    response.first.must_equal 201
   end
 
   it "notice error handling errors" 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/)
@@ -571,7 +777,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/)
@@ -579,12 +785,12 @@ describe Rack::Lint do
 
   it "notice HEAD errors" do
     Rack::Lint.new(lambda { |env|
-                     [200, { "Content-type" => "test/plain", "Content-length" => "3" }, []]
+                     [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"]]
+                       [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/)
@@ -596,7 +802,7 @@ describe Rack::Lint do
 
     Rack::Lint.new(lambda { |env|
                      env["rack.input"].send(:read, *args)
-                     [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
+                     [201, { "content-type" => "text/plain", "content-length" => "0" }, []]
                    }).call(env({ "rack.input" => StringIO.new(hello_str) })).
       first.must_equal 201
   end
@@ -610,36 +816,33 @@ describe Rack::Lint do
     assert_lint 1, ''.dup
   end
 
-  it "notice hijack errors" do
+  it "notices when request env doesn't have a valid rack.hijack callback" do
     lambda {
       Rack::Lint.new(lambda { |env|
                        env['rack.hijack'].call
-                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
-                     }).call(env({ 'rack.hijack?' => true, 'rack.hijack' => lambda { Object.new } }))
+                       [201, { "content-type" => "text/plain", "content-length" => "0" }, []]
+                     }).call(env({ 'rack.hijack' => Object.new }))
     }.must_raise(Rack::Lint::LintError).
-      message.must_match(/rack.hijack_io must respond to read/)
-
-      Rack::Lint.new(lambda { |env|
-                       env['rack.hijack'].call
-                       [201, { "Content-type" => "text/plain", "Content-length" => "0" }, []]
-                     }).call(env({ 'rack.hijack?' => true, 'rack.hijack' => lambda { StringIO.new }, 'rack.hijack_io' => StringIO.new })).
-        first.must_equal 201
+      message.must_match(/rack.hijack must respond to call/)
+  end
 
+  it "notices when the response headers don't have a valid rack.hijack callback" do
+    lambda {
       Rack::Lint.new(lambda { |env|
-                       env['rack.hijack?'] = true
-                       [201, { "Content-type" => "text/plain", "Content-length" => "0", 'rack.hijack' => lambda {|io| io }, 'rack.hijack_io' => StringIO.new }, []]
-                     }).call(env({}))[1]['rack.hijack'].call(StringIO.new).read.must_equal ''
+                       [201, { "content-type" => "text/plain", "content-length" => "0", 'rack.hijack' =>  Object.new }, []]
+                     }).call(env({ 'rack.hijack?' => true }))
+    }.must_raise(Rack::Lint::LintError).
+      message.must_equal 'rack.hijack header must respond to #call'
   end
 
-end
+  it "pass valid rack.response_finished" do
+    callable_object = Class.new do
+      def call(env, status, headers, error)
+      end
+    end.new
 
-describe "Rack::Lint::InputWrapper" do
-  it "delegate :rewind to underlying IO object" do
-    io = StringIO.new("123")
-    wrapper = Rack::Lint::InputWrapper.new(io)
-    wrapper.read.must_equal "123"
-    wrapper.read.must_equal ""
-    wrapper.rewind
-    wrapper.read.must_equal "123"
+    Rack::Lint.new(lambda { |env|
+                     [200, {}, ["foo"]]
+                   }).call(env({ "rack.response_finished" => [-> (env) {}, lambda { |env| }, callable_object], "content-length" => "3" })).first.must_equal 200
   end
 end
diff --git a/test/spec_lobster.rb b/test/spec_lobster.rb
deleted file mode 100644
index ac3f11934c258b4e29e7dfebe50b58e6bbe1e8e7..0000000000000000000000000000000000000000
--- a/test/spec_lobster.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'rack/lobster'
-
-module LobsterHelpers
-  def lobster
-    Rack::MockRequest.new Rack::Lint.new(Rack::Lobster.new)
-  end
-
-  def lambda_lobster
-    Rack::MockRequest.new Rack::Lint.new(Rack::Lobster::LambdaLobster)
-  end
-end
-
-describe Rack::Lobster::LambdaLobster do
-  include LobsterHelpers
-
-  it "be a single lambda" do
-    Rack::Lobster::LambdaLobster.must_be_kind_of Proc
-  end
-
-  it "look like a lobster" do
-    res = lambda_lobster.get("/")
-    res.must_be :ok?
-    res.body.must_include "(,(,,(,,,("
-    res.body.must_include "?flip"
-  end
-
-  it "be flippable" do
-    res = lambda_lobster.get("/?flip")
-    res.must_be :ok?
-    res.body.must_include "(,,,(,,(,("
-  end
-end
-
-describe Rack::Lobster do
-  include LobsterHelpers
-
-  it "look like a lobster" do
-    res = lobster.get("/")
-    res.must_be :ok?
-    res.body.must_include "(,(,,(,,,("
-    res.body.must_include "?flip"
-    res.body.must_include "crash"
-  end
-
-  it "be flippable" do
-    res = lobster.get("/?flip=left")
-    res.must_be :ok?
-    res.body.must_include "),,,),,),)"
-  end
-
-  it "provide crashing for testing purposes" do
-    lambda {
-      lobster.get("/?flip=crash")
-    }.must_raise RuntimeError
-  end
-end
diff --git a/test/spec_lock.rb b/test/spec_lock.rb
index 895704986b6c75ae2b147d036ca740499520628c..dfc07449c9062dd343e5bf4cf85fe4b7f2326f46 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/lock'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/lint'
+end
+
 class Lock
   attr_reader :synchronized
 
@@ -43,7 +49,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 }
@@ -57,7 +63,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
@@ -69,7 +75,7 @@ describe Rack::Lock do
 
       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
@@ -78,13 +84,13 @@ describe Rack::Lock do
 
   it 'call super on close' do
     env      = Rack::MockRequest.env_for("/")
-    response = Class.new {
+    response = Class.new do
       attr_accessor :close_called
       def initialize; @close_called = false; end
       def close; @close_called = true; end
-    }.new
+    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
@@ -95,7 +101,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
@@ -105,7 +111,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)
@@ -117,7 +123,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
@@ -139,28 +145,6 @@ describe Rack::Lock do
     lock.synchronized.must_equal false
   end
 
-  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 }]
-    }, false)
-    env = Rack::MockRequest.env_for("/")
-    env['rack.multithread'].must_equal true
-    _, _, body = app.call(env)
-    body.close
-    env['rack.multithread'].must_equal true
-  end
-
-  it "reset original multithread flag when exiting lock" do
-    app = Class.new(Rack::Lock) {
-      def call(env)
-        env['rack.multithread'].must_equal true
-        super
-      end
-    }.new(lambda { |env| [200, { "Content-Type" => "text/plain" }, %w{ a b c }] })
-    Rack::Lint.new(app).call(Rack::MockRequest.env_for("/"))
-  end
-
   it 'not unlock if an error is raised before the mutex is locked' do
     lock = Class.new do
       def initialize() @unlocked = false end
@@ -169,21 +153,11 @@ 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
 
-  it "not reset the environment while the body is proxied" do
-    proxy = Class.new 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)] }
-    response = app.call(Rack::MockRequest.env_for("/"))[2]
-    response.env['rack.multithread'].must_equal false
-  end
-
   it "unlock if an exception occurs before returning" do
     lock = Lock.new
     env  = Rack::MockRequest.env_for("/")
@@ -194,7 +168,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 8355fc828485a2a118e1618121f6b18f5cef9016..ab20d3638715fb52426f7b7ac3d6d38f619fb034 100644
--- a/test/spec_logger.rb
+++ b/test/spec_logger.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/logger'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Logger do
   app = lambda { |env|
     log = env['rack.logger']
@@ -9,7 +15,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 a00a767e04a7a09ab2a59a1a12d47396b15ef6ab..1e701f25ce0056d24c7bfa86e5cbac893d599524 100644
--- a/test/spec_media_type.rb
+++ b/test/spec_media_type.rb
@@ -2,6 +2,10 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/media_type'
+end
+
 describe Rack::MediaType do
   before { @empty_hash = {} }
 
diff --git a/test/spec_method_override.rb b/test/spec_method_override.rb
index ddb105bdfce0883c581c2db21db8eb6ab78b287a..f3b8ad729f790441098b6d5081ca40fcfc627192 100644
--- a/test/spec_method_override.rb
+++ b/test/spec_method_override.rb
@@ -2,10 +2,16 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/method_override'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 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
 
@@ -94,10 +100,10 @@ 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/
+    env[Rack::RACK_ERRORS].read.must_include 'Bad request content body'
   end
 
   it "not modify REQUEST_METHOD for POST requests when the params are unparseable because too deep" do
@@ -113,4 +119,15 @@ EOF
 
     env["REQUEST_METHOD"].must_equal "POST"
   end
+
+  it "not set form input when the content type is JSON" do
+    env = Rack::MockRequest.env_for("/",
+      "CONTENT_TYPE" => "application/json",
+      method: "POST",
+      input: '{"_method":"options"}')
+    app.call env
+
+    env["REQUEST_METHOD"].must_equal "POST"
+    env["rack.request.form_input"].must_be_nil
+  end
 end
diff --git a/test/spec_mime.rb b/test/spec_mime.rb
index 65a77f6f0f0294d1b4965656c2ac5427b762a5ea..4a8ff7c97a2669732e186313680fce77fa6d0c06 100644
--- a/test/spec_mime.rb
+++ b/test/spec_mime.rb
@@ -2,6 +2,10 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/mime'
+end
+
 describe Rack::Mime do
 
   it "should return the fallback mime-type for files with no extension" do
diff --git a/test/spec_mock.rb b/test/spec_mock_request.rb
similarity index 57%
rename from test/spec_mock.rb
rename to test/spec_mock_request.rb
index ed679c3e9bee614cde4832f0f417cfdc64f98695..b2a16fdedf8ffed7a73c5ed15f12b03357dba161 100644
--- a/test/spec_mock.rb
+++ b/test/spec_mock_request.rb
@@ -4,6 +4,13 @@ require_relative 'helper'
 require 'yaml'
 require_relative 'psych_fix'
 
+separate_testing do
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/request'
+  require_relative '../lib/rack/body_proxy'
+end
+
 app = Rack::Lint.new(lambda { |env|
   req = Rack::Request.new(env)
 
@@ -17,11 +24,13 @@ app = Rack::Lint.new(lambda { |env|
   response = Rack::Response.new(
     body,
     req.GET["status"] || 200,
-    "Content-Type" => "text/yaml"
+    "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.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" })
+  response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" })
   response.finish
 })
 
@@ -34,7 +43,13 @@ describe Rack::MockRequest do
   it "be able to only return the environment" do
     env = Rack::MockRequest.env_for("")
     env.must_be_kind_of Hash
-    env.must_include "rack.version"
+  end
+
+  it "should handle a non-GET request with both :input and :params" do
+    env = Rack::MockRequest.env_for("/", method: :post, input: nil, params: {})
+    env["PATH_INFO"].must_equal "/"
+    env.must_be_kind_of Hash
+    env['rack.input'].read.must_equal ''
   end
 
   it "return an environment with a path" do
@@ -42,7 +57,6 @@ describe Rack::MockRequest do
     env["QUERY_STRING"].must_equal "location[]=1&location[]=2&age_group[]=2"
     env["PATH_INFO"].must_equal "/parse"
     env.must_be_kind_of Hash
-    env.must_include "rack.version"
   end
 
   it "provide sensible defaults" do
@@ -52,6 +66,7 @@ describe Rack::MockRequest do
     env["REQUEST_METHOD"].must_equal "GET"
     env["SERVER_NAME"].must_equal "example.org"
     env["SERVER_PORT"].must_equal "80"
+    env["SERVER_PROTOCOL"].must_equal "HTTP/1.1"
     env["QUERY_STRING"].must_equal ""
     env["PATH_INFO"].must_equal "/"
     env["SCRIPT_NAME"].must_equal ""
@@ -160,22 +175,34 @@ describe Rack::MockRequest do
     env["REQUEST_METHOD"].must_equal "GET"
   end
 
+  it "accept :script_name option to set SCRIPT_NAME" do
+    res = Rack::MockRequest.new(app).get("/", script_name: '/foo')
+    env = YAML.unsafe_load(res.body)
+    env["SCRIPT_NAME"].must_equal "/foo"
+  end
+
+  it "accept :http_version option to set SERVER_PROTOCOL" do
+    res = Rack::MockRequest.new(app).get("/", http_version: 'HTTP/1.0')
+    env = YAML.unsafe_load(res.body)
+    env["SERVER_PROTOCOL"].must_equal "HTTP/1.0"
+  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" } })
     env = YAML.unsafe_load(res.body)
     env["REQUEST_METHOD"].must_equal "GET"
     env["QUERY_STRING"].must_include "baz=2"
-    env["QUERY_STRING"].must_include "foo[bar]=1"
+    env["QUERY_STRING"].must_include "foo%5Bbar%5D=1"
     env["PATH_INFO"].must_equal "/foo"
     env["mock.postdata"].must_equal ""
   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%5Bbar%5D=1")
     env = YAML.unsafe_load(res.body)
     env["REQUEST_METHOD"].must_equal "GET"
     env["QUERY_STRING"].must_include "baz=2"
-    env["QUERY_STRING"].must_include "foo[bar]=1"
+    env["QUERY_STRING"].must_include "foo%5Bbar%5D=1"
     env["PATH_INFO"].must_equal "/foo"
     env["mock.postdata"].must_equal ""
   end
@@ -187,17 +214,17 @@ describe Rack::MockRequest do
     env["QUERY_STRING"].must_equal ""
     env["PATH_INFO"].must_equal "/foo"
     env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded"
-    env["mock.postdata"].must_equal "foo[bar]=1"
+    env["mock.postdata"].must_equal "foo%5Bbar%5D=1"
   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%5Bbar%5D=1")
     env = YAML.unsafe_load(res.body)
     env["REQUEST_METHOD"].must_equal "POST"
     env["QUERY_STRING"].must_equal ""
     env["PATH_INFO"].must_equal "/foo"
     env["CONTENT_TYPE"].must_equal "application/x-www-form-urlencoded"
-    env["mock.postdata"].must_equal "foo[bar]=1"
+    env["mock.postdata"].must_equal "foo%5Bbar%5D=1"
   end
 
   it "accept params and build multipart encoded params for POST requests" do
@@ -221,7 +248,7 @@ describe Rack::MockRequest do
   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)
     called.must_equal true
@@ -244,185 +271,3 @@ describe Rack::MockRequest do
     end
   end
 end
-
-describe Rack::MockResponse do
-  it 'has standard constructor' do
-    headers = { "header" => "value" }
-    body = ["body"]
-
-    response = Rack::MockResponse[200, headers, body]
-
-    response.status.must_equal 200
-    response.headers.must_equal headers
-    response.body.must_equal body.join
-  end
-
-  it "provide access to the HTTP status" do
-    res = Rack::MockRequest.new(app).get("")
-    res.must_be :successful?
-    res.must_be :ok?
-
-    res = Rack::MockRequest.new(app).get("/?status=404")
-    res.wont_be :successful?
-    res.must_be :client_error?
-    res.must_be :not_found?
-
-    res = Rack::MockRequest.new(app).get("/?status=501")
-    res.wont_be :successful?
-    res.must_be :server_error?
-
-    res = Rack::MockRequest.new(app).get("/?status=307")
-    res.must_be :redirect?
-
-    res = Rack::MockRequest.new(app).get("/?status=201", lint: true)
-    res.must_be :empty?
-  end
-
-  it "provide access to the HTTP headers" do
-    res = Rack::MockRequest.new(app).get("")
-    res.must_include "Content-Type"
-    res.headers["Content-Type"].must_equal "text/yaml"
-    res.original_headers["Content-Type"].must_equal "text/yaml"
-    res["Content-Type"].must_equal "text/yaml"
-    res.content_type.must_equal "text/yaml"
-    res.content_length.wont_equal 0
-    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/)
-    assert_match(res, /rack/)
-
-    res.match('rack')[0].must_equal 'rack'
-    res.match('banana').must_be_nil
-  end
-
-  it "provide access to the Rack errors" do
-    res = Rack::MockRequest.new(app).get("/?error=foo", lint: true)
-    res.must_be :ok?
-    res.errors.wont_be :empty?
-    res.errors.must_include "foo"
-  end
-
-  it "allow calling body.close afterwards" do
-    # this is exactly what rack-test does
-    body = StringIO.new("hi")
-    res = Rack::MockResponse.new(200, {}, body)
-    body.close if body.respond_to?(:close)
-    res.body.must_equal 'hi'
-  end
-
-  it "optionally make Rack errors fatal" do
-    lambda {
-      Rack::MockRequest.new(app).get("/?error=foo", fatal: true)
-    }.must_raise Rack::MockRequest::FatalWarning
-
-    lambda {
-      Rack::MockRequest.new(lambda { |env| env['rack.errors'].write(env['rack.errors'].string) }).get("/", fatal: true)
-    }.must_raise(Rack::MockRequest::FatalWarning).message.must_equal ''
-  end
-end
-
-describe Rack::MockResponse, 'headers' do
-  before do
-    @res = Rack::MockRequest.new(app).get('')
-    @res.set_header 'FOO', '1'
-  end
-
-  it 'has_header?' do
-    lambda { @res.has_header? nil }.must_raise NoMethodError
-
-    @res.has_header?('FOO').must_equal true
-    @res.has_header?('Foo').must_equal true
-  end
-
-  it 'get_header' do
-    lambda { @res.get_header nil }.must_raise NoMethodError
-
-    @res.get_header('FOO').must_equal '1'
-    @res.get_header('Foo').must_equal '1'
-  end
-
-  it 'set_header' do
-    lambda { @res.set_header nil, '1' }.must_raise NoMethodError
-
-    @res.set_header('FOO', '2').must_equal '2'
-    @res.get_header('FOO').must_equal '2'
-
-    @res.set_header('Foo', '3').must_equal '3'
-    @res.get_header('Foo').must_equal '3'
-    @res.get_header('FOO').must_equal '3'
-
-    @res.set_header('FOO', nil).must_be_nil
-    @res.get_header('FOO').must_be_nil
-    @res.has_header?('FOO').must_equal true
-  end
-
-  it 'add_header' do
-    lambda { @res.add_header nil, '1' }.must_raise NoMethodError
-
-    # Sets header on first addition
-    @res.add_header('FOO', '1').must_equal '1,1'
-    @res.get_header('FOO').must_equal '1,1'
-
-    # Ignores nil additions
-    @res.add_header('FOO', nil).must_equal '1,1'
-    @res.get_header('FOO').must_equal '1,1'
-
-    # Converts additions to strings
-    @res.add_header('FOO', 2).must_equal '1,1,2'
-    @res.get_header('FOO').must_equal '1,1,2'
-
-    # Respects underlying case-sensitivity
-    @res.add_header('Foo', 'yep').must_equal '1,1,2,yep'
-    @res.get_header('Foo').must_equal '1,1,2,yep'
-    @res.get_header('FOO').must_equal '1,1,2,yep'
-  end
-
-  it 'delete_header' do
-    lambda { @res.delete_header nil }.must_raise NoMethodError
-
-    @res.delete_header('FOO').must_equal '1'
-    @res.has_header?('FOO').must_equal false
-
-    @res.has_header?('Foo').must_equal false
-    @res.delete_header('Foo').must_be_nil
-  end
-end
diff --git a/test/spec_mock_response.rb b/test/spec_mock_response.rb
new file mode 100644
index 0000000000000000000000000000000000000000..83fba9287315e1c5a41d532f5e441bd2c6301dde
--- /dev/null
+++ b/test/spec_mock_response.rb
@@ -0,0 +1,276 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+require 'yaml'
+require_relative 'psych_fix'
+
+separate_testing do
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/mock_response'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/request'
+end
+
+app = Rack::Lint.new(lambda { |env|
+  req = Rack::Request.new(env)
+
+  env["mock.postdata"] = env["rack.input"].read
+  if req.GET["error"]
+    env["rack.errors"].puts req.GET["error"]
+    env["rack.errors"].flush
+  end
+
+  body = req.head? ? "" : env.to_yaml
+  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.set_cookie("persistent_with_expires_test", { value: "persistent_with_expires_test", expires: Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT"), path: "/" })
+  response.set_cookie("expires_and_max-age_test", { value: "expires_and_max-age_test", expires: Time.now + 15552000 * 2, max_age: 15552000, path: "/" })
+  response.finish
+})
+
+describe Rack::MockResponse do
+  it 'has standard constructor' do
+    headers = { "header" => "value" }
+    body = ["body"]
+
+    response = Rack::MockResponse[200, headers, body]
+
+    response.status.must_equal 200
+    response.headers.must_equal headers
+    response.body.must_equal body.join
+  end
+
+  it "provide access to the HTTP status" do
+    res = Rack::MockRequest.new(app).get("")
+    res.must_be :successful?
+    res.must_be :ok?
+
+    res = Rack::MockRequest.new(app).get("/?status=404")
+    res.wont_be :successful?
+    res.must_be :client_error?
+    res.must_be :not_found?
+
+    res = Rack::MockRequest.new(app).get("/?status=501")
+    res.wont_be :successful?
+    res.must_be :server_error?
+
+    res = Rack::MockRequest.new(app).get("/?status=307")
+    res.must_be :redirect?
+
+    res = Rack::MockRequest.new(app).get("/?status=201", lint: true)
+    res.must_be :empty?
+  end
+
+  it "provide access to the HTTP headers" do
+    res = Rack::MockRequest.new(app).get("")
+    res.must_include "content-type"
+    res.headers["content-type"].must_equal "text/yaml"
+    res.original_headers["content-type"].must_equal "text/yaml"
+    res["content-type"].must_equal "text/yaml"
+    res.content_type.must_equal "text/yaml"
+    res.content_length.wont_equal 0
+    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 "provides access to persistent cookies set with max-age" 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 "provides access to persistent cookies set with expires" do
+    res = Rack::MockRequest.new(app).get("")
+    persistent_cookie = res.cookie("persistent_with_expires_test")
+    persistent_cookie.value[0].must_equal "persistent_with_expires_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_equal Time.httpdate("Thu, 31 Oct 2021 07:28:00 GMT")
+  end
+
+  it "parses cookies giving max-age precedence over expires" do
+    res = Rack::MockRequest.new(app).get("")
+    persistent_cookie = res.cookie("expires_and_max-age_test")
+    persistent_cookie.value[0].must_equal "expires_and_max-age_test"
+    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 "parses cookie headers with equals sign at the end" do
+    res = Rack::MockRequest.new(->(env) { [200, { "Set-Cookie" => "__cf_bm=_somebase64encodedstringwithequalsatthened=; array=awesome" }, [""]] }).get("")
+    cookie = res.cookie("__cf_bm")
+    cookie.value[0].must_equal "_somebase64encodedstringwithequalsatthened="
+  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
+
+  deprecated "parses cookie headers provided as an array" do
+    res = Rack::MockRequest.new(->(env) { [200, [["set-cookie", "array=awesome"]], [""]] }).get("")
+    array_cookie = res.cookie("array")
+    array_cookie.value[0].must_equal "awesome"
+  end
+
+  deprecated "parses multiple set-cookie headers provided as an array" do
+    cookie_headers = [["set-cookie", "array=awesome\nmultiple=times"]]
+    res = Rack::MockRequest.new(->(env) { [200, cookie_headers, [""]] }).get("")
+    array_cookie = res.cookie("array")
+    array_cookie.value[0].must_equal "awesome"
+    second_cookie = res.cookie("multiple")
+    second_cookie.value[0].must_equal "times"
+  end
+
+  it "parses multiple set-cookie headers provided as hash with array value" do
+    cookie_headers = { "set-cookie" => ["array=awesome", "multiple=times"]}
+    res = Rack::MockRequest.new(->(env) { [200, cookie_headers, [""]] }).get("")
+    array_cookie = res.cookie("array")
+    array_cookie.value[0].must_equal "awesome"
+    second_cookie = res.cookie("multiple")
+    second_cookie.value[0].must_equal "times"
+  end
+
+  it "provide access to the HTTP body" do
+    res = Rack::MockRequest.new(app).get("")
+    res.body.must_match(/rack/)
+    assert_match(res, /rack/)
+
+    res.match('rack')[0].must_equal 'rack'
+    res.match('banana').must_be_nil
+  end
+
+  it "provide access to the Rack errors" do
+    res = Rack::MockRequest.new(app).get("/?error=foo", lint: true)
+    res.must_be :ok?
+    res.errors.wont_be :empty?
+    res.errors.must_include "foo"
+  end
+
+  deprecated "handle enumerable headers that are not a hash" do
+    # this is exactly what rack-test does
+    res = Rack::MockResponse.new(200, [], [])
+    res.cookies.must_equal({})
+  end
+
+  it "allow calling body.close afterwards" do
+    # this is exactly what rack-test does
+    body = StringIO.new("hi")
+    res = Rack::MockResponse.new(200, {}, body)
+    body.close if body.respond_to?(:close)
+    res.body.must_equal 'hi'
+  end
+
+  it "ignores plain strings passed as errors" do
+    Rack::MockResponse.new(200, {}, [], 'e').errors.must_be_nil
+  end
+
+  it "optionally make Rack errors fatal" do
+    lambda {
+      Rack::MockRequest.new(app).get("/?error=foo", fatal: true)
+    }.must_raise Rack::MockRequest::FatalWarning
+
+    lambda {
+      Rack::MockRequest.new(lambda { |env| env['rack.errors'].write(env['rack.errors'].string) }).get("/", fatal: true)
+    }.must_raise(Rack::MockRequest::FatalWarning).message.must_equal ''
+  end
+end
+
+describe Rack::MockResponse, 'headers' do
+  before do
+    @res = Rack::MockRequest.new(app).get('')
+    @res.set_header 'FOO', '1'
+  end
+
+  it 'has_header?' do
+    lambda { @res.has_header? nil }.must_raise ArgumentError
+
+    @res.has_header?('FOO').must_equal true
+    @res.has_header?('Foo').must_equal true
+  end
+
+  it 'get_header' do
+    lambda { @res.get_header nil }.must_raise ArgumentError
+
+    @res.get_header('FOO').must_equal '1'
+    @res.get_header('Foo').must_equal '1'
+  end
+
+  it 'set_header' do
+    lambda { @res.set_header nil, '1' }.must_raise ArgumentError
+
+    @res.set_header('FOO', '2').must_equal '2'
+    @res.get_header('FOO').must_equal '2'
+
+    @res.set_header('Foo', '3').must_equal '3'
+    @res.get_header('Foo').must_equal '3'
+    @res.get_header('FOO').must_equal '3'
+
+    @res.set_header('FOO', nil).must_be_nil
+    @res.get_header('FOO').must_be_nil
+    @res.has_header?('FOO').must_equal true
+  end
+
+  it 'add_header' do
+    lambda { @res.add_header nil, '1' }.must_raise ArgumentError
+
+    # Sets header on first addition
+    @res.add_header('FOO', '1').must_equal ['1', '1']
+    @res.get_header('FOO').must_equal ['1', '1']
+
+    # Ignores nil additions
+    @res.add_header('FOO', nil).must_equal ['1', '1']
+    @res.get_header('FOO').must_equal ['1', '1']
+
+    # Converts additions to strings
+    @res.add_header('FOO', 2).must_equal ['1', '1', '2']
+    @res.get_header('FOO').must_equal ['1', '1', '2']
+
+    # Respects underlying case-sensitivity
+    @res.add_header('Foo', 'yep').must_equal ['1', '1', '2', 'yep']
+    @res.get_header('Foo').must_equal ['1', '1', '2', 'yep']
+    @res.get_header('FOO').must_equal ['1', '1', '2', 'yep']
+  end
+
+  it 'delete_header' do
+    lambda { @res.delete_header nil }.must_raise ArgumentError
+
+    @res.delete_header('FOO').must_equal '1'
+    @res.has_header?('FOO').must_equal false
+
+    @res.has_header?('Foo').must_equal false
+    @res.delete_header('Foo').must_be_nil
+  end
+end
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index d5bf30d3b9fece6fdd1f5209e53bbd4af634d392..c415056b6afa485611760b6dac1d1229e750d75d 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -3,6 +3,15 @@
 require_relative 'helper'
 require 'timeout'
 
+separate_testing do
+  require_relative '../lib/rack/multipart'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/query_parser'
+  require_relative '../lib/rack/utils'
+  require_relative '../lib/rack/request'
+end
+
 describe Rack::Multipart do
   def multipart_fixture(name, boundary = "AaB03x")
     file = multipart_file(name)
@@ -26,6 +35,13 @@ describe Rack::Multipart do
     Rack::Multipart.parse_multipart(env).must_be_nil
   end
 
+  it "raises exception if boundary is too long" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename, "A"*71))
+    lambda {
+      Rack::Multipart.parse_multipart(env)
+    }.must_raise Rack::Multipart::Error
+  end
+
   it "parse multipart content when content type present but disposition is not" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_disposition))
     params = Rack::Multipart.parse_multipart(env)
@@ -49,18 +65,49 @@ describe Rack::Multipart do
     params["text"].must_equal "contents"
   end
 
+  it "raises for invalid data preceding the boundary" do
+    env = Rack::MockRequest.env_for '/', multipart_fixture(:preceding_boundary)
+    lambda {
+      Rack::Multipart.parse_multipart(env)
+    }.must_raise Rack::Multipart::EmptyContentError
+  end
+
+  it "ignores initial end boundaries" do
+    env = Rack::MockRequest.env_for '/', multipart_fixture(:end_boundary_first)
+    params = Rack::Multipart.parse_multipart(env)
+    params["files"][:filename].must_equal "foo"
+  end
+
+  it "parse multipart content with different filename and filename*" do
+    env = Rack::MockRequest.env_for '/', multipart_fixture(:filename_multi)
+    params = Rack::Multipart.parse_multipart(env)
+    params["files"][:filename].must_equal "bar"
+  end
+
   it "set US_ASCII encoding based on charset" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
     params = Rack::Multipart.parse_multipart(env)
     params["text"].encoding.must_equal Encoding::US_ASCII
 
     # I'm not 100% sure if making the param name encoding match the
-    # Content-Type charset is the right thing to do.  We should revisit this.
+    # content-type charset is the right thing to do.  We should revisit this.
     params.keys.each do |key|
       key.encoding.must_equal Encoding::US_ASCII
     end
   end
 
+  it "sets BINARY encoding for invalid charsets" do
+    env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_unknown_charset))
+    params = Rack::Multipart.parse_multipart(env)
+    params["text"].encoding.must_equal Encoding::BINARY
+
+    # I'm not 100% sure if making the param name encoding match the
+    # content-type charset is the right thing to do.  We should revisit this.
+    params.keys.each do |key|
+      key.encoding.must_equal Encoding::BINARY
+    end
+  end
+
   it "set BINARY encoding on things without content type" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
     params = Rack::Multipart.parse_multipart(env)
@@ -92,17 +139,6 @@ describe Rack::Multipart do
     params['user_sid'].encoding.must_equal Encoding::UTF_8
   end
 
-  it "raise ParamsTooDeepError if the key space is exhausted" do
-    env = Rack::MockRequest.env_for("/", multipart_fixture(:content_type_and_no_filename))
-
-    old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1
-    begin
-      lambda { Rack::Multipart.parse_multipart(env) }.must_raise(Rack::QueryParser::ParamsTooDeepError)
-    ensure
-      Rack::Utils.key_space_limit = old
-    end
-  end
-
   it "parse multipart form webkit style" do
     env = Rack::MockRequest.env_for '/', multipart_fixture(:webkit)
     env['CONTENT_TYPE'] = "multipart/form-data; boundary=----WebKitFormBoundaryWLHCs9qmcJJoyjKR"
@@ -128,12 +164,16 @@ describe Rack::Multipart do
 
         # make the initial boundary a few gigs long
         longer = "0123456789" * 1024 * 1024
-        (1024 * 1024).times { wr.write(longer) }
+        (1024 * 1024).times do
+          while wr.write_nonblock(longer, exception: false) == :wait_writable
+            Thread.pass
+          end
+        end
 
         wr.write("\r\n")
-        wr.write('Content-Disposition: form-data; name="a"; filename="a.txt"')
+        wr.write('content-disposition: form-data; name="a"; filename="a.txt"')
         wr.write("\r\n")
-        wr.write("Content-Type: text/plain\r\n")
+        wr.write("content-type: text/plain\r\n")
         wr.write("\r\na")
         wr.write("--AaB03x--\r\n")
         wr.close
@@ -151,7 +191,7 @@ describe Rack::Multipart do
     env = Rack::MockRequest.env_for '/', fixture
     lambda {
       Rack::Multipart.parse_multipart(env)
-    }.must_raise EOFError
+    }.must_raise Rack::Multipart::EmptyContentError
     rd.close
 
     err = thr.value
@@ -166,13 +206,14 @@ describe Rack::Multipart do
     data = StringIO.new
     data.write("--#{boundary}")
     data.write("\r\n")
-    data.write('Content-Disposition: form-data; name="a"; filename="a.pdf"')
+    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("content-type:application/pdf\r\n")
     data.write("\r\n")
     data.write("-" * (1024 * 1024))
     data.write("\r\n")
     data.write("--#{boundary}--\r\n")
+    data.rewind
 
     fixture = {
       "CONTENT_TYPE" => "multipart/form-data; boundary=#{boundary}",
@@ -184,7 +225,7 @@ describe Rack::Multipart do
     Timeout::timeout(10) { Rack::Multipart.parse_multipart(env) }
   end
 
-  it 'raises an EOF error on content-length mistmatch' do
+  it 'raises an EOF error on content-length mismatch' do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:empty))
     env['rack.input'] = StringIO.new
     assert_raises(EOFError) do
@@ -199,9 +240,9 @@ describe Rack::Multipart do
     params["submit-name-with-content"].must_equal "Berry"
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "file1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -213,7 +254,7 @@ describe Rack::Multipart do
         @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
-    query_parser = Rack::QueryParser.new c, 65536, 100
+    query_parser = Rack::QueryParser.new c, 100
     env = Rack::MockRequest.env_for("/", multipart_fixture(:text))
     params = Rack::Multipart.parse_multipart(env, query_parser)
     params[:files][:type].must_equal "text/plain"
@@ -230,9 +271,9 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["file1.txt"][:type].must_equal "text/plain"
     params["file1.txt"][:filename].must_equal "file1.txt"
-    params["file1.txt"][:head].must_equal "Content-Disposition: form-data; " +
+    params["file1.txt"][:head].must_equal "content-disposition: form-data; " +
       "filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["file1.txt"][:name].must_equal "file1.txt"
     params["file1.txt"][:tempfile].read.must_equal "contents"
   end
@@ -252,9 +293,9 @@ describe Rack::Multipart do
     params["foo"]["submit-name"].must_equal "Larry"
     params["foo"]["files"][:type].must_equal "text/plain"
     params["foo"]["files"][:filename].must_equal "file1.txt"
-    params["foo"]["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["foo"]["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"foo[files]\"; filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["foo"]["files"][:name].must_equal "foo[files]"
     params["foo"]["files"][:tempfile].read.must_equal "contents"
   end
@@ -266,9 +307,9 @@ describe Rack::Multipart do
 
     params["files"][:type].must_equal "image/png"
     params["files"][:filename].must_equal "rack-logo.png"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"rack-logo.png\"\r\n" +
-      "Content-Type: image/png\r\n"
+      "content-type: image/png\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.length.must_equal 26473
   end
@@ -279,9 +320,9 @@ describe Rack::Multipart do
     params["submit-name"].must_equal "Larry"
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "file1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal ""
   end
@@ -291,9 +332,9 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "fi;le1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"fi;le1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -305,9 +346,9 @@ describe Rack::Multipart do
     params["submit-name-with-content"].must_equal "Berry"
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "file1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -317,9 +358,9 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_match(/invalid/)
-    head = "Content-Disposition: form-data; " +
+    head = "content-disposition: form-data; " +
       "name=\"files\"; filename=\"invalid\xC3.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     head = head.force_encoding(Encoding::ASCII_8BIT)
     params["files"][:head].must_equal head
     params["files"][:name].must_equal "files"
@@ -344,7 +385,7 @@ describe Rack::Multipart do
     params["files"][:filename].must_equal "flowers.exe\u0000.jpg"
   end
 
-  it "is robust separating Content-Disposition fields" do
+  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"
@@ -371,10 +412,10 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "file1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; " +
       'filename="C:\Documents and Settings\Administrator\Desktop\file1.txt"' +
-      "\r\nContent-Type: text/plain\r\n"
+      "\r\ncontent-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -384,8 +425,8 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "image/jpeg"
     params["files"][:filename].must_equal "genome.jpeg"
-    params["files"][:head].must_equal "Content-Type: image/jpeg\r\n" +
-      "Content-Disposition: attachment; " +
+    params["files"][:head].must_equal "content-type: image/jpeg\r\n" +
+      "content-disposition: attachment; " +
       "name=\"files\"; " +
       "filename=genome.jpeg; " +
       "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" +
@@ -399,10 +440,10 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "application/octet-stream"
     params["files"][:filename].must_equal "escape \"quotes"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; " +
       "filename=\"escape \\\"quotes\"\r\n" +
-      "Content-Type: application/octet-stream\r\n"
+      "content-type: application/octet-stream\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -412,10 +453,10 @@ describe Rack::Multipart do
     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; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; " +
       "filename=\"foo+bar\"\r\n" +
-      "Content-Type: application/octet-stream\r\n"
+      "content-type: application/octet-stream\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -425,10 +466,10 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "application/octet-stream"
     params["files"][:filename].must_equal "escape \"quotes"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; " +
       "filename=\"escape %22quotes\"\r\n" +
-      "Content-Type: application/octet-stream\r\n"
+      "content-type: application/octet-stream\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -438,8 +479,8 @@ describe Rack::Multipart do
     params = Rack::Multipart.parse_multipart(env)
     params["files"][:type].must_equal "image/jpeg"
     params["files"][:filename].must_equal "\"human\" genome.jpeg"
-    params["files"][:head].must_equal "Content-Type: image/jpeg\r\n" +
-      "Content-Disposition: attachment; " +
+    params["files"][:head].must_equal "content-type: image/jpeg\r\n" +
+      "content-disposition: attachment; " +
       "name=\"files\"; " +
       "filename=\"\\\"human\\\" genome.jpeg\"; " +
       "modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\";\r\n" +
@@ -455,8 +496,8 @@ describe Rack::Multipart do
     files[:type].must_equal "image/jpeg"
     files[:filename].must_equal "100% of a photo.jpeg"
     files[:head].must_equal <<-MULTIPART
-Content-Disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r
-Content-Type: image/jpeg\r
+content-disposition: form-data; name="document[attachment]"; filename="100% of a photo.jpeg"\r
+content-type: image/jpeg\r
     MULTIPART
 
     files[:name].must_equal "document[attachment]"
@@ -470,8 +511,8 @@ Content-Type: image/jpeg\r
     files[:type].must_equal "image/jpeg"
     files[:filename].must_equal "100%a"
     files[:head].must_equal <<-MULTIPART
-Content-Disposition: form-data; name="document[attachment]"; filename="100%a"\r
-Content-Type: image/jpeg\r
+content-disposition: form-data; name="document[attachment]"; filename="100%a"\r
+content-type: image/jpeg\r
     MULTIPART
 
     files[:name].must_equal "document[attachment]"
@@ -485,26 +526,41 @@ Content-Type: image/jpeg\r
     files[:type].must_equal "image/jpeg"
     files[:filename].must_equal "100%"
     files[:head].must_equal <<-MULTIPART
-Content-Disposition: form-data; name="document[attachment]"; filename="100%"\r
-Content-Type: image/jpeg\r
+content-disposition: form-data; name="document[attachment]"; filename="100%"\r
+content-type: image/jpeg\r
     MULTIPART
 
     files[:name].must_equal "document[attachment]"
     files[:tempfile].read.must_equal "contents"
   end
 
-  it "rewinds input after parsing upload" do
-    options = multipart_fixture(:text)
-    input = options[:input]
+  it "raises RuntimeError for invalid file path" do
+    proc{Rack::Multipart::UploadedFile.new('non-existant')}.must_raise RuntimeError
+  end
+
+  it "supports uploading files in binary mode" do
+    Rack::Multipart::UploadedFile.new(multipart_file("file1.txt")).wont_be :binmode?
+    Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"), binary: true).must_be :binmode?
+  end
+
+  it "builds multipart body" do
+    files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"))
+    data  = Rack::Multipart.build_multipart("submit-name" => "Larry", "files" => files)
+
+    options = {
+      "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x",
+      "CONTENT_LENGTH" => data.length.to_s,
+      :input => StringIO.new(data)
+    }
     env = Rack::MockRequest.env_for("/", options)
     params = Rack::Multipart.parse_multipart(env)
     params["submit-name"].must_equal "Larry"
     params["files"][:filename].must_equal "file1.txt"
-    input.read.length.must_equal 307
+    params["files"][:tempfile].read.must_equal "contents"
   end
 
-  it "builds multipart body" do
-    files = Rack::Multipart::UploadedFile.new(multipart_file("file1.txt"))
+  it "builds multipart filename with space" do
+    files = Rack::Multipart::UploadedFile.new(multipart_file("space case.txt"))
     data  = Rack::Multipart.build_multipart("submit-name" => "Larry", "files" => files)
 
     options = {
@@ -515,7 +571,7 @@ Content-Type: image/jpeg\r
     env = Rack::MockRequest.env_for("/", options)
     params = Rack::Multipart.parse_multipart(env)
     params["submit-name"].must_equal "Larry"
-    params["files"][:filename].must_equal "file1.txt"
+    params["files"][:filename].must_equal "space case.txt"
     params["files"][:tempfile].read.must_equal "contents"
   end
 
@@ -620,7 +676,22 @@ Content-Type: image/jpeg\r
     end
   end
 
-  it "reach a multipart limit" do
+  it "treat a multipart limit of 0 as no limit" do
+    begin
+      previous_limit = Rack::Utils.multipart_part_limit
+      Rack::Utils.multipart_part_limit = 0
+
+      env = Rack::MockRequest.env_for '/', multipart_fixture(:three_files_three_fields)
+      params = Rack::Multipart.parse_multipart(env)
+      params['reply'].must_equal 'yes'
+      params['to'].must_equal 'people'
+      params['from'].must_equal 'others'
+    ensure
+      Rack::Utils.multipart_part_limit = previous_limit
+    end
+  end
+
+  it "reach a multipart file limit" do
     begin
       previous_limit = Rack::Utils.multipart_part_limit
       Rack::Utils.multipart_part_limit = 3
@@ -658,8 +729,8 @@ Content-Type: image/jpeg\r
   it "can parse fields with a content type" do
     data = <<-EOF
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon\r
-Content-Disposition: form-data; name="description"\r
-Content-Type: text/plain"\r
+content-disposition: form-data; name="description"\r
+content-type: text/plain"\r
 \r
 Very very blue\r
 --1yy3laWhgX31qpiHinh67wJXqKalukEUTvqTzmon--\r
@@ -686,8 +757,8 @@ EOF
   it "parse very long unquoted multipart file names" do
     data = <<-EOF
 --AaB03x\r
-Content-Type: text/plain\r
-Content-Disposition: attachment; name=file; filename=#{'long' * 100}\r
+content-type: text/plain\r
+content-disposition: attachment; name=file; filename=#{'long' * 100}\r
 \r
 contents\r
 --AaB03x--\r
@@ -707,8 +778,8 @@ contents\r
   it "parse unquoted parameter values at end of line" do
     data = <<-EOF
 --AaB03x\r
-Content-Type: text/plain\r
-Content-Disposition: attachment; name=inline\r
+content-type: text/plain\r
+content-disposition: attachment; name=inline\r
 \r
 true\r
 --AaB03x--\r
@@ -727,8 +798,8 @@ true\r
   it "parse quoted chars in name parameter" do
     data = <<-EOF
 --AaB03x\r
-Content-Type: text/plain\r
-Content-Disposition: attachment; name="quoted\\\\chars\\"in\rname"\r
+content-type: text/plain\r
+content-disposition: attachment; name="quoted\\\\chars\\"in\rname"\r
 \r
 true\r
 --AaB03x--\r
@@ -761,9 +832,9 @@ true\r
     params["submit-name-with-content"].must_equal "Berry"
     params["files"][:type].must_equal "text/plain"
     params["files"][:filename].must_equal "file1.txt"
-    params["files"][:head].must_equal "Content-Disposition: form-data; " +
+    params["files"][:head].must_equal "content-disposition: form-data; " +
       "name=\"files\"; filename=\"file1.txt\"\r\n" +
-      "Content-Type: text/plain\r\n"
+      "content-type: text/plain\r\n"
     params["files"][:name].must_equal "files"
     params["files"][:tempfile].read.must_equal "contents"
   end
@@ -773,15 +844,15 @@ true\r
 
     data = <<-EOF.dup
 --AaB03x\r
-Content-Type: text/plain\r
+content-type: text/plain\r
 \r
 some text\r
 --AaB03x\r
 \r
 \r
-some more text (I didn't specify Content-Type)\r
+some more text (I didn't specify content-type)\r
 --AaB03x\r
-Content-Type: image/png\r
+content-type: image/png\r
 \r
 #{rack_logo}\r
 --AaB03x--\r
@@ -795,7 +866,7 @@ Content-Type: image/png\r
     env = Rack::MockRequest.env_for("/", options)
     params = Rack::Multipart.parse_multipart(env)
 
-    params["text/plain"].must_equal ["some text", "some more text (I didn't specify Content-Type)"]
+    params["text/plain"].must_equal ["some text", "some more text (I didn't specify content-type)"]
     params["image/png"].length.must_equal 1
 
     f = Tempfile.new("rack-logo")
diff --git a/test/spec_null_logger.rb b/test/spec_null_logger.rb
index 435d051eadab6efd6b07649b632675178d69e18c..65ddb279e2b306f821ac0e553b36fb871fc5bb21 100644
--- a/test/spec_null_logger.rb
+++ b/test/spec_null_logger.rb
@@ -2,18 +2,24 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/null_logger'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 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_query_parser.rb b/test/spec_query_parser.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dbb8b14eda730928ea07f7a97d4aa9a91c09adb4
--- /dev/null
+++ b/test/spec_query_parser.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative 'helper'
+
+separate_testing do
+  require_relative '../lib/rack/query_parser'
+end
+
+describe Rack::QueryParser do
+  query_parser ||= Rack::QueryParser.make_default(8)
+
+  it "can normalize values with missing values" do
+    query_parser.parse_nested_query("a=a").must_equal({"a" => "a"})
+    query_parser.parse_nested_query("a=").must_equal({"a" => ""})
+    query_parser.parse_nested_query("a").must_equal({"a" => nil})
+  end
+end
diff --git a/test/spec_recursive.rb b/test/spec_recursive.rb
index 62e3a4f16bb547d7e84a754b610fa7f35cc21bd5..650626c0f9ee320847c0470b49a9fa6d6348b864 100644
--- a/test/spec_recursive.rb
+++ b/test/spec_recursive.rb
@@ -2,12 +2,19 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/recursive'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/urlmap'
+end
+
 describe Rack::Recursive do
   before do
   @app1 = lambda { |env|
     res = Rack::Response.new
-    res["X-Path-Info"] = env["PATH_INFO"]
-    res["X-Query-String"] = env["QUERY_STRING"]
+    res["x-path-info"] = env["PATH_INFO"]
+    res["x-query-string"] = env["QUERY_STRING"]
     res.finish do |inner_res|
       inner_res.write "App1"
     end
@@ -68,7 +75,7 @@ describe Rack::Recursive do
     res = Rack::MockRequest.new(app).get("/app4")
     res.must_be :ok?
     res.body.must_equal "App1"
-    res["X-Path-Info"].must_equal "/quux"
-    res["X-Query-String"].must_equal "meh"
+    res["x-path-info"].must_equal "/quux"
+    res["x-query-string"].must_equal "meh"
   end
 end
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 51cfcdc88c329c8b7d89d3655ad6dd377f91ce16..169118635a1cd74d1757139fd88615cd9d48b5fa 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -5,9 +5,20 @@ require 'cgi'
 require 'forwardable'
 require 'securerandom'
 
+separate_testing do
+  require_relative '../lib/rack/request'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/lint'
+end
+
 class RackRequestTest < Minitest::Spec
   it "copies the env when duping" do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+
+    if req.delegate?
+      skip "delegate requests don't dup environments"
+    end
+
     refute_same req.env, req.dup.env
   end
 
@@ -36,6 +47,20 @@ class RackRequestTest < Minitest::Spec
     assert_equal "example.com:443", req.authority
   end
 
+  it 'can calculate the server authority' do
+    req = make_request('SERVER_NAME' => 'example.com')
+    assert_equal "example.com", req.server_authority
+    req = make_request('SERVER_NAME' => 'example.com', 'SERVER_PORT' => 8080)
+    assert_equal "example.com:8080", req.server_authority
+  end
+
+  it 'can calculate the port without an authority' do
+    req = make_request('SERVER_PORT' => 8080)
+    assert_equal 8080, req.port
+    req = make_request('HTTPS' => 'on')
+    assert_equal 443, req.port
+  end
+
   it 'yields to the block if no value has been set' do
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
     yielded = false
@@ -121,11 +146,68 @@ class RackRequestTest < Minitest::Spec
     req.host.must_equal "123foo.example.com"
     req.hostname.must_equal "123foo.example.com"
 
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com")
+    req.host.must_equal "♡.com"
+    req.hostname.must_equal "♡.com"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "♡.com:80")
+    req.host.must_equal "♡.com"
+    req.hostname.must_equal "♡.com"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.谷歌")
+    req.host.must_equal "nic.谷歌"
+    req.hostname.must_equal "nic.谷歌"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "nic.谷歌:80")
+    req.host.must_equal "nic.谷歌"
+    req.hostname.must_equal "nic.谷歌"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "technically_invalid.example.com")
+    req.host.must_equal "technically_invalid.example.com"
+    req.hostname.must_equal "technically_invalid.example.com"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "technically_invalid.example.com:80")
+    req.host.must_equal "technically_invalid.example.com"
+    req.hostname.must_equal "technically_invalid.example.com"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "trailing_newline.com\n")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "really\nbad\ninput")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
     req = make_request \
       Rack::MockRequest.env_for("/", "SERVER_NAME" => "example.org", "SERVER_PORT" => "9292")
     req.host.must_equal "example.org"
     req.hostname.must_equal "example.org"
 
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.org:9292")
+    req.host.must_equal "example.org"
+
+    # Test obfuscated identifier: https://tools.ietf.org/html/rfc7239#section-6.3
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=ObFuScaTeD")
+    req.host.must_equal "ObFuScaTeD"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_FORWARDED" => "host=example.com; host=example.org:9292")
+    req.host.must_equal "example.org"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292", "HTTP_FORWARDED" => "host=example.com")
+    req.host.must_equal "example.com"
+
     req = make_request \
       Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org:9292")
     req.host.must_equal "example.org"
@@ -141,6 +223,71 @@ class RackRequestTest < Minitest::Spec
     req.host.must_equal "[2001:db8:cafe::17]"
     req.hostname.must_equal "2001:db8:cafe::17"
 
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[::]:47011")
+    req.host.must_equal "[::]"
+    req.hostname.must_equal "::"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:123.123.123.123]")
+    req.host.must_equal "[1111:2222:3333:4444:5555:6666:123.123.123.123]"
+    req.hostname.must_equal "1111:2222:3333:4444:5555:6666:123.123.123.123"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:123.123.123.123]:47011")
+    req.host.must_equal "[1111:2222:3333:4444:5555:6666:123.123.123.123]"
+    req.hostname.must_equal "1111:2222:3333:4444:5555:6666:123.123.123.123"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0.0.0")
+    req.host.must_equal "0.0.0.0"
+    req.hostname.must_equal "0.0.0.0"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0.0.0:47011")
+    req.host.must_equal "0.0.0.0"
+    req.hostname.must_equal "0.0.0.0"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.255")
+    req.host.must_equal "255.255.255.255"
+    req.hostname.must_equal "255.255.255.255"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.255:47011")
+    req.host.must_equal "255.255.255.255"
+    req.hostname.must_equal "255.255.255.255"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "really\nbad\ninput")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[0]")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[:::]")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "[1111:2222:3333:4444:5555:6666:7777:88888]")
+    req.host.must_be_nil
+    req.hostname.must_be_nil
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "0.0..0.0")
+    req.host.must_equal '0.0..0.0'
+    req.hostname.must_equal '0.0..0.0'
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "255.255.255.0255")
+    req.host.must_equal "255.255.255.0255"
+    req.hostname.must_equal "255.255.255.0255"
+
     env = Rack::MockRequest.env_for("/")
     env.delete("SERVER_NAME")
     req = make_request(env)
@@ -203,6 +350,187 @@ class RackRequestTest < Minitest::Spec
     req = make_request \
       Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_X_FORWARDED_PROTO" => "https,https", "SERVER_PORT" => "80")
     req.port.must_equal 443
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost", "HTTP_FORWARDED" => "proto=https", "HTTP_X_FORWARDED_PROTO" => "http", "SERVER_PORT" => "9393")
+    req.port.must_equal 443
+  end
+
+  it "have forwarded_* methods respect forwarded_priority" do
+    begin
+      default_priority = Rack::Request.forwarded_priority
+      default_proto_priority = Rack::Request.x_forwarded_proto_priority
+
+      def self.req(headers)
+        req = make_request Rack::MockRequest.env_for("/", headers)
+        req.singleton_class.send(:public, :forwarded_scheme)
+        req
+      end
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_FOR" => "2.3.4.5").
+        forwarded_for.must_equal ['1.2.3.4']
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4:1234",
+        "HTTP_X_FORWARDED_PORT" => "2345").
+        forwarded_port.must_equal [1234]
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_PORT" => "2345").
+        forwarded_port.must_equal []
+
+      req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6",
+        "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7").
+        forwarded_authority.must_equal '3.4.5.6'
+
+      req("HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "ws"
+
+      req("HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "http"
+
+      Rack::Request.forwarded_priority = [nil, :x_forwarded, :forwarded]
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_FOR" => "2.3.4.5").
+        forwarded_for.must_equal ['2.3.4.5']
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_PORT" => "2345").
+        forwarded_port.must_equal [2345]
+
+      req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6",
+        "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7").
+        forwarded_authority.must_equal '4.5.6.7'
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "ws"
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "http"
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_equal "https"
+
+      Rack::Request.x_forwarded_proto_priority = [nil, :scheme, :proto]
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "http"
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws").
+        forwarded_scheme.must_equal "ws"
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_equal "https"
+
+      Rack::Request.forwarded_priority = [:x_forwarded]
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "http"
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws").
+        forwarded_scheme.must_equal "ws"
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_be_nil
+
+      Rack::Request.x_forwarded_proto_priority = [:scheme]
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "http"
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_be_nil
+
+      Rack::Request.x_forwarded_proto_priority = [:proto]
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal "ws"
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_be_nil
+
+      Rack::Request.x_forwarded_proto_priority = []
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_be_nil
+
+      Rack::Request.x_forwarded_proto_priority = default_proto_priority
+      Rack::Request.forwarded_priority = [:forwarded]
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_equal 'https'
+
+      req("HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_X_FORWARDED_PROTO" => "ws").
+        forwarded_scheme.must_be_nil
+
+      Rack::Request.forwarded_priority = []
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_FOR" => "2.3.4.5").
+        forwarded_for.must_be_nil
+
+      req("HTTP_FORWARDED"=>"for=1.2.3.4",
+        "HTTP_X_FORWARDED_PORT" => "2345").
+        forwarded_port.must_be_nil
+
+      req("HTTP_FORWARDED"=>"host=1.2.3.4, host=3.4.5.6",
+        "HTTP_X_FORWARDED_HOST" => "2.3.4.5,4.5.6.7").
+        forwarded_authority.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_PROTO" => "ws",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https",
+        "HTTP_X_FORWARDED_SCHEME" => "http").
+        forwarded_scheme.must_be_nil
+
+      req("HTTP_FORWARDED"=>"proto=https").
+        forwarded_scheme.must_be_nil
+
+    ensure
+      Rack::Request.forwarded_priority = default_priority
+      Rack::Request.x_forwarded_proto_priority = default_proto_priority
+    end
   end
 
   it "figure out the correct host with port" do
@@ -237,14 +565,18 @@ class RackRequestTest < Minitest::Spec
     req = make_request \
       Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org", "SERVER_PORT" => "9393")
     req.host_with_port.must_equal "example.org"
+
+    req = make_request \
+      Rack::MockRequest.env_for("/", "HTTP_HOST" => "localhost:81", "HTTP_X_FORWARDED_HOST" => "example.org", "HTTP_FORWARDED" => "host=example.com:9292", "SERVER_PORT" => "9393")
+    req.host_with_port.must_equal "example.com:9292"
   end
 
   it "parse the query string" do
-    req = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla"))
-    req.query_string.must_equal "foo=bar&quux=bla"
-    req.GET.must_equal "foo" => "bar", "quux" => "bla"
-    req.POST.must_be :empty?
-    req.params.must_equal "foo" => "bar", "quux" => "bla"
+    request = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=bla&nothing&empty="))
+    request.query_string.must_equal "foo=bar&quux=bla&nothing&empty="
+    request.GET.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => ""
+    request.POST.must_be :empty?
+    request.params.must_equal "foo" => "bar", "quux" => "bla", "nothing" => nil, "empty" => ""
   end
 
   it "not truncate query strings containing semi-colons #543 only in POST" do
@@ -265,7 +597,7 @@ class RackRequestTest < Minitest::Spec
         @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
-    parser = Rack::QueryParser.new(c, 65536, 100)
+    parser = Rack::QueryParser.new(c, 100)
     c = Class.new(Rack::Request) do
       define_method(:query_parser) do
         parser
@@ -278,49 +610,23 @@ class RackRequestTest < Minitest::Spec
     req.params[:quux].must_equal "bla"
   end
 
-  it "use semi-colons as separators for query strings in GET" do
+  it "does not use semi-colons as separators for query strings in GET" do
     req = make_request(Rack::MockRequest.env_for("/?foo=bar&quux=b;la;wun=duh"))
     req.query_string.must_equal "foo=bar&quux=b;la;wun=duh"
-    req.GET.must_equal "foo" => "bar", "quux" => "b", "la" => nil, "wun" => "duh"
+    req.GET.must_equal "foo" => "bar", "quux" => "b;la;wun=duh"
     req.POST.must_be :empty?
-    req.params.must_equal "foo" => "bar", "quux" => "b", "la" => nil, "wun" => "duh"
-  end
-
-  it "limit the keys from the GET query string" do
-    env = Rack::MockRequest.env_for("/?foo=bar")
-
-    old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1
-    begin
-      req = make_request(env)
-      lambda { req.GET }.must_raise Rack::QueryParser::ParamsTooDeepError
-    ensure
-      Rack::Utils.key_space_limit = old
-    end
-  end
-
-  it "limit the key size per nested params hash" do
-    nested_query = Rack::MockRequest.env_for("/?foo%5Bbar%5D%5Bbaz%5D%5Bqux%5D=1")
-    plain_query  = Rack::MockRequest.env_for("/?foo_bar__baz__qux_=1")
-
-    old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 3
-    begin
-      exp = { "foo" => { "bar" => { "baz" => { "qux" => "1" } } } }
-      make_request(nested_query).GET.must_equal exp
-      lambda { make_request(plain_query).GET  }.must_raise Rack::QueryParser::ParamsTooDeepError
-    ensure
-      Rack::Utils.key_space_limit = old
-    end
+    req.params.must_equal "foo" => "bar", "quux" => "b;la;wun=duh"
   end
 
   it "limit the allowed parameter depth when parsing parameters" do
-    env = Rack::MockRequest.env_for("/?a#{'[a]' * 110}=b")
+    env = Rack::MockRequest.env_for("/?a#{'[a]' * 40}=b")
     req = make_request(env)
     lambda { req.GET }.must_raise Rack::QueryParser::ParamsTooDeepError
 
-    env = Rack::MockRequest.env_for("/?a#{'[a]' * 90}=b")
+    env = Rack::MockRequest.env_for("/?a#{'[a]' * 30}=b")
     req = make_request(env)
     params = req.GET
-    90.times { params = params['a'] }
+    30.times { params = params['a'] }
     params['a'].must_equal 'b'
 
     old, Rack::Utils.param_depth_limit = Rack::Utils.param_depth_limit, 3
@@ -358,7 +664,7 @@ class RackRequestTest < Minitest::Spec
         @params = Hash.new{|h, k| h[k.to_s] if k.is_a?(Symbol)}
       end
     end
-    parser = Rack::QueryParser.new(c, 65536, 100)
+    parser = Rack::QueryParser.new(c, 100)
     c = Class.new(Rack::Request) do
       define_method(:query_parser) do
         parser
@@ -390,12 +696,7 @@ class RackRequestTest < Minitest::Spec
       message.must_equal "invalid %-encoding (a%)"
   end
 
-  it "raise if rack.input is missing" do
-    req = make_request({})
-    lambda { req.POST }.must_raise RuntimeError
-  end
-
-  it "parse POST data when method is POST and no Content-Type given" do
+  it "parse POST data when method is POST and no content-type given" do
     req = make_request \
       Rack::MockRequest.env_for("/?foo=quux",
         "REQUEST_METHOD" => 'POST',
@@ -408,20 +709,6 @@ class RackRequestTest < Minitest::Spec
     req.params.must_equal "foo" => "bar", "quux" => "bla"
   end
 
-  it "limit the keys from the POST form data" do
-    env = Rack::MockRequest.env_for("",
-            "REQUEST_METHOD" => 'POST',
-            :input => "foo=bar&quux=bla")
-
-    old, Rack::Utils.key_space_limit = Rack::Utils.key_space_limit, 1
-    begin
-      req = make_request(env)
-      lambda { req.POST }.must_raise Rack::QueryParser::ParamsTooDeepError
-    ensure
-      Rack::Utils.key_space_limit = old
-    end
-  end
-
   it "parse POST data with explicit content type regardless of method" do
     req = make_request \
       Rack::MockRequest.env_for("/",
@@ -444,7 +731,9 @@ class RackRequestTest < Minitest::Spec
     req.media_type.must_equal 'text/plain'
     req.media_type_params['charset'].must_equal 'utf-8'
     req.content_charset.must_equal 'utf-8'
-    req.POST.must_be :empty?
+    post = req.POST
+    post.must_be_empty
+    req.POST.must_be_same_as post
     req.params.must_equal "foo" => "quux"
     req.body.read.must_equal "foo=bar&quux=bla"
   end
@@ -456,17 +745,6 @@ class RackRequestTest < Minitest::Spec
         "CONTENT_TYPE" => 'application/x-www-form-urlencoded',
         :input => "foo=bar&quux=bla")
     req.POST.must_equal "foo" => "bar", "quux" => "bla"
-    req.body.read.must_equal "foo=bar&quux=bla"
-  end
-
-  it "rewind input after parsing POST data" do
-    input = StringIO.new("foo=bar&quux=bla")
-    req = make_request \
-      Rack::MockRequest.env_for("/",
-        "CONTENT_TYPE" => 'application/x-www-form-urlencoded;foo=bar',
-        :input => input)
-    req.params.must_equal "foo" => "bar", "quux" => "bla"
-    input.read.must_equal "foo=bar&quux=bla"
   end
 
   it "safely accepts POST requests with empty body" do
@@ -493,19 +771,21 @@ class RackRequestTest < Minitest::Spec
   it "get value by key from params with #[]" do
     req = make_request \
       Rack::MockRequest.env_for("?foo=quux")
-    req['foo'].must_equal 'quux'
-    req[:foo].must_equal 'quux'
+    assert_output(nil, /deprecated/) do
+      req['foo'].must_equal 'quux'
+      req[:foo].must_equal 'quux'
+    end
 
     next if self.class == TestProxyRequest
     verbose = $VERBOSE
     warn_arg = nil
-    req.define_singleton_method(:warn) do |arg|
-      warn_arg = arg
+    req.define_singleton_method(:warn) do |*args|
+      warn_arg = args
     end
     begin
       $VERBOSE = true
       req['foo'].must_equal 'quux'
-      warn_arg.must_equal "Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead"
+      warn_arg.must_equal ["Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", { uplevel: 1 }]
     ensure
       $VERBOSE = verbose
     end
@@ -514,33 +794,43 @@ class RackRequestTest < Minitest::Spec
   it "set value to key on params with #[]=" do
     req = make_request \
       Rack::MockRequest.env_for("?foo=duh")
-    req['foo'].must_equal 'duh'
-    req[:foo].must_equal 'duh'
+    assert_output(nil, /deprecated/) do
+      req['foo'].must_equal 'duh'
+      req[:foo].must_equal 'duh'
+    end
     req.params.must_equal 'foo' => 'duh'
 
     if req.delegate?
       skip "delegate requests don't cache params, so mutations have no impact"
     end
 
-    req['foo'] = 'bar'
+    assert_output(nil, /deprecated/) do
+      req['foo'] = 'bar'
+    end
     req.params.must_equal 'foo' => 'bar'
-    req['foo'].must_equal 'bar'
-    req[:foo].must_equal 'bar'
+    assert_output(nil, /deprecated/) do
+      req['foo'].must_equal 'bar'
+      req[:foo].must_equal 'bar'
+    end
 
-    req[:foo] = 'jaz'
+    assert_output(nil, /deprecated/) do
+      req[:foo] = 'jaz'
+    end
     req.params.must_equal 'foo' => 'jaz'
-    req['foo'].must_equal 'jaz'
-    req[:foo].must_equal 'jaz'
+    assert_output(nil, /deprecated/) do
+      req['foo'].must_equal 'jaz'
+      req[:foo].must_equal 'jaz'
+    end
 
     verbose = $VERBOSE
     warn_arg = nil
-    req.define_singleton_method(:warn) do |arg|
-      warn_arg = arg
+    req.define_singleton_method(:warn) do |*args|
+      warn_arg = args
     end
     begin
       $VERBOSE = true
       req['foo'] = 'quux'
-      warn_arg.must_equal "Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead"
+      warn_arg.must_equal ["Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", { uplevel: 1 }]
       req.params['foo'].must_equal 'quux'
     ensure
       $VERBOSE = verbose
@@ -625,6 +915,25 @@ class RackRequestTest < Minitest::Spec
     request.scheme.must_equal "http"
     request.wont_be :ssl?
 
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'ws'))
+    request.scheme.must_equal "ws"
+    request.wont_be :ssl?
+
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'ws'))
+    request.scheme.must_equal "ws"
+
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https'))
+    request.scheme.must_equal "https"
+    request.must_be :ssl?
+
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=https, proto=http'))
+    request.scheme.must_equal "http"
+    request.wont_be :ssl?
+
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_FORWARDED' => 'proto=http, proto=https'))
+    request.scheme.must_equal "https"
+    request.must_be :ssl?
+
     request = make_request(Rack::MockRequest.env_for("/", 'HTTPS' => 'on'))
     request.scheme.must_equal "https"
     request.must_be :ssl?
@@ -653,12 +962,20 @@ class RackRequestTest < Minitest::Spec
     request.scheme.must_equal "https"
     request.must_be :ssl?
 
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_SCHEME' => 'wss'))
+    request.scheme.must_equal "wss"
+    request.must_be :ssl?
+
     request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'https'))
     request.scheme.must_equal "https"
     request.must_be :ssl?
 
     request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'https, http, http'))
-    request.scheme.must_equal "https"
+    request.scheme.must_equal "http"
+    request.wont_be :ssl?
+
+    request = make_request(Rack::MockRequest.env_for("/", 'HTTP_X_FORWARDED_PROTO' => 'wss'))
+    request.scheme.must_equal "wss"
     request.must_be :ssl?
   end
 
@@ -896,6 +1213,22 @@ class RackRequestTest < Minitest::Spec
       req.media_type_params['weird'].must_equal 'lol"'
   end
 
+  it "returns the same error for invalid post inputs" do
+    env = {
+      'REQUEST_METHOD' => 'POST',
+      'PATH_INFO' => '/foo',
+      'rack.input' => StringIO.new('invalid=bar&invalid[foo]=bar'),
+      'HTTP_CONTENT_TYPE' => "application/x-www-form-urlencoded",
+    }
+    
+    2.times do
+      # The actual exception type here is unimportant - just that it fails.
+      assert_raises(Rack::Utils::ParameterTypeError) do
+        Rack::Request.new(env).POST
+      end
+    end
+  end
+
   it "parse with junk before boundary" do
     # Adapted from RFC 1867.
     input = <<EOF
@@ -907,8 +1240,8 @@ content-disposition: form-data; name="reply"\r
 yes\r
 --AaB03x\r
 content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r
-Content-Type: image/jpeg\r
-Content-Transfer-Encoding: base64\r
+content-type: image/jpeg\r
+content-transfer-encoding: base64\r
 \r
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
 --AaB03x--\r
@@ -946,8 +1279,8 @@ content-disposition: form-data; name="reply"
 yes
 --AaB03x
 content-disposition: form-data; name="fileupload"; filename="dj.jpg"
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
+content-type: image/jpeg
+content-transfer-encoding: base64
 
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg
 --AaB03x--
@@ -970,8 +1303,8 @@ content-disposition: form-data; name="reply"\r
 yes\r
 --AaB03x\r
 content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r
-Content-Type: image/jpeg\r
-Content-Transfer-Encoding: base64\r
+content-type: image/jpeg\r
+content-transfer-encoding: base64\r
 \r
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
 --AaB03x--\r
@@ -1002,7 +1335,7 @@ EOF
 
   it "MultipartPartLimitError when request has too many multipart file parts if limit set" do
     begin
-      data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
+      data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
       data += "--AaB03x--\r"
 
       options = {
@@ -1034,7 +1367,7 @@ EOF
 
   it 'closes tempfiles it created in the case of too many created' do
     begin
-      data = 10000.times.map { "--AaB03x\r\nContent-Type: text/plain\r\nContent-Disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
+      data = 10000.times.map { "--AaB03x\r\ncontent-type: text/plain\r\ncontent-disposition: attachment; name=#{SecureRandom.hex(10)}; filename=#{SecureRandom.hex(10)}\r\n\r\ncontents\r\n" }.join("\r\n")
       data += "--AaB03x--\r"
 
       files = []
@@ -1084,14 +1417,14 @@ EOF
     input = <<EOF
 --AaB03x\r
 content-disposition: form-data; name="fileupload"; filename="foo.jpg"\r
-Content-Type: image/jpeg\r
-Content-Transfer-Encoding: base64\r
+content-type: image/jpeg\r
+content-transfer-encoding: base64\r
 \r
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
 --AaB03x\r
 content-disposition: form-data; name="fileupload"; filename="bar.jpg"\r
-Content-Type: image/jpeg\r
-Content-Transfer-Encoding: base64\r
+content-type: image/jpeg\r
+content-transfer-encoding: base64\r
 \r
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
 --AaB03x--\r
@@ -1161,9 +1494,9 @@ EOF
   it "correctly parse the part name from Content-Id header" do
     input = <<EOF
 --AaB03x\r
-Content-Type: text/xml; charset=utf-8\r
+content-type: text/xml; charset=utf-8\r
 Content-Id: <soap-start>\r
-Content-Transfer-Encoding: 7bit\r
+content-transfer-encoding: 7bit\r
 \r
 foo\r
 --AaB03x--\r
@@ -1201,19 +1534,23 @@ EOF
     rack_input.write(input)
     rack_input.rewind
 
-    req = make_request Rack::MockRequest.env_for("/",
-                      "rack.request.form_hash" => { 'foo' => 'bar' },
-                      "rack.request.form_input" => rack_input,
-                      :input => rack_input)
+    form_hash = {}
+
+    req = make_request Rack::MockRequest.env_for(
+      "/",
+      "rack.request.form_hash" => form_hash,
+      "rack.request.form_input" => rack_input,
+      :input => rack_input
+    )
 
-    req.POST.must_equal req.env['rack.request.form_hash']
+    req.POST.must_be_same_as form_hash
   end
 
   it "conform to the Rack spec" do
     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.dup
@@ -1223,8 +1560,8 @@ content-disposition: form-data; name="reply"\r
 yes\r
 --AaB03x\r
 content-disposition: form-data; name="fileupload"; filename="dj.jpg"\r
-Content-Type: image/jpeg\r
-Content-Transfer-Encoding: base64\r
+content-type: image/jpeg\r
+content-transfer-encoding: base64\r
 \r
 /9j/4AAQSkZJRgABAQAAAQABAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcg\r
 --AaB03x--\r
@@ -1290,7 +1627,7 @@ EOF
     res.body.must_equal 'fe80::202:b3ff:fe1e:8329'
 
     res = mock.get '/', 'REMOTE_ADDR' => '1.2.3.4,3.4.5.6'
-    res.body.must_equal '1.2.3.4'
+    res.body.must_equal '3.4.5.6'
 
     res = mock.get '/', 'REMOTE_ADDR' => '127.0.0.1'
     res.body.must_equal '127.0.0.1'
@@ -1302,6 +1639,21 @@ EOF
   it 'deals with proxies' do
     mock = Rack::MockRequest.new(Rack::Lint.new(ip_app))
 
+    res = mock.get '/',
+      'REMOTE_ADDR' => '1.2.3.4',
+      'HTTP_FORWARDED' => 'for=3.4.5.6'
+    res.body.must_equal '1.2.3.4'
+
+    res = mock.get '/',
+      'HTTP_X_FORWARDED_FOR' => '3.4.5.6',
+      'HTTP_FORWARDED' => 'for=5.6.7.8'
+    res.body.must_equal '5.6.7.8'
+
+    res = mock.get '/',
+      'HTTP_X_FORWARDED_FOR' => '3.4.5.6',
+      'HTTP_FORWARDED' => 'for=5.6.7.8, for=7.8.9.0'
+    res.body.must_equal '7.8.9.0'
+
     res = mock.get '/',
       'REMOTE_ADDR' => '1.2.3.4',
       'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
@@ -1336,6 +1688,9 @@ EOF
     res = mock.get '/', 'HTTP_X_FORWARDED_FOR' => '[2001:db8:cafe::17]:47011'
     res.body.must_equal '2001:db8:cafe::17'
 
+    res = mock.get '/', 'HTTP_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'
 
@@ -1431,14 +1786,21 @@ EOF
   it "regards local addresses as proxies" do
     req = make_request(Rack::MockRequest.env_for("/"))
     req.trusted_proxy?('127.0.0.1').must_equal true
+    req.trusted_proxy?('127.000.000.001').must_equal true
+    req.trusted_proxy?('127.0.0.6').must_equal true
+    req.trusted_proxy?('127.0.0.30').must_equal true
     req.trusted_proxy?('10.0.0.1').must_equal true
+    req.trusted_proxy?('10.000.000.001').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?('172.31.000.001').must_equal true
     req.trusted_proxy?('192.168.0.1').must_equal true
+    req.trusted_proxy?('192.168.000.001').must_equal true
     req.trusted_proxy?('::1').must_equal true
     req.trusted_proxy?('fd00::').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
@@ -1446,15 +1808,35 @@ EOF
     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?("127.256.0.1").must_equal false
+    req.trusted_proxy?("127.0.256.1").must_equal false
+    req.trusted_proxy?("127.0.0.256").must_equal false
+    req.trusted_proxy?('127.0.0.300').must_equal false
+    req.trusted_proxy?("10.256.0.1").must_equal false
+    req.trusted_proxy?("10.0.256.1").must_equal false
+    req.trusted_proxy?("10.0.0.256").must_equal false
     req.trusted_proxy?("11.0.0.1").must_equal false
+    req.trusted_proxy?("11.000.000.001").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?("172.16.256.1").must_equal false
+    req.trusted_proxy?("172.16.0.256").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
     req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
-    assert_equal Hash.new, req.session
+    session = req.session
+    assert_equal Hash.new, session
+    req.env['rack.session'].must_be_same_as session
+  end
+
+  it "sets the default session options to an empty hash" do
+    req = make_request(Rack::MockRequest.env_for("http://example.com:8080/"))
+    session_options = req.session_options
+    assert_equal Hash.new, session_options
+    req.env['rack.session.options'].must_be_same_as session_options
+    assert_equal Hash.new, req.session_options
   end
 
   class MyRequest < Rack::Request
@@ -1506,6 +1888,30 @@ EOF
     end
   }
 
+  (24..27).each do |exp|
+    length = 2**exp
+    it "handles ASCII NUL input of #{length} bytes" do
+      mr = Rack::MockRequest.env_for("/",
+        "REQUEST_METHOD" => 'POST',
+        :input => "\0"*length)
+      req = make_request mr
+      req.query_string.must_equal ""
+      req.GET.must_be :empty?
+      keys = req.POST.keys
+      keys.length.must_equal 1
+      keys.first.length.must_equal(length-1)
+      keys.first.must_equal("\0"*(length-1))
+    end
+  end
+
+  it "Env sets @env on initialization" do
+    c = Class.new do
+      include Rack::Request::Env
+    end
+    h = {}
+    c.new(h).env.must_be_same_as h
+  end
+
   class NonDelegate < Rack::Request
     def delegate?; false; end
   end
@@ -1519,7 +1925,7 @@ EOF
       include Rack::Request::Helpers
       extend Forwardable
 
-      def_delegators :@req, :has_header?, :get_header, :fetch_header,
+      def_delegators :@req, :env, :has_header?, :get_header, :fetch_header,
         :each_header, :set_header, :add_header, :delete_header
 
       def_delegators :@req, :[], :[]=, :values_at
@@ -1529,8 +1935,6 @@ EOF
       end
 
       def delegate?; true; end
-
-      def env; @req.env.dup; end
     end
 
     def make_request(env)
diff --git a/test/spec_response.rb b/test/spec_response.rb
index 1dfafcdb5ff68e01f47569a5eedbc23137a43b33..ef8aa481c11fb66bf5b141443b4c9fa293bfeb54 100644
--- a/test/spec_response.rb
+++ b/test/spec_response.rb
@@ -2,7 +2,15 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/response'
+end
+
 describe Rack::Response do
+  deprecated "#header returns headers" do
+    Rack::Response[200, { "v" => "1" }, []].header['v'].must_equal '1'
+  end
+
   it 'has standard constructor' do
     headers = { "header" => "value" }
     body = ["body"]
@@ -19,7 +27,7 @@ describe Rack::Response do
     cc = 'foo'
     response.cache_control = cc
     assert_equal cc, response.cache_control
-    assert_equal cc, response.to_a[1]['Cache-Control']
+    assert_equal cc, response.to_a[1]['cache-control']
   end
 
   it 'has an etag method' do
@@ -27,7 +35,7 @@ describe Rack::Response do
     etag = 'foo'
     response.etag = etag
     assert_equal etag, response.etag
-    assert_equal etag, response.to_a[1]['ETag']
+    assert_equal etag, response.to_a[1]['etag']
   end
 
   it 'has a content-type method' do
@@ -35,7 +43,7 @@ describe Rack::Response do
     content_type = 'foo'
     response.content_type = content_type
     assert_equal content_type, response.content_type
-    assert_equal content_type, response.to_a[1]['Content-Type']
+    assert_equal content_type, response.to_a[1]['content-type']
   end
 
   it "have sensible default values" do
@@ -43,7 +51,7 @@ describe Rack::Response do
     status, header, body = response.finish
     status.must_equal 200
     header.must_equal({})
-    body.each { |part|
+    response.each { |part|
       part.must_equal ""
     }
 
@@ -56,7 +64,7 @@ describe Rack::Response do
     }
   end
 
-  it "can be written to inside finish block, but does not update Content-Length" do
+  it "can be written to inside finish block, but does not update content-length" do
     response = Rack::Response.new('foo')
     response.write "bar"
 
@@ -68,139 +76,156 @@ describe Rack::Response do
     body.each { |part| parts << part }
 
     parts.must_equal ["foo", "bar", "baz"]
-    h['Content-Length'].must_equal '6'
+    h['content-length'].must_equal '6'
+  end
+
+  it "#write calls #<< on non-iterable body" do
+    content = []
+    body = proc{|x| content << x}
+    body.singleton_class.class_eval{alias << call}
+    response = Rack::Response.new(body)
+    response.write "bar"
+    content.must_equal ["bar"]
   end
 
   it "can set and read headers" do
     response = Rack::Response.new
-    response["Content-Type"].must_be_nil
-    response["Content-Type"] = "text/plain"
-    response["Content-Type"].must_equal "text/plain"
+    response["content-type"].must_be_nil
+    response["content-type"] = "text/plain"
+    response["content-type"].must_equal "text/plain"
   end
 
   it "doesn't mutate given headers" do
-    headers = {}
+    headers = {}.freeze
 
     response = Rack::Response.new([], 200, headers)
-    response.headers["Content-Type"] = "text/plain"
-    response.headers["Content-Type"].must_equal "text/plain"
+    response.headers["content-type"] = "text/plain"
+    response.headers["content-type"].must_equal "text/plain"
 
-    headers.wont_include("Content-Type")
+    headers.wont_include("content-type")
   end
 
-  it "can override the initial Content-Type with a different case" do
+  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"
+    response["content-type"].must_equal "text/plain"
+  end
+
+  it "can get and set set-cookie header" do
+    response = Rack::Response.new
+    response.set_cookie_header.must_be_nil
+    response.set_cookie_header = 'v=1;'
+    response.set_cookie_header.must_equal 'v=1;'
+    response.headers['set-cookie'].must_equal 'v=1;'
   end
 
   it "can set cookies" do
     response = Rack::Response.new
 
     response.set_cookie "foo", "bar"
-    response["Set-Cookie"].must_equal "foo=bar"
+    response["set-cookie"].must_equal "foo=bar"
     response.set_cookie "foo2", "bar2"
-    response["Set-Cookie"].must_equal ["foo=bar", "foo2=bar2"].join("\n")
+    response["set-cookie"].must_equal ["foo=bar", "foo2=bar2"]
     response.set_cookie "foo3", "bar3"
-    response["Set-Cookie"].must_equal ["foo=bar", "foo2=bar2", "foo3=bar3"].join("\n")
+    response["set-cookie"].must_equal ["foo=bar", "foo2=bar2", "foo3=bar3"]
   end
 
   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"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
+    response["set-cookie"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"]
   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"].must_match(
+    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"].must_equal "foo=bar; secure"
+    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"].must_equal "foo=bar; HttpOnly"
+    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"].must_equal "foo=bar; HttpOnly"
+    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"].must_equal "foo=bar"
+    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"
+    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"
+    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"
+    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"].must_equal "foo=bar; SameSite=Lax"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Lax"
   end
 
   it "can set SameSite cookies with symbol value :Lax" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: :lax }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Lax"
   end
 
   it "can set SameSite cookies with string value 'Lax'" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: "Lax" }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Lax"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Lax"
   end
 
   it "can set SameSite cookies with boolean value true" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: true }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Strict"
   end
 
   it "can set SameSite cookies with symbol value :strict" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: :strict }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Strict"
   end
 
   it "can set SameSite cookies with symbol value :Strict" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: :Strict }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Strict"
   end
 
   it "can set SameSite cookies with string value 'Strict'" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: "Strict" }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Strict"
   end
 
   it "validates the SameSite option value" do
@@ -214,14 +239,14 @@ describe Rack::Response do
   it "can set SameSite cookies with symbol value" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", same_site: :Strict }
-    response["Set-Cookie"].must_equal "foo=bar; SameSite=Strict"
+    response["set-cookie"].must_equal "foo=bar; SameSite=Strict"
   end
 
   [ nil, false ].each do |non_truthy|
     it "omits SameSite attribute given a #{non_truthy.inspect} value" do
       response = Rack::Response.new
       response.set_cookie "foo", { value: "bar", same_site: non_truthy }
-      response["Set-Cookie"].must_equal "foo=bar"
+      response["set-cookie"].must_equal "foo=bar"
     end
   end
 
@@ -230,135 +255,131 @@ describe Rack::Response do
     response.set_cookie "foo", "bar"
     response.set_cookie "foo2", "bar2"
     response.delete_cookie "foo"
-    response["Set-Cookie"].must_equal [
+    response["set-cookie"].must_equal [
+      "foo=bar",
       "foo2=bar2",
       "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"].must_equal ["foo=bar; domain=sample.example.com", "foo=bar; domain=.example.com"].join("\n")
+    response["set-cookie"].must_equal [
+      "foo=bar; domain=sample.example.com",
+      "foo=bar; domain=.example.com"
+    ]
+
     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["set-cookie"].must_equal [
+      "foo=bar; domain=sample.example.com",
+      "foo=bar; domain=.example.com",
+      "foo=; domain=.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
+
     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")
+    response["set-cookie"].must_equal [
+      "foo=bar; domain=sample.example.com",
+      "foo=bar; domain=.example.com",
+      "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"
+    ]
   end
 
   it "only deletes cookies for the domain specified" do
     response = Rack::Response.new
     response.set_cookie "foo", { value: "bar", domain: "example.com.example.com" }
     response.set_cookie "foo", { value: "bar", domain: "example.com" }
-    response["Set-Cookie"].must_equal ["foo=bar; domain=example.com.example.com", "foo=bar; domain=example.com"].join("\n")
-    response.delete_cookie "foo", domain: "example.com"
-    response["Set-Cookie"].must_equal ["foo=bar; domain=example.com.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: "example.com.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=example.com.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
+    response["set-cookie"].must_equal [
+      "foo=bar; domain=example.com.example.com",
+      "foo=bar; domain=example.com"
+    ]
+
+    response.delete_cookie "foo", { domain: "example.com" }
+    response["set-cookie"].must_equal [
+      "foo=bar; domain=example.com.example.com",
+      "foo=bar; domain=example.com",
+      "foo=; domain=example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
+
+    response.delete_cookie "foo", { domain: "example.com.example.com" }
+    response["set-cookie"].must_equal [
+      "foo=bar; domain=example.com.example.com",
+      "foo=bar; domain=example.com",
+      "foo=; domain=example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
+      "foo=; domain=example.com.example.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   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"].must_equal ["foo=bar; path=/",
-                                         "foo=bar; path=/path"].join("\n")
+
+    response["set-cookie"].must_equal [
+      "foo=bar; path=/",
+      "foo=bar; 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 GMT"].join("\n")
+    response["set-cookie"].must_equal [
+      "foo=bar; path=/",
+      "foo=bar; path=/path",
+      "foo=; path=/path; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   end
 
   it "only delete cookies with the path specified" do
     response = Rack::Response.new
-    response.set_cookie "foo", value: "bar", path: "/"
-    response.set_cookie "foo", value: "bar", path: "/a"
     response.set_cookie "foo", value: "bar", path: "/a/b"
-    response["Set-Cookie"].must_equal ["foo=bar; path=/",
-                                       "foo=bar; path=/a",
-                                       "foo=bar; path=/a/b"].join("\n")
+    response["set-cookie"].must_equal(
+      "foo=bar; path=/a/b"
+    )
 
     response.delete_cookie "foo", path: "/a"
-    response["Set-Cookie"].must_equal ["foo=bar; path=/",
-                                       "foo=bar; path=/a/b",
-                                       "foo=; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"].join("\n")
+    response["set-cookie"].must_equal [
+      "foo=bar; path=/a/b",
+      "foo=; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   end
 
   it "only delete cookies with the domain and path specified" do
     response = Rack::Response.new
-    response.set_cookie "foo", value: "bar", path: "/"
-    response.set_cookie "foo", value: "bar", path: "/a"
-    response.set_cookie "foo", value: "bar", path: "/a/b"
-    response.set_cookie "foo", value: "bar", path: "/", domain: "example.com.example.com"
-    response.set_cookie "foo", value: "bar", path: "/a", domain: "example.com.example.com"
-    response.set_cookie "foo", value: "bar", path: "/a/b", domain: "example.com.example.com"
-    response.set_cookie "foo", value: "bar", path: "/", domain: "example.com"
-    response.set_cookie "foo", value: "bar", path: "/a", domain: "example.com"
-    response.set_cookie "foo", value: "bar", path: "/a/b", domain: "example.com"
-    response["Set-Cookie"].must_equal [
-      "foo=bar; path=/",
-      "foo=bar; path=/a",
-      "foo=bar; path=/a/b",
-      "foo=bar; domain=example.com.example.com; path=/",
-      "foo=bar; domain=example.com.example.com; path=/a",
-      "foo=bar; domain=example.com.example.com; path=/a/b",
-      "foo=bar; domain=example.com; path=/",
-      "foo=bar; domain=example.com; path=/a",
-      "foo=bar; domain=example.com; path=/a/b",
-    ].join("\n")
-
     response.delete_cookie "foo", path: "/a", domain: "example.com"
-    response["Set-Cookie"].must_equal [
-      "foo=bar; path=/",
-      "foo=bar; path=/a",
-      "foo=bar; path=/a/b",
-      "foo=bar; domain=example.com.example.com; path=/",
-      "foo=bar; domain=example.com.example.com; path=/a",
-      "foo=bar; domain=example.com.example.com; path=/a/b",
-      "foo=bar; domain=example.com; path=/",
-      "foo=bar; domain=example.com; path=/a/b",
+    response["set-cookie"].must_equal(
       "foo=; domain=example.com; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
-    ].join("\n")
+    )
 
     response.delete_cookie "foo", path: "/a/b", domain: "example.com"
-    response["Set-Cookie"].must_equal [
-      "foo=bar; path=/",
-      "foo=bar; path=/a",
-      "foo=bar; path=/a/b",
-      "foo=bar; domain=example.com.example.com; path=/",
-      "foo=bar; domain=example.com.example.com; path=/a",
-      "foo=bar; domain=example.com.example.com; path=/a/b",
-      "foo=bar; domain=example.com; path=/",
+    response["set-cookie"].must_equal [
       "foo=; domain=example.com; path=/a; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
       "foo=; domain=example.com; path=/a/b; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
-    ].join("\n")
+    ]
   end
 
   it "can do redirects" do
     response = Rack::Response.new
     response.redirect "/foo"
-    status, header, body = response.finish
+    status, header = response.finish
     status.must_equal 302
-    header["Location"].must_equal "/foo"
+    header["location"].must_equal "/foo"
 
     response = Rack::Response.new
     response.redirect "/foo", 307
-    status, header, body = response.finish
+    status, = response.finish
 
     status.must_equal 307
   end
 
   it "has a useful constructor" do
     r = Rack::Response.new("foo")
-    status, header, body = r.finish
+    body = r.finish[2]
     str = "".dup; body.each { |part| str << part }
     str.must_equal "foo"
 
     r = Rack::Response.new(["foo", "bar"])
-    status, header, body = r.finish
+    body = r.finish[2]
     str = "".dup; body.each { |part| str << part }
     str.must_equal "foobar"
 
@@ -369,7 +390,7 @@ describe Rack::Response do
     end
     r = Rack::Response.new(object_with_each)
     r.write "foo"
-    status, header, body = r.finish
+    body = r.finish[2]
     str = "".dup; body.each { |part| str << part }
     str.must_equal "foobarfoo"
 
@@ -391,7 +412,7 @@ describe Rack::Response do
     status.must_equal 404
   end
 
-  it "correctly updates Content-Type when writing when not initialized with body" do
+  it "correctly updates content-type when writing when not initialized with body" do
     r = Rack::Response.new
     r.write('foo')
     r.write('bar')
@@ -399,10 +420,10 @@ describe Rack::Response do
     _, header, body = r.finish
     str = "".dup; body.each { |part| str << part }
     str.must_equal "foobarbaz"
-    header['Content-Length'].must_equal '9'
+    header['content-length'].must_equal '9'
   end
 
-  it "correctly updates Content-Type when writing when initialized with body" do
+  it "correctly updates content-type when writing when initialized with body" do
     obj = Object.new
     def obj.each
       yield 'foo'
@@ -414,7 +435,7 @@ describe Rack::Response do
       _, header, body = r.finish
       str = "".dup; body.each { |part| str << part }
       str.must_equal "foobarbaz"
-      header['Content-Length'].must_equal '9'
+      header['content-length'].must_equal '9'
     end
   end
 
@@ -423,8 +444,8 @@ describe Rack::Response do
     _, header, body = r.finish
     str = "".dup; body.each { |part| str << part }
     str.must_be :empty?
-    header["Content-Type"].must_be_nil
-    header['Content-Length'].must_be_nil
+    header["content-type"].must_be_nil
+    header['content-length'].must_be_nil
 
     lambda {
       Rack::Response.new(Object.new).each{}
@@ -503,6 +524,16 @@ describe Rack::Response do
     res.must_be :client_error?
     res.must_be :method_not_allowed?
 
+    res.status = 406
+    res.wont_be :successful?
+    res.must_be :client_error?
+    res.must_be :not_acceptable?
+
+    res.status = 408
+    res.wont_be :successful?
+    res.must_be :client_error?
+    res.must_be :request_timeout?
+
     res.status = 412
     res.wont_be :successful?
     res.must_be :client_error?
@@ -520,11 +551,11 @@ describe Rack::Response do
 
   it "provide access to the HTTP headers" do
     res = Rack::Response.new
-    res["Content-Type"] = "text/yaml; charset=UTF-8"
+    res["content-type"] = "text/yaml; charset=UTF-8"
 
-    res.must_include "Content-Type"
-    res.headers["Content-Type"].must_equal "text/yaml; charset=UTF-8"
-    res["Content-Type"].must_equal "text/yaml; charset=UTF-8"
+    res.must_include "content-type"
+    res.headers["content-type"].must_equal "text/yaml; charset=UTF-8"
+    res["content-type"].must_equal "text/yaml; charset=UTF-8"
     res.content_type.must_equal "text/yaml; charset=UTF-8"
     res.media_type.must_equal "text/yaml"
     res.media_type_params.must_equal "charset" => "UTF-8"
@@ -532,27 +563,27 @@ describe Rack::Response do
     res.location.must_be_nil
   end
 
-  it "does not add or change Content-Length when #finish()ing" do
+  it "does not add or change content-length when #finish()ing" do
     res = Rack::Response.new
     res.status = 200
     res.finish
-    res.headers["Content-Length"].must_be_nil
+    res.headers["content-length"].must_be_nil
 
     res = Rack::Response.new
     res.status = 200
-    res.headers["Content-Length"] = "10"
+    res.headers["content-length"] = "10"
     res.finish
-    res.headers["Content-Length"].must_equal "10"
+    res.headers["content-length"].must_equal "10"
   end
 
-  it "updates Content-Length when body appended to using #write" do
+  it "updates content-length when body appended to using #write" do
     res = Rack::Response.new
     res.status = 200
-    res.headers["Content-Length"].must_be_nil
+    res.headers["content-length"].must_be_nil
     res.write "Hi"
-    res.headers["Content-Length"].must_equal "2"
+    res.headers["content-length"].must_equal "2"
     res.write " there"
-    res.headers["Content-Length"].must_equal "8"
+    res.headers["content-length"].must_equal "8"
   end
 
   it "does not wrap body" do
@@ -607,10 +638,20 @@ describe Rack::Response do
 
     res.body = StringIO.new
     res.status = 205
-    _, _, b = res.finish
+    res.finish
     res.body.wont_be :closed?
   end
 
+  it "doesn't clear #body when 101 and streaming" do
+    res = Rack::Response.new
+
+    streaming_body = proc{|stream| stream.close}
+    res.body = streaming_body
+    res.status = 101
+    res.finish
+    res.body.must_equal streaming_body
+  end
+
   it "flatten doesn't cause infinite loop" do
     # https://github.com/rack/rack/issues/419
     res = Rack::Response.new("Hello World")
@@ -624,9 +665,21 @@ describe Rack::Response do
     response.cache!(1000)
     response.do_not_cache!
 
-    expect(response['Cache-Control']).must_equal "no-cache, must-revalidate"
+    expect(response['cache-control']).must_equal "no-cache, must-revalidate"
 
-    expires_header = Time.parse(response['Expires'])
+    expires_header = Time.parse(response['expires'])
+    expect(expires_header).must_be :<=, Time.now
+  end
+
+  it "should not cache content if calling cache! after do_not_cache!" do
+    response = Rack::Response.new
+
+    response.do_not_cache!
+    response.cache!(1000)
+
+    expect(response['cache-control']).must_equal "no-cache, must-revalidate"
+
+    expires_header = Time.parse(response['expires'])
     expect(expires_header).must_be :<=, Time.now
   end
 
@@ -637,77 +690,115 @@ describe Rack::Response do
     expires = Time.now + 100 # At least this far into the future
     response.cache!(duration)
 
-    expect(response['Cache-Control']).must_equal "public, max-age=120"
+    expect(response['cache-control']).must_equal "public, max-age=120"
 
-    expires_header = Time.parse(response['Expires'])
+    expires_header = Time.parse(response['expires'])
     expect(expires_header).must_be :>=, expires
   end
 end
 
 describe Rack::Response, 'headers' do
   before do
-    @response = Rack::Response.new([], 200, { 'Foo' => '1' })
+    @response = Rack::Response.new([], 200, { 'foo' => '1' })
   end
 
   it 'has_header?' do
-    lambda { @response.has_header? nil }.must_raise NoMethodError
+    lambda { @response.has_header? nil }.must_raise ArgumentError
 
-    @response.has_header?('Foo').must_equal true
     @response.has_header?('foo').must_equal true
   end
 
   it 'get_header' do
-    lambda { @response.get_header nil }.must_raise NoMethodError
+    lambda { @response.get_header nil }.must_raise ArgumentError
 
-    @response.get_header('Foo').must_equal '1'
     @response.get_header('foo').must_equal '1'
   end
 
   it 'set_header' do
-    lambda { @response.set_header nil, '1' }.must_raise NoMethodError
+    lambda { @response.set_header nil, '1' }.must_raise ArgumentError
 
-    @response.set_header('Foo', '2').must_equal '2'
-    @response.has_header?('Foo').must_equal true
-    @response.get_header('Foo').must_equal('2')
+    @response.set_header('foo', '2').must_equal '2'
+    @response.has_header?('foo').must_equal true
+    @response.get_header('foo').must_equal('2')
 
-    @response.set_header('Foo', nil).must_be_nil
-    @response.has_header?('Foo').must_equal true
-    @response.get_header('Foo').must_be_nil
+    @response.set_header('foo', nil).must_be_nil
+    @response.get_header('foo').must_be_nil
   end
 
   it 'add_header' do
-    lambda { @response.add_header nil, '1' }.must_raise NoMethodError
+    lambda { @response.add_header nil, '1' }.must_raise ArgumentError
 
     # Add a value to an existing header
-    @response.add_header('Foo', '2').must_equal '1,2'
-    @response.get_header('Foo').must_equal '1,2'
+    @response.add_header('foo', '2').must_equal ["1", "2"]
+    @response.get_header('foo').must_equal ["1", "2"]
 
     # Add nil to an existing header
-    @response.add_header('Foo', nil).must_equal '1,2'
-    @response.get_header('Foo').must_equal '1,2'
+    @response.add_header('foo', nil).must_equal ["1", "2"]
+    @response.get_header('foo').must_equal ["1", "2"]
 
     # Add nil to a nonexistent header
-    @response.add_header('Bar', nil).must_be_nil
-    @response.has_header?('Bar').must_equal false
-    @response.get_header('Bar').must_be_nil
+    @response.add_header('bar', nil).must_be_nil
+    @response.has_header?('bar').must_equal false
+    @response.get_header('bar').must_be_nil
 
     # Add a value to a nonexistent header
-    @response.add_header('Bar', '1').must_equal '1'
-    @response.has_header?('Bar').must_equal true
-    @response.get_header('Bar').must_equal '1'
+    @response.add_header('bar', '1').must_equal '1'
+    @response.has_header?('bar').must_equal true
+    @response.get_header('bar').must_equal '1'
   end
 
   it 'delete_header' do
-    lambda { @response.delete_header nil }.must_raise NoMethodError
+    lambda { @response.delete_header nil }.must_raise ArgumentError
+
+    @response.delete_header('foo').must_equal '1'
+    @response.has_header?('foo').must_equal false
 
-    @response.delete_header('Foo').must_equal '1'
-    (!!@response.has_header?('Foo')).must_equal false
+    @response.delete_header('foo').must_be_nil
+    @response.has_header?('foo').must_equal false
+
+    @response.set_header('foo', 1)
+    @response.delete_header('foo').must_equal 1
+    @response.has_header?('foo').must_equal false
+  end
+end
+
+describe Rack::Response::Raw do
+  before do
+    @response = Rack::Response::Raw.new(200, { 'foo' => '1' })
+  end
+
+  it 'has_header?' do
+    @response.has_header?('foo').must_equal true
+    @response.has_header?(nil).must_equal false
+  end
+
+  it 'get_header' do
+    @response.get_header('foo').must_equal '1'
+    @response.get_header(nil).must_be_nil
+  end
+
+  it 'set_header' do
+
+    @response.set_header('foo', '2').must_equal '2'
+    @response.has_header?('foo').must_equal true
+    @response.get_header('foo').must_equal('2')
+
+    @response.set_header(nil, '1').must_equal '1'
+    @response.get_header(nil).must_equal '1'
+
+    @response.set_header('foo', nil).must_be_nil
+    @response.get_header('foo').must_be_nil
+  end
+
+  it 'delete_header' do
+    @response.delete_header('foo').must_equal '1'
+    @response.has_header?('foo').must_equal false
 
-    @response.delete_header('Foo').must_be_nil
-    @response.has_header?('Foo').must_equal false
+    @response.delete_header('foo').must_be_nil
+    @response.has_header?('foo').must_equal false
 
-    @response.set_header('Foo', 1)
+    @response.set_header('foo', 1)
     @response.delete_header('foo').must_equal 1
-    @response.has_header?('Foo').must_equal false
+    @response.has_header?('foo').must_equal false
   end
 end
diff --git a/test/spec_rewindable_input.rb b/test/spec_rewindable_input.rb
index 4efe7dc29c1658096c1707ebbbccf435c9948d51..603afbda934d8caf1f8f3d58ddccd92fe23ff1df 100644
--- a/test/spec_rewindable_input.rb
+++ b/test/spec_rewindable_input.rb
@@ -2,6 +2,10 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/rewindable_input'
+end
+
 module RewindableTest
   extend Minitest::Spec::DSL
 
@@ -9,10 +13,6 @@ module RewindableTest
     @rio = Rack::RewindableInput.new(@io)
   end
 
-  class << self # HACK to get this running w/ as few changes as possible
-    alias_method :should, :it
-  end
-
   it "be able to handle to read()" do
     @rio.read.must_equal "hello world"
   end
@@ -40,13 +40,23 @@ module RewindableTest
   end
 
   it "rewind to the beginning when #rewind is called" do
-    @rio.read(1)
+    @rio.rewind
+    @rio.read(1).must_equal 'h'
     @rio.rewind
     @rio.read.must_equal "hello world"
   end
 
   it "be able to handle gets" do
     @rio.gets.must_equal "hello world"
+    @rio.rewind
+    @rio.gets.must_equal "hello world"
+  end
+
+  it "be able to handle size" do
+    @rio.size.must_equal "hello world".size
+    @rio.size.must_equal "hello world".size
+    @rio.rewind
+    @rio.gets.must_equal "hello world"
   end
 
   it "be able to handle each" do
@@ -55,6 +65,13 @@ module RewindableTest
       array << data
     end
     array.must_equal ["hello world"]
+
+    @rio.rewind
+    array = []
+    @rio.each do |data|
+      array << data
+    end
+    array.must_equal ["hello world"]
   end
 
   it "not buffer into a Tempfile if no data has been read yet" do
@@ -147,3 +164,12 @@ describe Rack::RewindableInput do
     include RewindableTest
   end
 end
+
+describe Rack::RewindableInput::Middleware do
+  it "wraps rack.input in RewindableInput" do
+    app = proc{|env| [200, {}, [env['rack.input'].class.to_s]]}
+    app.call('rack.input'=>StringIO.new(''))[2].must_equal ['StringIO']
+    app = Rack::RewindableInput::Middleware.new(app)
+    app.call('rack.input'=>StringIO.new(''))[2].must_equal ['Rack::RewindableInput']
+  end
+end
diff --git a/test/spec_runtime.rb b/test/spec_runtime.rb
index e4fc3f95a8c4f7e72f51be902395b61403c34ad9..5f23dce742cc7efdb5390816358ea928d67460bf 100644
--- a/test/spec_runtime.rb
+++ b/test/spec_runtime.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/runtime'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Runtime do
   def runtime_app(app, *args)
     Rack::Lint.new Rack::Runtime.new(app, *args)
@@ -11,32 +17,26 @@ describe Rack::Runtime do
     Rack::MockRequest.env_for
   end
 
-  it "works even if headers is an array" do
-    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 "sets X-Runtime is none is set" do
-    app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, "Hello, World!"] }
+  it "sets x-runtime is none is set" do
+    app = lambda { |env| [200, { 'content-type' => 'text/plain' }, "Hello, World!"] }
     response = runtime_app(app).call(request)
-    response[1]['X-Runtime'].must_match(/[\d\.]+/)
+    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!"] }
+  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!"] }
     response = runtime_app(app).call(request)
-    response[1]['X-Runtime'].must_equal "foobar"
+    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\.]+/)
+    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
@@ -47,9 +47,9 @@ describe Rack::Runtime do
 
     response = runtime.call(request)
 
-    response[1]['X-Runtime-App'].must_match(/[\d\.]+/)
-    response[1]['X-Runtime-All'].must_match(/[\d\.]+/)
+    response[1]['x-runtime-app'].must_match(/[\d\.]+/)
+    response[1]['x-runtime-all'].must_match(/[\d\.]+/)
 
-    Float(response[1]['X-Runtime-All']).must_be :>, Float(response[1]['X-Runtime-App'])
+    Float(response[1]['x-runtime-all']).must_be :>, Float(response[1]['x-runtime-app'])
   end
 end
diff --git a/test/spec_sendfile.rb b/test/spec_sendfile.rb
index 09e810e9b6cc6ca6a324fde7c71960248abf8940..5205b671cd7cc9881f38ae3659a33c4bec01555e 100644
--- a/test/spec_sendfile.rb
+++ b/test/spec_sendfile.rb
@@ -4,6 +4,12 @@ require_relative 'helper'
 require 'fileutils'
 require 'tmpdir'
 
+separate_testing do
+  require_relative '../lib/rack/sendfile'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::Sendfile do
   def sendfile_body(filename = "rack_sendfile")
     FileUtils.touch File.join(Dir.tmpdir,  filename)
@@ -13,7 +19,7 @@ describe Rack::Sendfile do
   end
 
   def simple_app(body = sendfile_body)
-    lambda { |env| [200, { 'Content-Type' => 'text/plain' }, body] }
+    lambda { |env| [200, { 'content-type' => 'text/plain' }, body] }
   end
 
   def sendfile_app(body, mappings = [])
@@ -32,87 +38,100 @@ describe Rack::Sendfile do
     end.open(path, 'wb+')
   end
 
-  it "does nothing when no X-Sendfile-Type header present" do
+  it "does nothing when no x-sendfile-type header present" do
     request do |response|
       response.must_be :ok?
       response.body.must_equal 'Hello World'
-      response.headers.wont_include 'X-Sendfile'
+      response.headers.wont_include 'x-sendfile'
     end
   end
 
-  it "does nothing and logs to rack.errors when incorrect X-Sendfile-Type header present" do
+  it "does nothing and logs to rack.errors when incorrect x-sendfile-type header present" do
     io = StringIO.new
     request 'HTTP_X_SENDFILE_TYPE' => 'X-Banana', 'rack.errors' => io do |response|
       response.must_be :ok?
       response.body.must_equal 'Hello World'
-      response.headers.wont_include 'X-Sendfile'
+      response.headers.wont_include 'x-sendfile'
 
       io.rewind
       io.read.must_equal "Unknown x-sendfile variation: 'X-Banana'.\n"
     end
   end
 
-  it "sets X-Sendfile response header and discards body" do
-    request 'HTTP_X_SENDFILE_TYPE' => 'X-Sendfile' do |response|
+  it "sets x-sendfile response header and discards body" do
+    request 'HTTP_X_SENDFILE_TYPE' => 'x-sendfile' do |response|
+      response.must_be :ok?
+      response.body.must_be :empty?
+      response.headers['content-length'].must_equal '0'
+      response.headers['x-sendfile'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
+    end
+  end
+
+  it "closes body when x-sendfile used" do
+    body = sendfile_body
+    closed = false
+    body.define_singleton_method(:close){closed = true}
+    request({'HTTP_X_SENDFILE_TYPE' => 'x-sendfile'}, body) do |response|
       response.must_be :ok?
       response.body.must_be :empty?
-      response.headers['Content-Length'].must_equal '0'
-      response.headers['X-Sendfile'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
+      response.headers['content-length'].must_equal '0'
+      response.headers['x-sendfile'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
     end
+    closed.must_equal true
   end
 
-  it "sets X-Lighttpd-Send-File response header and discards body" do
-    request 'HTTP_X_SENDFILE_TYPE' => 'X-Lighttpd-Send-File' do |response|
+  it "sets x-lighttpd-send-file response header and discards body" do
+    request 'HTTP_X_SENDFILE_TYPE' => 'x-lighttpd-send-file' do |response|
       response.must_be :ok?
       response.body.must_be :empty?
-      response.headers['Content-Length'].must_equal '0'
-      response.headers['X-Lighttpd-Send-File'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
+      response.headers['content-length'].must_equal '0'
+      response.headers['x-lighttpd-send-file'].must_equal File.join(Dir.tmpdir,  "rack_sendfile")
     end
   end
 
-  it "sets X-Accel-Redirect response header and discards body" do
+  it "sets x-accel-redirect response header and discards body" do
     headers = {
-      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+      'HTTP_X_SENDFILE_TYPE' => 'x-accel-redirect',
       'HTTP_X_ACCEL_MAPPING' => "#{Dir.tmpdir}/=/foo/bar/"
     }
     request headers 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'
+      response.headers['content-length'].must_equal '0'
+      response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
     end
   end
 
-  it "sets X-Accel-Redirect response header to percent-encoded path" do
+  it "sets x-accel-redirect response header to percent-encoded path" do
     headers = {
-      'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+      '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'
+      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|
+  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?
       response.body.must_equal 'Hello World'
-      response.headers.wont_include 'X-Accel-Redirect'
-      response.errors.must_include 'X-Accel-Mapping'
+      response.headers.wont_include 'x-accel-redirect'
+      response.errors.must_include 'x-accel-mapping'
     end
   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'
+      response.headers.wont_include 'x-sendfile'
     end
   end
 
-  it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings" do
+  it "sets x-accel-redirect response header and discards body when initialized with multiple mappings" do
     begin
       dir1 = Dir.mktmpdir
       dir2 = Dir.mktmpdir
@@ -128,18 +147,18 @@ 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'
+        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({ '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'
+        response.headers['content-length'].must_equal '0'
+        response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
       end
     ensure
       FileUtils.remove_entry_secure dir1
@@ -147,7 +166,7 @@ describe Rack::Sendfile do
     end
   end
 
-  it "sets X-Accel-Redirect response header and discards body when initialized with multiple mappings via header" do
+  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
@@ -163,29 +182,29 @@ describe Rack::Sendfile do
       third_body.puts 'hello again world'
 
       headers = {
-        'HTTP_X_SENDFILE_TYPE' => 'X-Accel-Redirect',
+        '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'
+        response.headers['content-length'].must_equal '0'
+        response.headers['x-accel-redirect'].must_equal '/foo/bar/rack_sendfile'
       end
 
       request(headers, second_body) do |response|
         response.must_be :ok?
         response.body.must_be :empty?
-        response.headers['Content-Length'].must_equal '0'
-        response.headers['X-Accel-Redirect'].must_equal '/wibble/rack_sendfile'
+        response.headers['content-length'].must_equal '0'
+        response.headers['x-accel-redirect'].must_equal '/wibble/rack_sendfile'
       end
 
       request(headers, third_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 "#{dir3}/rack_sendfile"
+        response.headers['content-length'].must_equal '0'
+        response.headers['x-accel-redirect'].must_equal "#{dir3}/rack_sendfile"
       end
     ensure
       FileUtils.remove_entry_secure dir1
diff --git a/test/spec_server.rb b/test/spec_server.rb
deleted file mode 100644
index 34912b4d8e2f8f473b6a56851b9e38e5307ff753..0000000000000000000000000000000000000000
--- a/test/spec_server.rb
+++ /dev/null
@@ -1,471 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'tempfile'
-require 'socket'
-require 'webrick'
-require 'open-uri'
-require 'net/http'
-require 'net/https'
-
-module Minitest::Spec::DSL
-  alias :should :it
-end
-
-describe Rack::Server do
-  SPEC_ARGV = []
-
-  before { SPEC_ARGV[0..-1] = [] }
-
-  def app
-    lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['success']] }
-  end
-
-  def with_stderr
-    old, $stderr = $stderr, StringIO.new
-    yield $stderr
-  ensure
-    $stderr = old
-  end
-
-  it "overrides :config if :app is passed in" do
-    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.app.class.must_equal Proc
-    Rack::MockRequest.new(server.app).get("/").body.to_s.must_equal 'success'
-  end
-
-  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 []
-  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 []
-  end
-
-  it "only provide default middleware for development and deployment environments" do
-    Rack::Server.default_middleware_by_environment.keys.sort.must_equal %w(deployment development)
-  end
-
-  it "always return an empty array for unknown environments" do
-    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.middleware['deployment'].flatten.wont_include Rack::Lint
-  end
-
-  it "not include Rack::ShowExceptions in deployment environment" do
-    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.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.server.name =~ /CGI/
-      Rack::Server.logging_middleware.call(server).must_be_nil
-    ensure
-      ENV['REQUEST_METHOD'] = o
-    end
-  end
-
-  it "be quiet if said so" do
-    server = Rack::Server.new(app: "FOO", quiet: true)
-    Rack::Server.logging_middleware.call(server).must_be_nil
-  end
-
-  it "use a full path to the pidfile" do
-    # avoids issues with daemonize chdir
-    opts = Rack::Server.new.send(:parse_options, %w[--pid testing.pid])
-    opts[:pid].must_equal ::File.expand_path('testing.pid')
-  end
-
-  it "get options from ARGV" do
-    SPEC_ARGV[0..-1] = ['--debug', '-sthin', '--env', 'production', '-w', '-q', '-o', '127.0.0.1', '-O', 'NAME=VALUE', '-ONAME2', '-D']
-    server = Rack::Server.new
-    server.options[:debug].must_equal true
-    server.options[:server].must_equal 'thin'
-    server.options[:environment].must_equal 'production'
-    server.options[:warn].must_equal true
-    server.options[:quiet].must_equal true
-    server.options[:Host].must_equal '127.0.0.1'
-    server.options[:NAME].must_equal 'VALUE'
-    server.options[:NAME2].must_equal true
-    server.options[:daemonize].must_equal true
-  end
-
-  it "only override non-passed options from parsed .ru file" do
-    builder_file = File.join(File.dirname(__FILE__), 'builder', 'options.ru')
-    SPEC_ARGV[0..-1] = ['--debug', '-sthin', '--env', 'production', builder_file]
-    server = Rack::Server.new
-    server.app # force .ru file to be parsed
-
-    server.options[:debug].must_equal true
-    server.options[:server].must_equal 'thin'
-    server.options[:environment].must_equal 'production'
-    server.options[:Port].must_equal '2929'
-  end
-
-  def test_options_server(*args)
-    SPEC_ARGV[0..-1] = args
-    output = String.new
-    server = Class.new(Rack::Server) do
-      define_method(:opt_parser) do
-        Class.new(Rack::Server::Options) do
-          define_method(:puts) do |*args|
-            output << args.join("\n") << "\n"
-          end
-          alias warn puts
-          alias abort puts
-          define_method(:exit) do
-            output << "exited"
-          end
-        end.new
-      end
-    end.new
-    output
-  end
-
-  it "support -h option to get help" do
-    test_options_server('-scgi', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*exited\z/m)
-  end
-
-  it "support -h option to get handler-specific help" do
-    cgi = Rack::Handler.get('cgi')
-    begin
-      def cgi.valid_options; { "FOO=BAR" => "BAZ" } end
-      test_options_server('-scgi', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*Server-specific options for Rack::Handler::CGI.*-O +FOO=BAR +BAZ.*exited\z/m)
-    ensure
-      cgi.singleton_class.send(:remove_method, :valid_options)
-    end
-  end
-
-  it "support -h option to display warning for invalid handler" do
-    test_options_server('-sbanana', '-h').must_match(/\AUsage: rackup.*Ruby options:.*Rack options.*Profiling options.*Common options.*Warning: Could not find handler specified \(banana\) to determine handler-specific options.*exited\z/m)
-  end
-
-  it "support -v option to get version" do
-    test_options_server('-v').must_match(/\ARack \d\.\d \(Release: \d+\.\d+\.\d+(\.\d+)?\)\nexited\z/)
-  end
-
-  it "warn for invalid --profile-mode option" do
-    test_options_server('--profile-mode', 'foo').must_match(/\Ainvalid option: --profile-mode unknown profile mode: foo.*Usage: rackup/m)
-  end
-
-  it "warn for invalid options" do
-    test_options_server('--banana').must_match(/\Ainvalid option: --banana.*Usage: rackup/m)
-  end
-
-  it "support -b option to specify inline rackup config" do
-    SPEC_ARGV[0..-1] = ['-scgi', '-E', 'development', '-b', 'use Rack::ContentLength; run ->(env){[200, {}, []]}']
-    server = Rack::Server.new
-    def (server.server).run(app, **) app end
-    s, h, b = server.start.call('rack.errors' => StringIO.new)
-    s.must_equal 500
-    h['Content-Type'].must_equal 'text/plain'
-    b.join.must_include 'Rack::Lint::LintError'
-  end
-
-  it "support -e option to evaluate ruby code" do
-    SPEC_ARGV[0..-1] = ['-scgi', '-e', 'Object::XYZ = 2']
-    begin
-      server = Rack::Server.new
-      Object::XYZ.must_equal 2
-    ensure
-      Object.send(:remove_const, :XYZ)
-    end
-  end
-
-  it "abort if config file does not exist" do
-    SPEC_ARGV[0..-1] = ['-scgi']
-    server = Rack::Server.new
-    def server.abort(s) throw :abort, s end
-    message = catch(:abort) do
-      server.start
-    end
-    message.must_match(/\Aconfiguration .*config\.ru not found/)
-  end
-
-  it "support -I option to change the load path and -r to require" do
-    SPEC_ARGV[0..-1] = ['-scgi', '-Ifoo/bar', '-Itest/load', '-rrack-test-a', '-rrack-test-b']
-    begin
-      server = Rack::Server.new
-      def (server.server).run(*) end
-      def server.handle_profiling(*) end
-      def server.app(*) end
-      server.start
-      $LOAD_PATH.must_include('foo/bar')
-      $LOAD_PATH.must_include('test/load')
-      $LOADED_FEATURES.must_include(File.join(Dir.pwd, "test/load/rack-test-a.rb"))
-      $LOADED_FEATURES.must_include(File.join(Dir.pwd, "test/load/rack-test-b.rb"))
-    ensure
-      $LOAD_PATH.delete('foo/bar')
-      $LOAD_PATH.delete('test/load')
-      $LOADED_FEATURES.delete(File.join(Dir.pwd, "test/load/rack-test-a.rb"))
-      $LOADED_FEATURES.delete(File.join(Dir.pwd, "test/load/rack-test-b.rb"))
-    end
-  end
-
-  it "support -w option to warn and -d option to debug" do
-    SPEC_ARGV[0..-1] = ['-scgi', '-d', '-w']
-    warn = $-w
-    debug = $DEBUG
-    begin
-      server = Rack::Server.new
-      def (server.server).run(*) end
-      def server.handle_profiling(*) end
-      def server.app(*) end
-      def server.p(*) end
-      def server.pp(*) end
-      def server.require(*) end
-      server.start
-      $-w.must_equal true
-      $DEBUG.must_equal true
-    ensure
-      $-w = warn
-      $DEBUG = debug
-    end
-  end
-
-  if RUBY_ENGINE == "ruby"
-    it "support --heap option for heap profiling" do
-      begin
-        require 'objspace'
-      rescue LoadError
-      else
-        t = Tempfile.new
-        begin
-          SPEC_ARGV[0..-1] = ['-scgi', '--heap', t.path, '-E', 'production', '-b', 'run ->(env){[200, {}, []]}']
-          server = Rack::Server.new
-          def (server.server).run(*) end
-          def server.exit; throw :exit end
-          catch :exit do
-            server.start
-          end
-          File.file?(t.path).must_equal true
-        ensure
-          File.delete t.path
-        end
-      end
-    end
-
-    it "support --profile-mode option for stackprof profiling" do
-      begin
-        require 'stackprof'
-      rescue LoadError
-      else
-        t = Tempfile.new
-        begin
-          SPEC_ARGV[0..-1] = ['-scgi', '--profile', t.path, '--profile-mode', 'cpu', '-E', 'production', '-b', 'run ->(env){[200, {}, []]}']
-          server = Rack::Server.new
-          def (server.server).run(*) end
-          def server.puts(*) end
-          def server.exit; throw :exit end
-          catch :exit do
-            server.start
-          end
-          File.file?(t.path).must_equal true
-        ensure
-          File.delete t.path
-        end
-      end
-    end
-
-    it "support --profile-mode option for stackprof profiling without --profile option" do
-      begin
-        require 'stackprof'
-      rescue LoadError
-      else
-        begin
-          SPEC_ARGV[0..-1] = ['-scgi', '--profile-mode', 'cpu', '-E', 'production', '-b', 'run ->(env){[200, {}, []]}']
-          server = Rack::Server.new
-          def (server.server).run(*) end
-          filename = nil
-          server.define_singleton_method(:make_profile_name) do |fname, &block|
-            super(fname) do |fn|
-              filename = fn
-              block.call(filename)
-            end
-          end
-          def server.puts(*) end
-          def server.exit; throw :exit end
-          catch :exit do
-            server.start
-          end
-          File.file?(filename).must_equal true
-        ensure
-          File.delete filename
-        end
-      end
-    end
-  end
-
-  it "support exit for INT signal when server does not respond to shutdown" do
-    SPEC_ARGV[0..-1] = ['-scgi']
-    server = Rack::Server.new
-    def (server.server).run(*) end
-    def server.handle_profiling(*) end
-    def server.app(*) end
-    exited = false
-    server.define_singleton_method(:exit) do
-      exited = true
-    end
-    server.start
-    exited.must_equal false
-    Process.kill(:INT, $$)
-    sleep 1 unless RUBY_ENGINE == 'ruby'
-    exited.must_equal true
-  end
-
-  it "support support Server.start for starting" do
-    SPEC_ARGV[0..-1] = ['-scgi']
-    c = Class.new(Rack::Server) do
-      def start(*) [self.class, :started] end
-    end
-    c.start.must_equal [c, :started]
-  end
-
-
-  it "run a 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'
-    )
-    t = Thread.new { server.start { |s| Thread.current[:server] = s } }
-    t.join(0.01) until t[:server] && t[:server].status != :Stop
-    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, $$)
-    t.join
-    open(pidfile.path) { |f| f.read.must_equal $$.to_s }
-  end
-
-  it "run a secure 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',
-      SSLEnable: true,
-      SSLCertName: [['CN', 'nobody'], ['DC', 'example']]
-    )
-    t = Thread.new { server.start { |s| Thread.current[:server] = s } }
-    t.join(0.01) until t[:server] && t[:server].status != :Stop
-
-    uri = URI.parse("https://127.0.0.1:#{server.options[:Port]}/")
-
-    Net::HTTP.start("127.0.0.1", uri.port, use_ssl: true,
-      verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
-
-      request = Net::HTTP::Get.new uri
-
-      body = http.request(request).body
-      body.must_equal 'success'
-    end
-
-    Process.kill(:INT, $$)
-    t.join
-    open(pidfile.path) { |f| f.read.must_equal $$.to_s }
-  end if RUBY_VERSION >= "2.6"
-
-  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.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.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.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.send(:pidfile_process_status).must_equal :not_owned
-  end
-
-  it "rewrite pid file when it does not reference a running process" do
-    pidfile = Tempfile.open('pidfile') { |f| break f }.path
-    server = Rack::Server.new(pid: pidfile)
-    ::File.open(pidfile, 'w') { }
-    server.send(:write_pid)
-    ::File.read(pidfile).to_i.must_equal $$
-  end
-
-  it "not write pid file when it references a running process" do
-    pidfile = Tempfile.open('pidfile') { |f| break f }.path
-    ::File.delete(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
-      err.rewind
-      output = err.read
-      output.must_match(/already running/)
-      output.must_include pidfile
-    end
-  end
-
-  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)
-    with_stderr do |err|
-      lambda { server.start }.must_raise SystemExit
-      err.rewind
-      output = err.read
-      output.must_match(/already running/)
-      output.must_include pidfile
-    end
-  end
-
-end
diff --git a/test/spec_session_abstract_id.rb b/test/spec_session_abstract_id.rb
deleted file mode 100644
index 17cdb3e55cd91855ee980f112eb2f53e3167cb06..0000000000000000000000000000000000000000
--- a/test/spec_session_abstract_id.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-### WARNING: there be hax in this file.
-
-require 'rack/session/abstract/id'
-
-describe Rack::Session::Abstract::ID do
-  attr_reader :id
-
-  def setup
-    super
-    @id = Rack::Session::Abstract::ID
-  end
-
-  it "use securerandom" do
-    assert_equal ::SecureRandom, id::DEFAULT_OPTIONS[:secure_random]
-
-    id = @id.new nil
-    assert_equal ::SecureRandom, id.sid_secure
-  end
-
-  it "allow to use another securerandom provider" do
-    secure_random = Class.new do
-      def hex(*args)
-        'fake_hex'
-      end
-    end
-    id = Rack::Session::Abstract::ID.new nil, secure_random: secure_random.new
-    id.send(:generate_sid).must_equal 'fake_hex'
-  end
-
-  it "should warn when subclassing" do
-    verbose = $VERBOSE
-    begin
-      $VERBOSE = true
-      warn_arg = nil
-      @id.define_singleton_method(:warn) do |arg|
-        warn_arg = arg
-      end
-      c = Class.new(@id)
-      regexp = /is inheriting from Rack::Session::Abstract::ID.  Inheriting from Rack::Session::Abstract::ID is deprecated, please inherit from Rack::Session::Abstract::Persisted instead/
-      warn_arg.must_match(regexp)
-
-      warn_arg = nil
-      c = Class.new(c)
-      warn_arg.must_be_nil
-    ensure
-      $VERBOSE = verbose
-      @id.singleton_class.send(:remove_method, :warn)
-    end
-  end
-
-  it "#find_session should find session in request" do
-    id = @id.new(nil)
-    def id.get_session(env, sid)
-      [env['rack.session'], generate_sid]
-    end
-    req = Rack::Request.new('rack.session' => {})
-    session, sid = id.find_session(req, nil)
-    session.must_equal({})
-    sid.must_match(/\A\h+\z/)
-  end
-
-  it "#write_session should write session to request" do
-    id = @id.new(nil)
-    def id.set_session(env, sid, session, options)
-      [env, sid, session, options]
-    end
-    req = Rack::Request.new({})
-    id.write_session(req, 1, 2, 3).must_equal [{}, 1, 2, 3]
-  end
-
-  it "#delete_session should remove session from request" do
-    id = @id.new(nil)
-    def id.destroy_session(env, sid, options)
-      [env, sid, options]
-    end
-    req = Rack::Request.new({})
-    id.delete_session(req, 1, 2).must_equal [{}, 1, 2]
-  end
-end
diff --git a/test/spec_session_abstract_persisted.rb b/test/spec_session_abstract_persisted.rb
deleted file mode 100644
index 84ddf0728f115268665ba218f556622294b285c2..0000000000000000000000000000000000000000
--- a/test/spec_session_abstract_persisted.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'rack/session/abstract/id'
-
-describe Rack::Session::Abstract::Persisted do
-  def setup
-    @class = Rack::Session::Abstract::Persisted
-    @pers = @class.new(nil)
-  end
-
-  it "#generated_sid generates a session identifier" do
-    @pers.send(:generate_sid).must_match(/\A\h+\z/)
-    @pers.send(:generate_sid, nil).must_match(/\A\h+\z/)
-
-    obj = Object.new
-    def obj.hex(_); raise NotImplementedError end
-    @pers.send(:generate_sid, obj).must_match(/\A\h+\z/)
-  end
-
-  it "#commit_session? returns false if :skip option is given" do
-    @pers.send(:commit_session?, Rack::Request.new({}), {}, skip: true).must_equal false
-  end
-
-  it "#commit_session writes to rack.errors if session cannot be written" do
-    @pers = @class.new(nil)
-    def @pers.write_session(*) end
-    errors = StringIO.new
-    env = { 'rack.errors' => errors }
-    req = Rack::Request.new(env)
-    store = Class.new do
-      def load_session(req)
-        ["id", {}]
-      end
-      def session_exists?(req)
-        true
-      end
-    end
-    session = env['rack.session'] = Rack::Session::Abstract::SessionHash.new(store.new, req)
-    session['foo'] = 'bar'
-    @pers.send(:commit_session, req, Rack::Response.new)
-    errors.rewind
-    errors.read.must_equal "Warning! Rack::Session::Abstract::Persisted failed to save session. Content dropped.\n"
-  end
-
-  it "#cookie_value returns its argument" do
-    obj = Object.new
-    @pers.send(:cookie_value, obj).must_equal(obj)
-  end
-
-  it "#session_class returns the default session class" do
-    @pers.send(:session_class).must_equal Rack::Session::Abstract::SessionHash
-  end
-
-  it "#find_session raises" do
-    proc { @pers.send(:find_session, nil, nil) }.must_raise RuntimeError
-  end
-
-  it "#write_session raises" do
-    proc { @pers.send(:write_session, nil, nil, nil, nil) }.must_raise RuntimeError
-  end
-
-  it "#delete_session raises" do
-    proc { @pers.send(:delete_session, nil, nil, nil) }.must_raise RuntimeError
-  end
-end
diff --git a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb b/test/spec_session_abstract_persisted_secure_secure_session_hash.rb
deleted file mode 100644
index 1a007eb4ae1831dd2a1bc47c6d287c58901f5333..0000000000000000000000000000000000000000
--- a/test/spec_session_abstract_persisted_secure_secure_session_hash.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-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(:unknown) { :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_abstract_session_hash.rb b/test/spec_session_abstract_session_hash.rb
deleted file mode 100644
index ac0b7bb3afed06b477ca35c78817610ad687a517..0000000000000000000000000000000000000000
--- a/test/spec_session_abstract_session_hash.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'rack/session/abstract/id'
-
-describe Rack::Session::Abstract::SessionHash do
-  attr_reader :hash
-
-  def setup
-    super
-    store = Class.new do
-      def load_session(req)
-        ["id", { foo: :bar, baz: :qux, x: { y: 1 } }]
-      end
-      def session_exists?(req)
-        true
-      end
-    end
-    @class = Rack::Session::Abstract::SessionHash
-    @hash = @class.new(store.new, nil)
-  end
-
-  it ".find finds entry in request" do
-    assert_equal({}, @class.find(Rack::Request.new('rack.session' => {})))
-  end
-
-  it ".set sets session in request" do
-    req = Rack::Request.new({})
-    @class.set(req, {})
-    req.env['rack.session'].must_equal({})
-  end
-
-  it ".set_options sets session options in request" do
-    req = Rack::Request.new({})
-    h = {}
-    @class.set_options(req, h)
-    opts = req.env['rack.session.options']
-    opts.must_equal(h)
-    opts.wont_be_same_as(h)
-  end
-
-  it "#keys returns keys" do
-    assert_equal ["foo", "baz", "x"], hash.keys
-  end
-
-  it "#values returns values" do
-    assert_equal [:bar, :qux, { y: 1 }], hash.values
-  end
-
-  it "#dig operates like Hash#dig" do
-    assert_equal({ y: 1 }, hash.dig("x"))
-    assert_equal(1, hash.dig(:x, :y))
-    assert_nil(hash.dig(:z))
-    assert_nil(hash.dig(:x, :z))
-    lambda { hash.dig(:x, :y, :z) }.must_raise TypeError
-    lambda { hash.dig }.must_raise ArgumentError
-  end
-
-  it "#each iterates over entries" do
-    a = []
-    @hash.each do |k, v|
-      a << [k, v]
-    end
-    a.must_equal [["foo", :bar], ["baz", :qux], ["x", { y: 1 }]]
-  end
-
-  it "#has_key returns whether the key is in the hash" do
-    assert_equal true, hash.has_key?("foo")
-    assert_equal true, hash.has_key?(:foo)
-    assert_equal false, hash.has_key?("food")
-    assert_equal false, hash.has_key?(:food)
-  end
-
-  it "#replace replaces hash" do
-    hash.replace({ bar: "foo" })
-    assert_equal "foo", hash["bar"]
-  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(:unknown) { :default }
-    end
-
-    it "it raises when fetching unknown keys without defaults" do
-      lambda { hash.fetch(:unknown) }.must_raise KeyError
-    end
-  end
-
-  it "#stringify_keys returns hash or session hash with keys stringified" do
-    assert_equal({ "foo" => :bar, "baz" => :qux, "x" => { y: 1 } }, hash.send(:stringify_keys, hash).to_h)
-  end
-end
diff --git a/test/spec_session_cookie.rb b/test/spec_session_cookie.rb
deleted file mode 100644
index ce85ba321232a9ce2c7567acf7887d434612c2b9..0000000000000000000000000000000000000000
--- a/test/spec_session_cookie.rb
+++ /dev/null
@@ -1,498 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-describe Rack::Session::Cookie do
-  incrementor = lambda do |env|
-    env["rack.session"]["counter"] ||= 0
-    env["rack.session"]["counter"] += 1
-    hash = env["rack.session"].dup
-    hash.delete("session_id")
-    Rack::Response.new(hash.inspect).to_a
-  end
-
-  session_id = lambda do |env|
-    Rack::Response.new(env["rack.session"].to_hash.inspect).to_a
-  end
-
-  session_option = lambda do |opt|
-    lambda do |env|
-      Rack::Response.new(env["rack.session.options"][opt].inspect).to_a
-    end
-  end
-
-  nothing = lambda do |env|
-    Rack::Response.new("Nothing").to_a
-  end
-
-  renewer = lambda do |env|
-    env["rack.session.options"][:renew] = true
-    Rack::Response.new("Nothing").to_a
-  end
-
-  only_session_id = lambda do |env|
-    Rack::Response.new(env["rack.session"]["session_id"].to_s).to_a
-  end
-
-  bigcookie = lambda do |env|
-    env["rack.session"]["cookie"] = "big" * 3000
-    Rack::Response.new(env["rack.session"].inspect).to_a
-  end
-
-  destroy_session = lambda do |env|
-    env["rack.session"].destroy
-    Rack::Response.new("Nothing").to_a
-  end
-
-  def response_for(options = {})
-    request_options = options.fetch(:request, {})
-    cookie = if options[:cookie].is_a?(Rack::Response)
-      options[:cookie]["Set-Cookie"]
-    else
-      options[:cookie]
-    end
-    request_options["HTTP_COOKIE"] = cookie || ""
-
-    app_with_cookie = Rack::Session::Cookie.new(*options[:app])
-    app_with_cookie = Rack::Lint.new(app_with_cookie)
-    Rack::MockRequest.new(app_with_cookie).get("/", request_options)
-  end
-
-  before do
-    @warnings = warnings = []
-    Rack::Session::Cookie.class_eval do
-      define_method(:warn) { |m| warnings << m }
-    end
-  end
-
-  after do
-    Rack::Session::Cookie.class_eval { remove_method :warn }
-  end
-
-  describe 'Base64' do
-    it 'uses base64 to encode' do
-      coder = Rack::Session::Cookie::Base64.new
-      str   = 'fuuuuu'
-      coder.encode(str).must_equal [str].pack('m0')
-    end
-
-    it 'uses base64 to decode' do
-      coder = Rack::Session::Cookie::Base64.new
-      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('m0')
-      end
-
-      it 'marshals and base64 decodes' do
-        coder = Rack::Session::Cookie::Base64::Marshal.new
-        str   = [::Marshal.dump('fuuuuu')].pack('m0')
-        coder.decode(str).must_equal ::Marshal.load(str.unpack('m0').first)
-      end
-
-      it 'rescues failures on decode' do
-        coder = Rack::Session::Cookie::Base64::Marshal.new
-        coder.decode('lulz').must_be_nil
-      end
-    end
-
-    describe 'JSON' 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('m0')
-      end
-
-      it 'JSON and base64 decodes' do
-        coder = Rack::Session::Cookie::Base64::JSON.new
-        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
-        coder = Rack::Session::Cookie::Base64::JSON.new
-        coder.decode('lulz').must_be_nil
-      end
-    end
-
-    describe 'ZipJSON' do
-      it 'jsons, deflates, and base64 encodes' 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('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('m0')
-        coder.decode(b64).must_equal obj
-      end
-
-      it 'rescues failures on decode' do
-        coder = Rack::Session::Cookie::Base64::ZipJSON.new
-        coder.decode('lulz').must_be_nil
-      end
-    end
-  end
-
-  it "warns if no secret is given" do
-    Rack::Session::Cookie.new(incrementor)
-    @warnings.first.must_match(/no secret/i)
-    @warnings.clear
-    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)
-    @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)
-    @warnings.first.must_match(/no secret/i)
-  end
-
-  it 'uses a coder' do
-    identity = Class.new {
-      attr_reader :calls
-
-      def initialize
-        @calls = []
-      end
-
-      def encode(str); @calls << :encode; str; end
-      def decode(str); @calls << :decode; str; end
-    }.new
-    response = response_for(app: [incrementor, { coder: identity }])
-
-    response["Set-Cookie"].must_include "rack.session="
-    response.body.must_equal '{"counter"=>1}'
-    identity.calls.must_equal [:decode, :encode]
-  end
-
-  it "creates a new cookie" do
-    response = response_for(app: incrementor)
-    response["Set-Cookie"].must_include "rack.session="
-    response.body.must_equal '{"counter"=>1}'
-  end
-
-  it "passes through same_site option to session cookie" do
-    response = response_for(app: [incrementor, same_site: :none])
-    response["Set-Cookie"].must_include "SameSite=None"
-  end
-
-  it "allows using a lambda to specify same_site option, because some browsers require different settings" do
-    # Details of why this might need to be set dynamically:
-    # https://www.chromium.org/updates/same-site/incompatible-clients
-    # https://gist.github.com/bnorton/7dee72023787f367c48b3f5c2d71540f
-
-    response = response_for(app: [incrementor, same_site: lambda { |req, res| :none }])
-    response["Set-Cookie"].must_include "SameSite=None"
-
-    response = response_for(app: [incrementor, same_site: lambda { |req, res| :lax }])
-    response["Set-Cookie"].must_include "SameSite=Lax"
-  end
-
-  it "loads from a cookie" do
-    response = response_for(app: incrementor)
-
-    response = response_for(app: incrementor, cookie: response)
-    response.body.must_equal '{"counter"=>2}'
-
-    response = response_for(app: incrementor, cookie: response)
-    response.body.must_equal '{"counter"=>3}'
-  end
-
-  it "renew session id" do
-    response = response_for(app: incrementor)
-    cookie   = response['Set-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)
-    cookie   = response['Set-Cookie'] if response['Set-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.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.body.wont_equal ""
-    response.body.wont_equal old_session_id
-  end
-
-  it "survives broken cookies" do
-    response = response_for(
-      app: incrementor,
-      cookie: "rack.session=blarghfasel"
-    )
-    response.body.must_equal '{"counter"=>1}'
-
-    response = response_for(
-      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 })
-    }.must_raise Rack::MockRequest::FatalWarning
-  end
-
-  it "loads from a cookie with integrity hash" do
-    app = [incrementor, { secret: "test" }]
-
-    response = response_for(app: app)
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>2}'
-
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>3}'
-
-    app = [incrementor, { secret: "other" }]
-
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>1}'
-  end
-
-  it "loads from a cookie with accept-only integrity hash for graceful key rotation" do
-    response = response_for(app: [incrementor, { secret: "test" }])
-
-    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)
-    response.body.must_equal '{"counter"=>3}'
-  end
-
-  it "ignores tampered with session cookies" do
-    app = [incrementor, { secret: "test" }]
-    response = response_for(app: app)
-    response.body.must_equal '{"counter"=>1}'
-
-    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.body.must_equal '{"counter"=>1}'
-  end
-
-  it "supports either of secret or old_secret" do
-    app = [incrementor, { secret: "test" }]
-    response = response_for(app: app)
-    response.body.must_equal '{"counter"=>1}'
-
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>2}'
-
-    app = [incrementor, { old_secret: "test" }]
-    response = response_for(app: app)
-    response.body.must_equal '{"counter"=>1}'
-
-    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 }]
-
-    response = response_for(app: app)
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>2}'
-
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>3}'
-
-    app = [incrementor, { secret: "other" }]
-
-    response = response_for(app: app, cookie: response)
-    response.body.must_equal '{"counter"=>1}'
-  end
-
-  it "can handle Rack::Lint middleware" do
-    response = response_for(app: incrementor)
-
-    lint = Rack::Lint.new(session_id)
-    response = response_for(app: lint, cookie: response)
-    response.body.wont_be :nil?
-  end
-
-  it "can handle middleware that inspects the env" do
-    class TestEnvInspector
-      def initialize(app)
-        @app = app
-      end
-      def call(env)
-        env.inspect
-        @app.call(env)
-      end
-    end
-
-    response = response_for(app: incrementor)
-
-    inspector = TestEnvInspector.new(session_id)
-    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.body.must_equal '{"counter"=>1}'
-
-    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 }]
-
-    response = response_for(app: app)
-    response["Set-Cookie"].must_be_nil
-
-    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["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["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)
-    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)
-    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.body.must_equal '"foo"'
-  end
-
-  it "exposes :coder in env['rack.session.option']" do
-    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)
-    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)
-    response.body.must_match(/counter/)
-    response.body.must_match(/foo/)
-  end
-
-  it "allows more than one '--' in the cookie when calculating digests" do
-    @counter = 0
-    app = lambda do |env|
-      env["rack.session"]["message"] ||= ""
-      env["rack.session"]["message"] += "#{(@counter += 1).to_s}--"
-      hash = env["rack.session"].dup
-      hash.delete("session_id")
-      Rack::Response.new(hash["message"]).to_a
-    end
-    # another example of an unsafe coder is Base64.urlsafe_encode64
-    unsafe_coder = Class.new {
-      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)
-    response.body.must_equal "1--"
-    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_pool.rb b/test/spec_session_pool.rb
deleted file mode 100644
index aba93fb169bdec8cf51ff03897e0d74ff821c0e9..0000000000000000000000000000000000000000
--- a/test/spec_session_pool.rb
+++ /dev/null
@@ -1,264 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-
-describe Rack::Session::Pool do
-  session_key = Rack::Session::Pool::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
-
-  get_session_id = Rack::Lint.new(lambda do |env|
-    Rack::Response.new(env["rack.session"].inspect).to_a
-  end)
-
-  nothing = Rack::Lint.new(lambda do |env|
-    Rack::Response.new("Nothing").to_a
-  end)
-
-  drop_session = Rack::Lint.new(lambda do |env|
-    env['rack.session.options'][:drop] = true
-    incrementor.call(env)
-  end)
-
-  renew_session = Rack::Lint.new(lambda do |env|
-    env['rack.session.options'][:renew] = true
-    incrementor.call(env)
-  end)
-
-  defer_session = Rack::Lint.new(lambda do |env|
-    env['rack.session.options'][:defer] = true
-    incrementor.call(env)
-  end)
-
-  incrementor = Rack::Lint.new(incrementor)
-
-  it "creates a new cookie" do
-    pool = Rack::Session::Pool.new(incrementor)
-    res = Rack::MockRequest.new(pool).get("/")
-    res["Set-Cookie"].must_match(session_match)
-    res.body.must_equal '{"counter"=>1}'
-  end
-
-  it "determines session from a cookie" do
-    pool = Rack::Session::Pool.new(incrementor)
-    req = Rack::MockRequest.new(pool)
-    cookie = req.get("/")["Set-Cookie"]
-    req.get("/", "HTTP_COOKIE" => cookie).
-      body.must_equal '{"counter"=>2}'
-    req.get("/", "HTTP_COOKIE" => cookie).
-      body.must_equal '{"counter"=>3}'
-  end
-
-  it "survives nonexistent cookies" do
-    pool = Rack::Session::Pool.new(incrementor)
-    res = Rack::MockRequest.new(pool).
-      get("/", "HTTP_COOKIE" => "#{session_key}=blarghfasel")
-    res.body.must_equal '{"counter"=>1}'
-  end
-
-  it "does not send the same session id if it did not change" do
-    pool = Rack::Session::Pool.new(incrementor)
-    req = Rack::MockRequest.new(pool)
-
-    res0 = req.get("/")
-    cookie = res0["Set-Cookie"][session_match]
-    res0.body.must_equal '{"counter"=>1}'
-    pool.pool.size.must_equal 1
-
-    res1 = req.get("/", "HTTP_COOKIE" => cookie)
-    res1["Set-Cookie"].must_be_nil
-    res1.body.must_equal '{"counter"=>2}'
-    pool.pool.size.must_equal 1
-
-    res2 = req.get("/", "HTTP_COOKIE" => cookie)
-    res2["Set-Cookie"].must_be_nil
-    res2.body.must_equal '{"counter"=>3}'
-    pool.pool.size.must_equal 1
-  end
-
-  it "deletes cookies with :drop option" 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)
-
-    res1 = req.get("/")
-    session = (cookie = res1["Set-Cookie"])[session_match]
-    res1.body.must_equal '{"counter"=>1}'
-    pool.pool.size.must_equal 1
-
-    res2 = dreq.get("/", "HTTP_COOKIE" => cookie)
-    res2["Set-Cookie"].must_be_nil
-    res2.body.must_equal '{"counter"=>2}'
-    pool.pool.size.must_equal 0
-
-    res3 = req.get("/", "HTTP_COOKIE" => cookie)
-    res3["Set-Cookie"][session_match].wont_equal session
-    res3.body.must_equal '{"counter"=>1}'
-    pool.pool.size.must_equal 1
-  end
-
-  it "provides new session id with :renew option" do
-    pool = Rack::Session::Pool.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}'
-    pool.pool.size.must_equal 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}'
-    pool.pool.size.must_equal 1
-
-    res3 = req.get("/", "HTTP_COOKIE" => new_cookie)
-    res3.body.must_equal '{"counter"=>3}'
-    pool.pool.size.must_equal 1
-
-    res4 = req.get("/", "HTTP_COOKIE" => cookie)
-    res4.body.must_equal '{"counter"=>1}'
-    pool.pool.size.must_equal 2
-  end
-
-  it "omits cookie with :defer option" do
-    pool = Rack::Session::Pool.new(incrementor)
-    defer = Rack::Utils::Context.new(pool, defer_session)
-    dreq = Rack::MockRequest.new(defer)
-
-    res1 = dreq.get("/")
-    res1["Set-Cookie"].must_be_nil
-    res1.body.must_equal '{"counter"=>1}'
-    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
-
-  it "passes through same_site option to session pool" do
-    pool = Rack::Session::Pool.new(incrementor, same_site: :none)
-    req = Rack::MockRequest.new(pool)
-    res = req.get("/")
-    res["Set-Cookie"].must_include "SameSite=None"
-  end
-
-  it "allows using a lambda to specify same_site option, because some browsers require different settings" do
-    pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :none })
-    req = Rack::MockRequest.new(pool)
-    res = req.get("/")
-    res["Set-Cookie"].must_include "SameSite=None"
-
-    pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :lax })
-    req = Rack::MockRequest.new(pool)
-    res = req.get("/")
-    res["Set-Cookie"].must_include "SameSite=Lax"
-  end
-
-  # anyone know how to do this better?
-  it "should merge sessions when multithreaded" do
-    unless $DEBUG
-      1.must_equal 1
-      next
-    end
-
-    warn 'Running multithread tests for Session::Pool'
-    pool = Rack::Session::Pool.new(incrementor)
-    req = Rack::MockRequest.new(pool)
-
-    res = req.get('/')
-    res.body.must_equal '{"counter"=>1}'
-    cookie = res["Set-Cookie"]
-    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
-      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 |resp|
-      resp['Set-Cookie'].must_equal cookie
-      resp.body.must_include '"counter"=>2'
-    end
-
-    session = pool.pool[sess_id]
-    session.size.must_equal tnum + 1 # counter
-    session['counter'].must_equal 2 # meeeh
-  end
-
-  it "does not return a cookie if cookie was not read/written" do
-    app = Rack::Session::Pool.new(nothing)
-    res = Rack::MockRequest.new(app).get("/")
-    res["Set-Cookie"].must_be_nil
-  end
-
-  it "does not return a cookie if cookie was not written (only read)" do
-    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' })
-    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)
-    res = Rack::MockRequest.new(app).get("/")
-    res["Set-Cookie"].must_be_nil
-  end
-end
diff --git a/test/spec_show_exceptions.rb b/test/spec_show_exceptions.rb
index 441599b4f86657323f15ab799c30d6315fe6d411..debc5c30bc95274be38a66e5300cecda6606d316 100644
--- a/test/spec_show_exceptions.rb
+++ b/test/spec_show_exceptions.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/show_exceptions'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ShowExceptions do
   def show_exceptions(app)
     Rack::Lint.new Rack::ShowExceptions.new(app)
@@ -31,7 +37,7 @@ describe Rack::ShowExceptions do
 
     req = Rack::MockRequest.new(
       show_exceptions(
-        lambda{|env| raise RuntimeError, "foo", ["nonexistant.rb:2:in `a': adf (RuntimeError)", "bad-backtrace"] }
+        lambda{|env| raise RuntimeError, "foo", ["nonexistent.rb:2:in `a': adf (RuntimeError)", "bad-backtrace"] }
     ))
 
     res = req.get("/", "HTTP_ACCEPT" => "text/html")
@@ -43,7 +49,7 @@ describe Rack::ShowExceptions do
     assert_includes(res.body, 'ShowExceptions')
     assert_includes(res.body, 'No GET data')
     assert_includes(res.body, 'No POST data')
-    assert_includes(res.body, 'nonexistant.rb')
+    assert_includes(res.body, 'nonexistent.rb')
     refute_includes(res.body, 'bad-backtrace')
   end
 
@@ -171,4 +177,29 @@ describe Rack::ShowExceptions do
       assert_equal(expected, exc.prefers_plaintext?(env))
     end
   end
+
+  it "prefers Exception#detailed_message instead of Exception#message if available" do
+    res = nil
+
+    custom_exc_class = Class.new(RuntimeError) do
+      def detailed_message(highlight: false)
+        "detailed_message_test"
+      end
+    end
+
+    req = Rack::MockRequest.new(
+      show_exceptions(
+        lambda{|env| raise custom_exc_class }
+    ))
+
+    res = req.get("/", "HTTP_ACCEPT" => "text/html")
+
+    res.must_be :server_error?
+    res.status.must_equal 500
+
+    assert_match(res, /detailed_message_test/)
+    assert_match(res, /ShowExceptions/)
+    assert_match(res, /No GET data/)
+    assert_match(res, /No POST data/)
+  end
 end
diff --git a/test/spec_show_status.rb b/test/spec_show_status.rb
index 486076b8d5b0c004e068334bd01950bc05002b1b..14c3da45a58d0212de9bd6e908a184d5e56fdfcf 100644
--- a/test/spec_show_status.rb
+++ b/test/spec_show_status.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/show_status'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::ShowStatus do
   def show_status(app)
     Rack::Lint.new Rack::ShowStatus.new(app)
@@ -10,14 +16,14 @@ 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.must_be :not_found?
     res.wont_be_empty
 
-    res["Content-Type"].must_equal "text/html"
+    res["content-type"].must_equal "text/html"
     assert_match(res, /404/)
     assert_match(res, /Not Found/)
   end
@@ -27,14 +33,14 @@ 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.must_be :not_found?
     res.wont_be_empty
 
-    res["Content-Type"].must_equal "text/html"
+    res["content-type"].must_equal "text/html"
     assert_match(res, /404/)
     assert_match(res, /Not Found/)
     assert_match(res, /too meta/)
@@ -45,14 +51,14 @@ 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.must_be :not_found?
     res.wont_be_empty
 
-    res["Content-Type"].must_equal "text/html"
+    res["content-type"].must_equal "text/html"
     assert_includes(res.body, '404')
     assert_includes(res.body, 'Not Found')
     assert_includes(res.body, '[&quot;gone too meta.&quot;]')
@@ -64,13 +70,13 @@ 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.wont_be_empty
 
-    res["Content-Type"].must_equal "text/html"
+    res["content-type"].must_equal "text/html"
     assert_match(res, /500/)
     res.wont_include detail
     res.body.must_include Rack::Utils.escape_html(detail)
@@ -80,7 +86,7 @@ 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)
@@ -90,13 +96,13 @@ describe Rack::ShowStatus do
   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["WWW-Authenticate"].must_equal "Basic blah"
+    res["www-authenticate"].must_equal "Basic blah"
   end
 
   it "replace existing messages if there is detail" do
@@ -104,17 +110,33 @@ 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.must_be :not_found?
     res.wont_be_empty
 
-    res["Content-Type"].must_equal "text/html"
-    res["Content-Length"].wont_equal "4"
+    res["content-type"].must_equal "text/html"
+    res["content-length"].wont_equal "4"
     assert_match(res, /404/)
     assert_match(res, /too meta/)
     res.body.wont_match(/foo/)
   end
+
+  it "close the original body" do
+    closed = false
+
+    body = Object.new
+    def body.each; yield 's' end
+    body.define_singleton_method(:close) { closed = true }
+
+    req = Rack::MockRequest.new(
+      show_status(lambda{|env|
+        [404, { "content-type" => "text/plain", "content-length" => "0" }, body]
+    }))
+
+    req.get("/", lint: true)
+    closed.must_equal true
+  end
 end
diff --git a/test/spec_static.rb b/test/spec_static.rb
index 2a94d68cafbd75522123e42b5218a38393a5d5c9..7496c032cb7b93ff59e5812ca1ac771b1ced4f76 100644
--- a/test/spec_static.rb
+++ b/test/spec_static.rb
@@ -3,9 +3,15 @@
 require_relative 'helper'
 require 'zlib'
 
+separate_testing do
+  require_relative '../lib/rack/static'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 class DummyApp
   def call(env)
-    [200, { "Content-Type" => "text/plain" }, ["Hello World"]]
+    [200, { "content-type" => "text/plain" }, ["Hello World"]]
   end
 end
 
@@ -115,24 +121,24 @@ describe Rack::Static do
   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.must_be :ok?
-    res.headers['Content-Encoding'].must_equal 'gzip'
-    res.headers['Content-Type'].must_equal 'text/plain'
+    res.headers['content-encoding'].must_equal 'gzip'
+    res.headers['content-type'].must_equal 'text/plain'
     Zlib::GzipReader.wrap(StringIO.new(res.body), &:read).must_match(/ruby/)
   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.must_be :ok?
-    res.headers['Content-Encoding'].must_be_nil
-    res.headers['Content-Type'].must_equal 'text/x-script.ruby'
+    res.headers['content-encoding'].must_be_nil
+    res.headers['content-type'].must_equal 'text/x-script.ruby'
     res.body.must_match(/ruby/)
   end
 
   it "serves regular files if client does not accept gzip encoding" do
     res = @gzip_request.get("/cgi/test")
     res.must_be :ok?
-    res.headers['Content-Encoding'].must_be_nil
-    res.headers['Content-Type'].must_equal 'text/plain'
+    res.headers['content-encoding'].must_be_nil
+    res.headers['content-type'].must_equal 'text/plain'
     res.body.must_match(/ruby/)
   end
 
@@ -141,8 +147,16 @@ describe Rack::Static do
     res = @gzip_request.get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path).httpdate)
     res.status.must_equal 304
     res.body.must_be :empty?
-    res.headers['Content-Encoding'].must_be_nil
-    res.headers['Content-Type'].must_be_nil
+    res.headers['content-encoding'].must_be_nil
+    res.headers['content-type'].must_be_nil
+  end
+
+  it "return 304 if gzipped file isn't modified since last serve" do
+    path = File.join(DOCROOT, "/cgi/test")
+    res = @gzip_request.get("/cgi/test", 'HTTP_IF_MODIFIED_SINCE' => File.mtime(path+'.gz').httpdate, 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip')
+
+    res.status.must_equal 304
+    res.body.must_be :empty?
   end
 
   it "supports serving fixed cache-control (legacy option)" do
@@ -150,71 +164,71 @@ describe Rack::Static do
     request = Rack::MockRequest.new(static(DummyApp.new, opts))
     res = request.get("/cgi/test")
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public'
+    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' }],
-    [false, { 'Cache-Control' => 'public, max-age=600' }]
+    [: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' }],
+    [false, { 'cache-control' => 'public, max-age=600' }]
   ] }
 
   it "supports header rule :all" do
     # Headers for all files via :all shortcut
     res = @header_request.get('/cgi/assets/index.html')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=100'
+    res.headers['cache-control'].must_equal 'public, max-age=100'
   end
 
   it "supports header rule :fonts" do
     # Headers for web fonts via :fonts shortcut
     res = @header_request.get('/cgi/assets/fonts/font.eot')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=200'
+    res.headers['cache-control'].must_equal 'public, max-age=200'
   end
 
   it "supports file extension header rules provided as an Array" do
     # Headers for file extensions via array
     res = @header_request.get('/cgi/assets/images/image.png')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=300'
+    res.headers['cache-control'].must_equal 'public, max-age=300'
   end
 
   it "supports folder rules provided as a String" do
     # Headers for files in folder via string
     res = @header_request.get('/cgi/assets/folder/test.js')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=400'
+    res.headers['cache-control'].must_equal 'public, max-age=400'
   end
 
   it "supports folder header rules provided as a String not starting with a slash" do
     res = @header_request.get('/cgi/assets/javascripts/app.js')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=500'
+    res.headers['cache-control'].must_equal 'public, max-age=500'
   end
 
   it "supports flexible header rules provided as Regexp" do
     # Flexible Headers via Regexp
     res = @header_request.get('/cgi/assets/stylesheets/app.css')
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=600'
+    res.headers['cache-control'].must_equal 'public, max-age=600'
   end
 
   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' }]
+        [:all, { 'cache-control' => 'public, max-age=42' }]
       ])
 
     request = Rack::MockRequest.new(static(DummyApp.new, opts))
     res = request.get("/cgi/test")
     res.must_be :ok?
-    res.headers['Cache-Control'].must_equal 'public, max-age=42'
+    res.headers['cache-control'].must_equal 'public, max-age=42'
   end
 
   it "expands the root path upon the middleware initialization" do
diff --git a/test/spec_tempfile_reaper.rb b/test/spec_tempfile_reaper.rb
index 063687a094aae61c490b53a50e354ec9de8d4006..7153297f29ed133cf1f6fcb772c2da47fdafc77b 100644
--- a/test/spec_tempfile_reaper.rb
+++ b/test/spec_tempfile_reaper.rb
@@ -2,6 +2,12 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/tempfile_reaper'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::TempfileReaper do
   class MockTempfile
     attr_reader :closed
@@ -30,6 +36,24 @@ describe Rack::TempfileReaper do
     response[0].must_equal 200
   end
 
+  it 'close env[rack.tempfiles] when app raises an error' do
+    tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
+    @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
+    app = lambda { |_| raise 'foo' }
+    proc{call(app)}.must_raise RuntimeError
+    tempfile1.closed.must_equal true
+    tempfile2.closed.must_equal true
+  end
+
+  it 'close env[rack.tempfiles] when app raises an non-StandardError' do
+    tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
+    @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
+    app = lambda { |_| raise LoadError, 'foo' }
+    proc{call(app)}.must_raise LoadError
+    tempfile1.closed.must_equal true
+    tempfile2.closed.must_equal true
+  end
+
   it 'close env[rack.tempfiles] when body is closed' do
     tempfile1, tempfile2 = MockTempfile.new, MockTempfile.new
     @env['rack.tempfiles'] = [ tempfile1, tempfile2 ]
@@ -60,4 +84,20 @@ describe Rack::TempfileReaper do
     tempfile1.closed.must_equal true
     tempfile2.closed.must_equal true
   end
+
+  it 'handle missing rack.tempfiles on normal response' do
+    app = lambda do |env|
+      env.delete('rack.tempfiles')
+      [200, {}, ['Hello, World!']]
+    end
+    call(app)[2].close
+  end
+
+  it 'handle missing rack.tempfiles on error' do
+    app = lambda do |env|
+      env.delete('rack.tempfiles')
+      raise 'Foo'
+    end
+    proc{call(app)}.must_raise RuntimeError
+  end
 end
diff --git a/test/spec_thin.rb b/test/spec_thin.rb
deleted file mode 100644
index f7a1211027d0ce037af68ea84b1cce278fc3fa78..0000000000000000000000000000000000000000
--- a/test/spec_thin.rb
+++ /dev/null
@@ -1,98 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-begin
-require 'rack/handler/thin'
-require_relative 'testrequest'
-require 'timeout'
-
-describe Rack::Handler::Thin do
-  include TestRequest::Helpers
-
-  before do
-    @app = Rack::Lint.new(TestRequest.new)
-    @server = nil
-    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|
-        @server = server
-      end
-    end
-
-    Thread.pass until @server && @server.running?
-  end
-
-  after do
-    @server.stop!
-    @thread.join
-  end
-
-
-  it "respond" do
-    GET("/")
-    response.wont_be :nil?
-  end
-
-  it "be a Thin" do
-    GET("/")
-    status.must_equal 200
-    response["SERVER_SOFTWARE"].must_match(/thin/)
-    response["HTTP_VERSION"].must_equal "HTTP/1.1"
-    response["SERVER_PROTOCOL"].must_equal "HTTP/1.1"
-    response["SERVER_PORT"].must_equal "9204"
-    response["SERVER_NAME"].must_equal "127.0.0.1"
-  end
-
-  it "have rack headers" do
-    GET("/")
-    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
-  end
-
-  it "have CGI headers on GET" do
-    GET("/")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["REQUEST_PATH"].must_equal "/"
-    response["PATH_INFO"].must_equal "/"
-    response["QUERY_STRING"].must_equal ""
-    response["test.postdata"].must_equal ""
-
-    GET("/test/foo?quux=1")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["REQUEST_PATH"].must_equal "/test/foo"
-    response["PATH_INFO"].must_equal "/test/foo"
-    response["QUERY_STRING"].must_equal "quux=1"
-  end
-
-  it "have CGI headers on POST" do
-    POST("/", { "rack-form-data" => "23" }, { 'X-test-header' => '42' })
-    status.must_equal 200
-    response["REQUEST_METHOD"].must_equal "POST"
-    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
-
-  it "set tag for server" do
-    @server.tag.must_equal 'tag'
-  end
-end
-
-rescue LoadError
-  $stderr.puts "Skipping Rack::Handler::Thin tests (Thin is required). `gem install thin` and try again."
-end
diff --git a/test/spec_urlmap.rb b/test/spec_urlmap.rb
index 29af5587052f26291da3cefb18ff178f6a7ef33d..c3ecffe29e3a7c21d4716658e352cad22d94464d 100644
--- a/test/spec_urlmap.rb
+++ b/test/spec_urlmap.rb
@@ -2,13 +2,19 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/urlmap'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+end
+
 describe Rack::URLMap do
   it "dispatches paths correctly" do
     app = lambda { |env|
       [200, {
-        'X-ScriptName' => env['SCRIPT_NAME'],
-        'X-PathInfo' => env['PATH_INFO'],
-        'Content-Type' => 'text/plain'
+        'x-scriptname' => env['SCRIPT_NAME'],
+        'x-pathinfo' => env['PATH_INFO'],
+        'content-type' => 'text/plain'
       }, [""]]
     }
     map = Rack::Lint.new(Rack::URLMap.new({
@@ -25,111 +31,116 @@ describe Rack::URLMap do
 
     res = Rack::MockRequest.new(map).get("/foo")
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/foo"
-    res["X-PathInfo"].must_equal ""
+    res["x-scriptname"].must_equal "/foo"
+    res["x-pathinfo"].must_equal ""
 
     res = Rack::MockRequest.new(map).get("/foo/")
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/foo"
-    res["X-PathInfo"].must_equal "/"
+    res["x-scriptname"].must_equal "/foo"
+    res["x-pathinfo"].must_equal "/"
 
     res = Rack::MockRequest.new(map).get("/foo/bar")
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/foo/bar"
-    res["X-PathInfo"].must_equal ""
+    res["x-scriptname"].must_equal "/foo/bar"
+    res["x-pathinfo"].must_equal ""
+
+    res = Rack::MockRequest.new(map).get("/foo/bard")
+    res.must_be :ok?
+    res["x-scriptname"].must_equal "/foo"
+    res["x-pathinfo"].must_equal "/bard"
 
     res = Rack::MockRequest.new(map).get("/foo/bar/")
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/foo/bar"
-    res["X-PathInfo"].must_equal "/"
+    res["x-scriptname"].must_equal "/foo/bar"
+    res["x-pathinfo"].must_equal "/"
 
     res = Rack::MockRequest.new(map).get("/foo///bar//quux")
     res.status.must_equal 200
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/foo/bar"
-    res["X-PathInfo"].must_equal "//quux"
+    res["x-scriptname"].must_equal "/foo/bar"
+    res["x-pathinfo"].must_equal "//quux"
 
     res = Rack::MockRequest.new(map).get("/foo/quux", "SCRIPT_NAME" => "/bleh")
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/bleh/foo"
-    res["X-PathInfo"].must_equal "/quux"
+    res["x-scriptname"].must_equal "/bleh/foo"
+    res["x-pathinfo"].must_equal "/quux"
 
     res = Rack::MockRequest.new(map).get("/bar", 'HTTP_HOST' => 'foo.org')
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/bar"
-    res["X-PathInfo"].must_be :empty?
+    res["x-scriptname"].must_equal "/bar"
+    res["x-pathinfo"].must_be :empty?
 
     res = Rack::MockRequest.new(map).get("/bar/", 'HTTP_HOST' => 'foo.org')
     res.must_be :ok?
-    res["X-ScriptName"].must_equal "/bar"
-    res["X-PathInfo"].must_equal '/'
+    res["x-scriptname"].must_equal "/bar"
+    res["x-pathinfo"].must_equal '/'
   end
 
 
   it "dispatches hosts correctly" do
     map = Rack::Lint.new(Rack::URLMap.new("http://foo.org/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "foo.org",
-                                "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+                              { "content-type" => "text/plain",
+                                "x-position" => "foo.org",
+                                "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"],
                               }, [""]]},
                            "http://subdomain.foo.org/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "subdomain.foo.org",
-                                "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+                              { "content-type" => "text/plain",
+                                "x-position" => "subdomain.foo.org",
+                                "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"],
                               }, [""]]},
                            "http://bar.org/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "bar.org",
-                                "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+                              { "content-type" => "text/plain",
+                                "x-position" => "bar.org",
+                                "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"],
                               }, [""]]},
                            "/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "default.org",
-                                "X-Host" => env["HTTP_HOST"] || env["SERVER_NAME"],
+                              { "content-type" => "text/plain",
+                                "x-position" => "default.org",
+                                "x-host" => env["HTTP_HOST"] || env["SERVER_NAME"],
                               }, [""]]}
                            ))
 
     res = Rack::MockRequest.new(map).get("/")
     res.must_be :ok?
-    res["X-Position"].must_equal "default.org"
+    res["x-position"].must_equal "default.org"
 
     res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "bar.org")
     res.must_be :ok?
-    res["X-Position"].must_equal "bar.org"
+    res["x-position"].must_equal "bar.org"
 
     res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "foo.org")
     res.must_be :ok?
-    res["X-Position"].must_equal "foo.org"
+    res["x-position"].must_equal "foo.org"
 
     res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "subdomain.foo.org", "SERVER_NAME" => "foo.org")
     res.must_be :ok?
-    res["X-Position"].must_equal "subdomain.foo.org"
+    res["x-position"].must_equal "subdomain.foo.org"
 
     res = Rack::MockRequest.new(map).get("http://foo.org/")
     res.must_be :ok?
-    res["X-Position"].must_equal "foo.org"
+    res["x-position"].must_equal "foo.org"
 
     res = Rack::MockRequest.new(map).get("/", "HTTP_HOST" => "example.org")
     res.must_be :ok?
-    res["X-Position"].must_equal "default.org"
+    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["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["x-position"].must_equal "default.org"
 
     res = Rack::MockRequest.new(map).get("/",
                                          "HTTP_HOST" => "example.org:9292",
                                          "SERVER_PORT" => "9292")
     res.must_be :ok?
-    res["X-Position"].must_equal "default.org"
+    res["x-position"].must_equal "default.org"
   end
 
   it "be nestable" do
@@ -137,10 +148,10 @@ describe Rack::URLMap do
       Rack::URLMap.new("/bar" =>
         Rack::URLMap.new("/quux" => lambda { |env|
                            [200,
-                            { "Content-Type" => "text/plain",
-                              "X-Position" => "/foo/bar/quux",
-                              "X-PathInfo" => env["PATH_INFO"],
-                              "X-ScriptName" => env["SCRIPT_NAME"],
+                            { "content-type" => "text/plain",
+                              "x-position" => "/foo/bar/quux",
+                              "x-pathinfo" => env["PATH_INFO"],
+                              "x-scriptname" => env["SCRIPT_NAME"],
                             }, [""]]}
                          ))))
 
@@ -149,98 +160,98 @@ describe Rack::URLMap do
 
     res = Rack::MockRequest.new(map).get("/foo/bar/quux")
     res.must_be :ok?
-    res["X-Position"].must_equal "/foo/bar/quux"
-    res["X-PathInfo"].must_equal ""
-    res["X-ScriptName"].must_equal "/foo/bar/quux"
+    res["x-position"].must_equal "/foo/bar/quux"
+    res["x-pathinfo"].must_equal ""
+    res["x-scriptname"].must_equal "/foo/bar/quux"
   end
 
   it "route root apps correctly" do
     map = Rack::Lint.new(Rack::URLMap.new("/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "root",
-                                "X-PathInfo" => env["PATH_INFO"],
-                                "X-ScriptName" => env["SCRIPT_NAME"]
+                              { "content-type" => "text/plain",
+                                "x-position" => "root",
+                                "x-pathinfo" => env["PATH_INFO"],
+                                "x-scriptname" => env["SCRIPT_NAME"]
                               }, [""]]},
                            "/foo" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "foo",
-                                "X-PathInfo" => env["PATH_INFO"],
-                                "X-ScriptName" => env["SCRIPT_NAME"]
+                              { "content-type" => "text/plain",
+                                "x-position" => "foo",
+                                "x-pathinfo" => env["PATH_INFO"],
+                                "x-scriptname" => env["SCRIPT_NAME"]
                               }, [""]]}
                            ))
 
     res = Rack::MockRequest.new(map).get("/foo/bar")
     res.must_be :ok?
-    res["X-Position"].must_equal "foo"
-    res["X-PathInfo"].must_equal "/bar"
-    res["X-ScriptName"].must_equal "/foo"
+    res["x-position"].must_equal "foo"
+    res["x-pathinfo"].must_equal "/bar"
+    res["x-scriptname"].must_equal "/foo"
 
     res = Rack::MockRequest.new(map).get("/foo")
     res.must_be :ok?
-    res["X-Position"].must_equal "foo"
-    res["X-PathInfo"].must_equal ""
-    res["X-ScriptName"].must_equal "/foo"
+    res["x-position"].must_equal "foo"
+    res["x-pathinfo"].must_equal ""
+    res["x-scriptname"].must_equal "/foo"
 
     res = Rack::MockRequest.new(map).get("/bar")
     res.must_be :ok?
-    res["X-Position"].must_equal "root"
-    res["X-PathInfo"].must_equal "/bar"
-    res["X-ScriptName"].must_equal ""
+    res["x-position"].must_equal "root"
+    res["x-pathinfo"].must_equal "/bar"
+    res["x-scriptname"].must_equal ""
 
     res = Rack::MockRequest.new(map).get("")
     res.must_be :ok?
-    res["X-Position"].must_equal "root"
-    res["X-PathInfo"].must_equal "/"
-    res["X-ScriptName"].must_equal ""
+    res["x-position"].must_equal "root"
+    res["x-pathinfo"].must_equal "/"
+    res["x-scriptname"].must_equal ""
   end
 
   it "not squeeze slashes" do
     map = Rack::Lint.new(Rack::URLMap.new("/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "root",
-                                "X-PathInfo" => env["PATH_INFO"],
-                                "X-ScriptName" => env["SCRIPT_NAME"]
+                              { "content-type" => "text/plain",
+                                "x-position" => "root",
+                                "x-pathinfo" => env["PATH_INFO"],
+                                "x-scriptname" => env["SCRIPT_NAME"]
                               }, [""]]},
                            "/foo" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "foo",
-                                "X-PathInfo" => env["PATH_INFO"],
-                                "X-ScriptName" => env["SCRIPT_NAME"]
+                              { "content-type" => "text/plain",
+                                "x-position" => "foo",
+                                "x-pathinfo" => env["PATH_INFO"],
+                                "x-scriptname" => env["SCRIPT_NAME"]
                               }, [""]]}
                            ))
 
     res = Rack::MockRequest.new(map).get("/http://example.org/bar")
     res.must_be :ok?
-    res["X-Position"].must_equal "root"
-    res["X-PathInfo"].must_equal "/http://example.org/bar"
-    res["X-ScriptName"].must_equal ""
+    res["x-position"].must_equal "root"
+    res["x-pathinfo"].must_equal "/http://example.org/bar"
+    res["x-scriptname"].must_equal ""
   end
 
   it "not be case sensitive with hosts" do
     map = Rack::Lint.new(Rack::URLMap.new("http://example.org/" => lambda { |env|
                              [200,
-                              { "Content-Type" => "text/plain",
-                                "X-Position" => "root",
-                                "X-PathInfo" => env["PATH_INFO"],
-                                "X-ScriptName" => env["SCRIPT_NAME"]
+                              { "content-type" => "text/plain",
+                                "x-position" => "root",
+                                "x-pathinfo" => env["PATH_INFO"],
+                                "x-scriptname" => env["SCRIPT_NAME"]
                               }, [""]]}
                            ))
 
     res = Rack::MockRequest.new(map).get("http://example.org/")
     res.must_be :ok?
-    res["X-Position"].must_equal "root"
-    res["X-PathInfo"].must_equal "/"
-    res["X-ScriptName"].must_equal ""
+    res["x-position"].must_equal "root"
+    res["x-pathinfo"].must_equal "/"
+    res["x-scriptname"].must_equal ""
 
     res = Rack::MockRequest.new(map).get("http://EXAMPLE.ORG/")
     res.must_be :ok?
-    res["X-Position"].must_equal "root"
-    res["X-PathInfo"].must_equal "/"
-    res["X-ScriptName"].must_equal ""
+    res["x-position"].must_equal "root"
+    res["x-pathinfo"].must_equal "/"
+    res["x-scriptname"].must_equal ""
   end
 
   it "not allow locations unless they start with /" do
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 90676258fd9783b2203011812c3f834af1003d37..c4f9b27fa1b2fd847c81db7196dfa006e3ed05f7 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -3,6 +3,14 @@
 require_relative 'helper'
 require 'timeout'
 
+separate_testing do
+  require_relative '../lib/rack/utils'
+  require_relative '../lib/rack/lint'
+  require_relative '../lib/rack/mock_request'
+  require_relative '../lib/rack/request'
+  require_relative '../lib/rack/content_length'
+end
+
 describe Rack::Utils do
 
   def assert_sets(exp, act)
@@ -114,7 +122,7 @@ describe Rack::Utils do
     ex = { "foo" => nil }
     ex["foo"] = ex
 
-    params = Rack::Utils::KeySpaceConstrainedParams.new(65536)
+    params = Rack::Utils::KeySpaceConstrainedParams.new
     params['foo'] = params
     params.to_params_hash.to_s.must_equal ex.to_s
   end
@@ -123,6 +131,22 @@ describe Rack::Utils do
     Rack::Utils.parse_nested_query(nil).must_equal({})
   end
 
+  deprecated "should warn for deprecated QueryParser.make_default call with key_space_limit" do
+    Rack::QueryParser.make_default(1, 1).must_be_kind_of Rack::QueryParser
+  end
+
+  deprecated "should warn using deprecated QueryParser.new call with key_space_limit" do
+    Rack::QueryParser.new(Rack::QueryParser::Params, 1, 1).must_be_kind_of Rack::QueryParser
+  end
+
+  deprecated "should warn using deprecated Rack::Util.key_space_limit=" do
+    Rack::Utils.key_space_limit = 65536
+  end
+
+  deprecated "should warn using deprecated Rack::Util.key_space_limit" do
+    Rack::Utils.key_space_limit
+  end
+
   it "raise an exception if the params are too deep" do
     len = Rack::Utils.param_depth_limit
 
@@ -133,12 +157,6 @@ describe Rack::Utils do
     Rack::Utils.parse_nested_query("foo#{"[a]" * (len - 1)}=bar")
   end
 
-  # ParamsTooDeepError was introduced in the middle of 2.2 releases
-  # and this test is here to ensure backwards compatibility
-  it "ParamsTooDeepError is inherited from originally used RangeError" do
-    (Rack::QueryParser::ParamsTooDeepError < RangeError).must_equal(true)
-  end
-
   it "parse nested query strings correctly" do
     Rack::Utils.parse_nested_query("foo").
       must_equal "foo" => nil
@@ -191,6 +209,8 @@ describe Rack::Utils do
     Rack::Utils.parse_nested_query("foo[]=bar&baz[]=1&baz[]=2&baz[]=3").
       must_equal "foo" => ["bar"], "baz" => ["1", "2", "3"]
 
+    Rack::Utils.parse_nested_query("x[y][z]").
+      must_equal "x" => { "y" => { "z" => nil } }
     Rack::Utils.parse_nested_query("x[y][z]=1").
       must_equal "x" => { "y" => { "z" => "1" } }
     Rack::Utils.parse_nested_query("x[y][z][]=1").
@@ -237,9 +257,8 @@ describe Rack::Utils do
       must_raise(Rack::Utils::ParameterTypeError).
       message.must_equal "expected Array (got String) for param `y'"
 
-    lambda { Rack::Utils.parse_nested_query("foo%81E=1") }.
-      must_raise(Rack::Utils::InvalidParameterError).
-      message.must_equal "invalid byte sequence in UTF-8"
+    Rack::Utils.parse_nested_query("foo%81E=1").
+      must_equal "foo\x81E"=>"1"
   end
 
   it "only moves to a new array when the full key has been seen" do
@@ -254,6 +273,20 @@ describe Rack::Utils do
       ]
   end
 
+  it "handles unexpected use of [ and ] in parameter keys as normal characters" do
+    Rack::Utils.parse_nested_query("[]=1&[a]=2&b[=3&c]=4").
+      must_equal "[]" => "1", "[a]" => "2", "b[" => "3", "c]" => "4"
+
+    Rack::Utils.parse_nested_query("d[[]=5&e][]=6&f[[]]=7").
+      must_equal "d" => {"[" => "5"}, "e]" => ["6"], "f" => { "[" => { "]" => "7" } }
+
+    Rack::Utils.parse_nested_query("g[h]i=8&j[k]l[m]=9").
+      must_equal "g" => { "h" => { "i" => "8" } }, "j" => { "k" => { "l[m]" =>"9" } }
+
+    Rack::Utils.parse_nested_query("l[[[[[[[[]]]]]]]=10").
+      must_equal "l"=>{"[[[[[[["=>{"]]]]]]"=>"10"}}
+  end
+
   it "allow setting the params hash class to use for parsing query strings" do
     begin
       default_parser = Rack::Utils.default_query_parser
@@ -263,7 +296,7 @@ describe Rack::Utils do
           @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)
+      Rack::Utils.default_query_parser = Rack::QueryParser.new(param_parser_class, 100)
       h1 = Rack::Utils.parse_query(",foo=bar;,", ";,")
       h1[:foo].must_equal "bar"
       h2 = Rack::Utils.parse_nested_query("x[y][][z]=1&x[y][][w]=2")
@@ -295,9 +328,9 @@ describe Rack::Utils do
     assert_nested_query("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
                         "my weird field" => "q1!2\"'w$5&7/z8)?")
 
-    Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo[]"
-    Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo[]="
-    Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo[]=bar"
+    Rack::Utils.build_nested_query("foo" => [nil]).must_equal "foo%5B%5D"
+    Rack::Utils.build_nested_query("foo" => [""]).must_equal "foo%5B%5D="
+    Rack::Utils.build_nested_query("foo" => ["bar"]).must_equal "foo%5B%5D=bar"
     Rack::Utils.build_nested_query('foo' => []).must_equal ''
     Rack::Utils.build_nested_query('foo' => {}).must_equal ''
     Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => []).must_equal 'foo=bar'
@@ -308,35 +341,39 @@ describe Rack::Utils do
     Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => '').
       must_equal 'foo=bar&baz='
     Rack::Utils.build_nested_query('foo' => ['1', '2']).
-      must_equal 'foo[]=1&foo[]=2'
+      must_equal 'foo%5B%5D=1&foo%5B%5D=2'
     Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => ['1', '2', '3']).
-      must_equal 'foo=bar&baz[]=1&baz[]=2&baz[]=3'
+      must_equal 'foo=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
     Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']).
-      must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3'
+      must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
     Rack::Utils.build_nested_query('foo' => ['bar'], 'baz' => ['1', '2', '3']).
-      must_equal 'foo[]=bar&baz[]=1&baz[]=2&baz[]=3'
+      must_equal 'foo%5B%5D=bar&baz%5B%5D=1&baz%5B%5D=2&baz%5B%5D=3'
     Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => '1' } }).
-      must_equal 'x[y][z]=1'
+      must_equal 'x%5By%5D%5Bz%5D=1'
     Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1'] } }).
-      must_equal 'x[y][z][]=1'
+      must_equal 'x%5By%5D%5Bz%5D%5B%5D=1'
     Rack::Utils.build_nested_query('x' => { 'y' => { 'z' => ['1', '2'] } }).
-      must_equal 'x[y][z][]=1&x[y][z][]=2'
+      must_equal 'x%5By%5D%5Bz%5D%5B%5D=1&x%5By%5D%5Bz%5D%5B%5D=2'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }] }).
-      must_equal 'x[y][][z]=1'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D=1'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => ['1'] }] }).
-      must_equal 'x[y][][z][]=1'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D%5B%5D=1'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'w' => '2' }] }).
-      must_equal 'x[y][][z]=1&x[y][][w]=2'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=2'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'v' => { 'w' => '1' } }] }).
-      must_equal 'x[y][][v][w]=1'
+      must_equal 'x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=1'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1', 'v' => { 'w' => '2' } }] }).
-      must_equal 'x[y][][z]=1&x[y][][v][w]=2'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bv%5D%5Bw%5D=2'
     Rack::Utils.build_nested_query('x' => { 'y' => [{ 'z' => '1' }, { 'z' => '2' }] }).
-      must_equal 'x[y][][z]=1&x[y][][z]=2'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bz%5D=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'
+      must_equal 'x%5By%5D%5B%5D%5Bz%5D=1&x%5By%5D%5B%5D%5Bw%5D=a&x%5By%5D%5B%5D%5Bz%5D=2&x%5By%5D%5B%5D%5Bw%5D=3'
     Rack::Utils.build_nested_query({ "foo" => ["1", ["2"]] }).
-      must_equal 'foo[]=1&foo[][]=2'
+      must_equal 'foo%5B%5D=1&foo%5B%5D%5B%5D=2'
+
+    # A nested hash is the same as string keys with brackets.
+    Rack::Utils.build_nested_query('foo' => { 'bar' => 'baz' }).
+      must_equal Rack::Utils.build_nested_query('foo[bar]' => 'baz')
 
     lambda { Rack::Utils.build_nested_query("foo=bar") }.
       must_raise(ArgumentError).
@@ -344,7 +381,7 @@ describe Rack::Utils do
   end
 
   it 'performs the inverse function of #parse_nested_query' do
-    [{ "foo" => nil, "bar" => "" },
+    [{ "bar" => "" },
       { "foo" => "bar", "baz" => "" },
       { "foo" => ["1", "2"] },
       { "foo" => "bar", "baz" => ["1", "2", "3"] },
@@ -392,6 +429,45 @@ describe Rack::Utils do
     ]
   end
 
+  it "parses RFC 7239 Forwarded header" do
+    Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({
+      for: [ '3.4.5.6' ],
+    })
+
+    Rack::Utils.forwarded_values(';;;for=3.4.5.6,,').must_equal({
+      for: [ '3.4.5.6' ],
+    })
+
+    Rack::Utils.forwarded_values('for=3.4.5.6').must_equal({
+      for: [ '3.4.5.6' ],
+    })
+
+    Rack::Utils.forwarded_values('for =  3.4.5.6').must_equal({
+      for: [ '3.4.5.6' ],
+    })
+
+    Rack::Utils.forwarded_values('for="3.4.5.6"').must_equal({
+      for: [ '3.4.5.6' ],
+    })
+
+    Rack::Utils.forwarded_values('for=3.4.5.6;proto=https').must_equal({
+      for: [ '3.4.5.6' ],
+      proto: [ 'https' ]
+    })
+
+    Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https').must_equal({
+      for: [ '3.4.5.6' ],
+      proto: [ 'http', 'https' ]
+    })
+
+    Rack::Utils.forwarded_values('for=3.4.5.6; proto=http, proto=https; for=1.2.3.4').must_equal({
+      for: [ '3.4.5.6', '1.2.3.4' ],
+      proto: [ 'http', 'https' ]
+    })
+
+    Rack::Utils.forwarded_values('for=3.4.5.6; foo=bar').must_be_nil
+  end
+
   it "select best quality match" do
     Rack::Utils.best_q_match("text/html", %w[text/html]).must_equal "text/html"
 
@@ -463,6 +539,7 @@ describe Rack::Utils do
   it "should perform constant time string comparison" do
     Rack::Utils.secure_compare('a', 'a').must_equal true
     Rack::Utils.secure_compare('a', 'b').must_equal false
+    Rack::Utils.secure_compare('a', 'bb').must_equal false
   end
 
   it "return status code for integer" do
@@ -487,10 +564,6 @@ describe Rack::Utils do
     Rack::Utils.rfc2822(Time.at(0).gmtime).must_equal "Thu, 01 Jan 1970 00:00:00 -0000"
   end
 
-  it "return rfc2109 format from rfc2109 helper" do
-    Rack::Utils.rfc2109(Time.at(0).gmtime).must_equal "Thu, 01-Jan-1970 00:00:00 GMT"
-  end
-
   it "clean directory traversal" do
     Rack::Utils.clean_path_info("/cgi/../cgi/test").must_equal "/cgi/test"
     Rack::Utils.clean_path_info(".").must_be_empty
@@ -513,6 +586,9 @@ end
 
 describe Rack::Utils, "cookies" do
   it "parses cookies" do
+    env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "a=b; ; c=d")
+    Rack::Utils.parse_cookies(env).must_equal({ "a" => "b", "c" => "d" })
+
     env = Rack::MockRequest.env_for("", "HTTP_COOKIE" => "zoo=m")
     Rack::Utils.parse_cookies(env).must_equal({ "zoo" => "m" })
 
@@ -536,236 +612,192 @@ describe Rack::Utils, "cookies" do
     cookies.must_equal({ "%66oo" => "baz", "foo" => "bar" })
   end
 
-  it "adds new cookies to nil header" do
+  it "generates appropriate cookie header value" do
+    Rack::Utils.set_cookie_header('name', 'value').must_equal 'name=value'
+    Rack::Utils.set_cookie_header('name', %w[value]).must_equal 'name=value'
+    Rack::Utils.set_cookie_header('name', %w[va ue]).must_equal 'name=va&ue'
+  end
+
+  deprecated "adds new cookies to nil header" do
     Rack::Utils.add_cookie_to_header(nil, 'name', 'value').must_equal 'name=value'
   end
 
-  it "adds new cookies to blank header" do
+  deprecated "adds new cookies to blank header" do
     header = ''
     Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal 'name=value'
     header.must_equal ''
   end
 
-  it "adds new cookies to string header" do
+  deprecated "adds new cookies to string header" do
     header = 'existing-cookie'
-    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value"
+    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal ["existing-cookie", "name=value"]
     header.must_equal 'existing-cookie'
   end
 
-  it "adds new cookies to array header" do
+  deprecated "adds new cookies to array header" do
     header = %w[ existing-cookie ]
-    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal "existing-cookie\nname=value"
+    Rack::Utils.add_cookie_to_header(header, 'name', 'value').must_equal ["existing-cookie", "name=value"]
     header.must_equal %w[ existing-cookie ]
   end
 
-  it "adds new cookies to an unrecognized header" do
+  deprecated "adds new cookies to an unrecognized header" do
     lambda {
       Rack::Utils.add_cookie_to_header(Object.new, 'name', 'value')
     }.must_raise ArgumentError
   end
 
   it "sets and deletes cookies in header hash" do
-    header = { 'Set-Cookie' => '' }
-    Rack::Utils.set_cookie_header!(header, 'name', 'value').must_be_nil
-    header['Set-Cookie'].must_equal 'name=value'
-    Rack::Utils.set_cookie_header!(header, 'name2', 'value2').must_be_nil
-    header['Set-Cookie'].must_equal "name=value\nname2=value2"
-    Rack::Utils.set_cookie_header!(header, 'name2', 'value3').must_be_nil
-    header['Set-Cookie'].must_equal "name=value\nname2=value2\nname2=value3"
-
-    Rack::Utils.delete_cookie_header!(header, 'name2').must_be_nil
-    header['Set-Cookie'].must_equal "name=value\nname2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
-    Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil
-    header['Set-Cookie'].must_equal "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT\nname=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
-
-    header = { 'Set-Cookie' => nil }
-    Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil
-    header['Set-Cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
-
-    header = { 'Set-Cookie' => [] }
-    Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil
-    header['Set-Cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    headers = {}
+    Rack::Utils.set_cookie_header!(headers, 'name', 'value')
+    headers['set-cookie'].must_equal 'name=value'
+    Rack::Utils.set_cookie_header!(headers, 'name2', 'value2')
+    headers['set-cookie'].must_equal ['name=value', 'name2=value2']
+    Rack::Utils.set_cookie_header!(headers, 'name2', 'value3')
+    headers['set-cookie'].must_equal ['name=value', 'name2=value2', 'name2=value3']
   end
 
-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
-    # 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
-  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)]
+  it "encodes cookie key values by default" do
+    Rack::Utils.set_cookie_header('na e', 'value').must_equal 'na+e=value'
   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)]
+  it "does not encode cookie key values if :escape_key is false" do
+    Rack::Utils.set_cookie_header('na e', value: 'value', escape_key: false).must_equal 'na e=value'
   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)]
-  end
+  it "deletes cookies in header field" do
+    header = []
 
-  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 []
-  end
+    Rack::Utils.delete_set_cookie_header!(header, 'name2')
+    header.must_equal [
+      "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
 
-  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.delete_set_cookie_header!(header, 'name')
+    header.must_equal [
+      "name2=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT",
+      "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   end
-end
 
-describe Rack::Utils::HeaderHash do
-  it "retain header case" do
-    h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
-    h['ETag'] = 'Boo!'
-    h.to_hash.must_equal "Content-MD5" => "d5ff4e2a0 ...", "ETag" => 'Boo!'
-  end
+  it "deletes cookies in header field with domain" do
+    header = []
 
-  it "check existence of keys case insensitively" do
-    h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
-    h.must_include 'content-md5'
-    h.wont_include 'ETag'
+    Rack::Utils.delete_set_cookie_header!(header, 'name', {domain: "mydomain.com"})
+    header.must_equal [
+      "name=; domain=mydomain.com; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   end
 
-  it "create deep HeaderHash copy on dup" do
-    h1 = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
-    h2 = h1.dup
+  it "deletes cookies in header field with path" do
+    header = []
 
-    h1.must_include 'content-md5'
-    h2.must_include 'content-md5'
-
-    h2.delete("Content-MD5")
-
-    h2.wont_include 'content-md5'
-    h1.must_include 'content-md5'
-  end
-
-  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'
+    Rack::Utils.delete_set_cookie_header!(header, 'name', {path: "/a/b"})
+    header.must_equal [
+      "name=; path=/a/b; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    ]
   end
 
-  it "overwrite case insensitively and assume the new key's case" do
-    h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
-    h["foo-bar"] = "bizzle"
-    h["FOO-BAR"].must_equal "bizzle"
-    h.length.must_equal 1
-    h.to_hash.must_equal "foo-bar" => "bizzle"
-  end
+  it "sets and deletes cookies in header hash" do
+    header = { 'set-cookie' => nil }
+    Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil
+    header['set-cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
 
-  it "be converted to real Hash" do
-    h = Rack::Utils::HeaderHash.new("foo" => "bar")
-    h.to_hash.must_be_instance_of Hash
+    header = { 'set-cookie' => nil }
+    Rack::Utils.delete_cookie_header!(header, 'name').must_be_nil
+    header['set-cookie'].must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
   end
 
-  it "convert Array values to Strings when converting to Hash" do
-    h = Rack::Utils::HeaderHash.new("foo" => ["bar", "baz"])
-    h.to_hash.must_equal({ "foo" => "bar\nbaz" })
+  deprecated "sets deleted cookie" do
+    Rack::Utils.make_delete_cookie_header(nil, 'name', {}).
+      must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
+    Rack::Utils.add_remove_cookie_to_header(nil, 'name').
+      must_equal "name=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT"
   end
+end
 
-  it "replace hashes correctly" do
-    h = Rack::Utils::HeaderHash.new("Foo-Bar" => "baz")
-    j = { "foo" => "bar" }
-    h.replace(j)
-    h["foo"].must_equal "bar"
+describe Rack::Utils, "get_byte_ranges" do
+  deprecated "pase simple byte ranges from env" do
+    Rack::Utils.byte_ranges({ "HTTP_RANGE" => "bytes=123-456" }, 500).must_equal [(123..456)]
   end
 
-  it "be able to delete the given key case-sensitively" do
-    h = Rack::Utils::HeaderHash.new("foo" => "bar")
-    h.delete("foo")
-    h["foo"].must_be_nil
-    h["FOO"].must_be_nil
+  it "ignore missing or syntactically invalid byte ranges" do
+    Rack::Utils.get_byte_ranges(nil, 500).must_be_nil
+    Rack::Utils.get_byte_ranges("foobar", 500).must_be_nil
+    Rack::Utils.get_byte_ranges("furlongs=123-456", 500).must_be_nil
+    Rack::Utils.get_byte_ranges("bytes=", 500).must_be_nil
+    Rack::Utils.get_byte_ranges("bytes=-", 500).must_be_nil
+    Rack::Utils.get_byte_ranges("bytes=123,456", 500).must_be_nil
+    # A range of non-positive length is syntactically invalid and ignored:
+    Rack::Utils.get_byte_ranges("bytes=456-123", 500).must_be_nil
+    Rack::Utils.get_byte_ranges("bytes=456-455", 500).must_be_nil
   end
 
-  it "be able to delete the given key case-insensitively" do
-    h = Rack::Utils::HeaderHash.new("foo" => "bar")
-    h.delete("FOO")
-    h["foo"].must_be_nil
-    h["FOO"].must_be_nil
+  it "parse simple byte ranges" do
+    Rack::Utils.get_byte_ranges("bytes=123-456", 500).must_equal [(123..456)]
+    Rack::Utils.get_byte_ranges("bytes=123-", 500).must_equal [(123..499)]
+    Rack::Utils.get_byte_ranges("bytes=-100", 500).must_equal [(400..499)]
+    Rack::Utils.get_byte_ranges("bytes=0-0", 500).must_equal [(0..0)]
+    Rack::Utils.get_byte_ranges("bytes=499-499", 500).must_equal [(499..499)]
   end
 
-  it "return the deleted value when #delete is called on an existing key" do
-    h = Rack::Utils::HeaderHash.new("foo" => "bar")
-    h.delete("Foo").must_equal "bar"
+  it "parse several byte ranges" do
+    Rack::Utils.get_byte_ranges("bytes=500-600,601-999", 1000).must_equal [(500..600), (601..999)]
   end
 
-  it "return nil when #delete is called on a non-existent key" do
-    h = Rack::Utils::HeaderHash.new("foo" => "bar")
-    h.delete("Hello").must_be_nil
+  it "truncate byte ranges" do
+    Rack::Utils.get_byte_ranges("bytes=123-999", 500).must_equal [(123..499)]
+    Rack::Utils.get_byte_ranges("bytes=600-999", 500).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=-999", 500).must_equal [(0..499)]
   end
 
-  it "dups given HeaderHash" do
-    a = Rack::Utils::HeaderHash.new("foo" => "bar")
-    b = Rack::Utils::HeaderHash.new(a)
-    b.object_id.wont_equal a.object_id
-    b.must_equal a
+  it "ignore unsatisfiable byte ranges" do
+    Rack::Utils.get_byte_ranges("bytes=500-501", 500).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=500-", 500).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=999-", 500).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=-0", 500).must_equal []
   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|
-      k.must_equal "foo"
-      v.must_equal "bar\nbaz"
-    end
+  it "handle byte ranges of empty files" do
+    Rack::Utils.get_byte_ranges("bytes=123-456", 0).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=0-", 0).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=-100", 0).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=0-0", 0).must_equal []
+    Rack::Utils.get_byte_ranges("bytes=-0", 0).must_equal []
   end
+end
 
-  it "not create headers out of thin air" do
-    h = Rack::Utils::HeaderHash.new
-    h['foo']
-    h['foo'].must_be_nil
-    h.wont_include 'foo'
+describe Rack::Utils::HeaderHash do
+  deprecated ".[] returns Rack::Headers as is if not frozen" do
+    h1 = Rack::Headers["Content-MD5" => "d5ff4e2a0 ..."]
+    h2 = Rack::Utils::HeaderHash[h1]
+    h2.must_be_same_as h1
+    h3 = Rack::Utils::HeaderHash[h1.freeze]
+    h3.wont_be_same_as h1
+    h3.must_equal h1
   end
 
-  it "uses memoized header hash" do
-    env = {}
-    headers = Rack::Utils::HeaderHash.new({ 'content-type' => "text/plain", "content-length" => "3" })
-
-    app = lambda do |env|
-      [200, headers, []]
-    end
-
-    app = Rack::ContentLength.new(app)
+  deprecated ".[] returns instance of Rack::Headers" do
+    h = Rack::Utils::HeaderHash["Content-MD5" => "d5ff4e2a0 ..."]
+    h.must_be_kind_of Rack::Headers
+    h['content-md5'].must_equal "d5ff4e2a0 ..."
 
-    response = app.call(env)
-    assert_same response[1], headers
+    h = Rack::Utils::HeaderHash[[["Content-MD5","d5ff4e2a0 ..."]]]
+    h.must_be_kind_of Rack::Headers
+    h['content-md5'].must_equal "d5ff4e2a0 ..."
   end
 
-  it "duplicates header hash" do
-    env = {}
-    headers = Rack::Utils::HeaderHash.new({ 'content-type' => "text/plain", "content-length" => "3" })
-    headers.freeze
-
-    app = lambda do |env|
-      [200, headers, []]
-    end
+  deprecated ".new returns instance of Rack::Headers" do
+    h = Rack::Utils::HeaderHash.new("Content-MD5" => "d5ff4e2a0 ...")
+    h.must_be_kind_of Rack::Headers
+    h['content-md5'].must_equal "d5ff4e2a0 ..."
 
-    app = Rack::ContentLength.new(app)
+    h = Rack::Utils::HeaderHash.new([["Content-MD5","d5ff4e2a0 ..."]])
+    h.must_be_kind_of Rack::Headers
+    h['content-md5'].must_equal "d5ff4e2a0 ..."
+  end
 
-    response = app.call(env)
-    refute_same response[1], headers
+  it ".allocate raises" do
+    proc { Rack::Utils::HeaderHash.allocate }.must_raise TypeError
   end
 end
 
@@ -779,7 +811,7 @@ describe Rack::Utils::Context do
   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
@@ -823,4 +855,10 @@ describe Rack::Utils::Context do
     r5.status.must_equal 200
     r4.body.must_equal r5.body
   end
+
+  it "raises for invalid context" do
+    proc do
+      Rack::Utils::Context.new(nil, test_target1)
+    end.must_raise RuntimeError
+  end
 end
diff --git a/test/spec_version.rb b/test/spec_version.rb
index 68c4b4c72815672df01c11922cbd1817b8d772d2..bdd903d1d7d4b09751f9446f401610e7a502715d 100644
--- a/test/spec_version.rb
+++ b/test/spec_version.rb
@@ -2,10 +2,14 @@
 
 require_relative 'helper'
 
+separate_testing do
+  require_relative '../lib/rack/version'
+end
+
 describe Rack do
   describe 'version' do
-    it 'defaults to a hard-coded api version' do
-      Rack.version.must_equal "1.3"
+    it 'is a version string' do
+      Rack::RELEASE.must_match(/\d+\.\d+\.\d+/)
     end
   end
 end
diff --git a/test/spec_webrick.rb b/test/spec_webrick.rb
deleted file mode 100644
index a3c324a90e531e62c9cabb947e76430ab558eeba..0000000000000000000000000000000000000000
--- a/test/spec_webrick.rb
+++ /dev/null
@@ -1,216 +0,0 @@
-# frozen_string_literal: true
-
-require_relative 'helper'
-require 'thread'
-require_relative 'testrequest'
-
-Thread.abort_on_exception = true
-
-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.mount "/test", Rack::Handler::WEBrick,
-    Rack::Lint.new(TestRequest.new)
-  @thread = Thread.new { @server.start }
-  trap(:INT) { @server.shutdown }
-  @status_thread = Thread.new do
-    seconds = 10
-    wait_time = 0.1
-    until is_running? || seconds <= 0
-      seconds -= wait_time
-      sleep wait_time
-    end
-    raise "Server never reached status 'Running'" unless is_running?
-  end
-  end
-
-  def is_running?
-    @server.status == :Running
-  end
-
-  it "respond" do
-    GET("/test")
-    status.must_equal 200
-  end
-
-  it "be a WEBrick" do
-    GET("/test")
-    status.must_equal 200
-    response["SERVER_SOFTWARE"].must_match(/WEBrick/)
-    response["HTTP_VERSION"].must_equal "HTTP/1.1"
-    response["SERVER_PROTOCOL"].must_equal "HTTP/1.1"
-    response["SERVER_PORT"].must_equal "9202"
-    response["SERVER_NAME"].must_equal "127.0.0.1"
-  end
-
-  it "have rack headers" do
-    GET("/test")
-    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"]
-  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 "/test"
-    response["PATH_INFO"].must_equal ""
-    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 "/test/foo"
-    response["PATH_INFO"].must_equal "/foo"
-    response["QUERY_STRING"].must_equal "quux=1"
-
-    GET("/test/foo%25encoding?quux=1")
-    response["REQUEST_METHOD"].must_equal "GET"
-    response["SCRIPT_NAME"].must_equal "/test"
-    response["REQUEST_PATH"].must_equal "/test/foo%25encoding"
-    response["PATH_INFO"].must_equal "/foo%25encoding"
-    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 "/test"
-    response["PATH_INFO"].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
-
-  it "correctly set cookies" do
-    @server.mount "/cookie-test", Rack::Handler::WEBrick,
-    Rack::Lint.new(lambda { |req|
-                     res = Rack::Response.new
-                     res.set_cookie "one", "1"
-                     res.set_cookie "two", "2"
-                     res.finish
-                   })
-
-    Net::HTTP.start(@host, @port) { |http|
-      res = http.get("/cookie-test")
-      res.code.to_i.must_equal 200
-      res.get_fields("set-cookie").must_equal ["one=1", "two=2"]
-    }
-  end
-
-  it "provide a .run" do
-    queue = Queue.new
-
-    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|
-        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
-
-  it "return repeated headers" do
-    @server.mount "/headers", Rack::Handler::WEBrick,
-    Rack::Lint.new(lambda { |req|
-        [
-          401,
-          { "Content-Type" => "text/plain",
-            "WWW-Authenticate" => "Bar realm=X\nBaz realm=Y" },
-          [""]
-        ]
-      })
-
-    Net::HTTP.start(@host, @port) { |http|
-      res = http.get("/headers")
-      res.code.to_i.must_equal 401
-      res["www-authenticate"].must_equal "Bar realm=X, Baz realm=Y"
-    }
-  end
-
-  it "support Rack partial hijack" do
-    io_lambda = lambda{ |io|
-      5.times do
-        io.write "David\r\n"
-      end
-      io.close
-    }
-
-    @server.mount "/partial", Rack::Handler::WEBrick,
-    Rack::Lint.new(lambda{ |req|
-      [
-        200,
-        [ [ "rack.hijack", io_lambda ] ],
-        [""]
-      ]
-    })
-
-    Net::HTTP.start(@host, @port){ |http|
-      res = http.get("/partial")
-      res.body.must_equal "David\r\nDavid\r\nDavid\r\nDavid\r\nDavid\r\n"
-    }
-  end
-
-  it "produce correct HTTP semantics with and without app chunking" do
-    @server.mount "/chunked", Rack::Handler::WEBrick,
-    Rack::Lint.new(lambda{ |req|
-      [
-        200,
-        { "Transfer-Encoding" => "chunked" },
-        ["7\r\nchunked\r\n0\r\n\r\n"]
-      ]
-    })
-
-    Net::HTTP.start(@host, @port){ |http|
-      res = http.get("/chunked")
-      res["Transfer-Encoding"].must_equal "chunked"
-      res["Content-Length"].must_be_nil
-      res.body.must_equal "chunked"
-    }
-  end
-
-  after do
-    @status_thread.join
-    @server.shutdown
-    @thread.join
-  end
-end
diff --git a/test/testrequest.rb b/test/testrequest.rb
deleted file mode 100644
index b85aae8312dafedbfa6a64b48a84b8cdb8661b06..0000000000000000000000000000000000000000
--- a/test/testrequest.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# frozen_string_literal: true
-
-require 'yaml'
-require_relative 'psych_fix'
-require 'net/http'
-require 'rack/lint'
-
-class TestRequest
-  NOSERIALIZE = [Method, Proc, Rack::Lint::InputWrapper]
-
-  def call(env)
-    status = env["QUERY_STRING"] =~ /secret/ ? 403 : 200
-    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) } }
-    body = minienv.to_yaml
-    size = body.bytesize
-    [status, { "Content-Type" => "text/yaml", "Content-Length" => size.to_s }, [body]]
-  end
-
-  module Helpers
-    attr_reader :status, :response
-
-    ROOT = File.expand_path(File.dirname(__FILE__) + "/..")
-    ENV["RUBYOPT"] = "-I#{ROOT}/lib -rubygems"
-
-    def root
-      ROOT
-    end
-
-    def rackup
-      "#{ROOT}/bin/rackup"
-    end
-
-    def GET(path, header = {})
-      Net::HTTP.start(@host, @port) { |http|
-        user = header.delete(:user)
-        passwd = header.delete(:passwd)
-
-        get = Net::HTTP::Get.new(path, header)
-        get.basic_auth user, passwd  if user && passwd
-        http.request(get) { |response|
-          @status = response.code.to_i
-          begin
-            @response = YAML.unsafe_load(response.body)
-          rescue TypeError, ArgumentError
-            @response = nil
-          end
-        }
-      }
-    end
-
-    def POST(path, formdata = {}, header = {})
-      Net::HTTP.start(@host, @port) { |http|
-        user = header.delete(:user)
-        passwd = header.delete(:passwd)
-
-        post = Net::HTTP::Post.new(path, header)
-        post.form_data = formdata
-        post.basic_auth user, passwd  if user && passwd
-        http.request(post) { |response|
-          @status = response.code.to_i
-          @response = YAML.unsafe_load(response.body)
-        }
-      }
-    end
-  end
-end
-
-class StreamingRequest
-  def self.call(env)
-    [200, { "Content-Type" => "text/plain" }, new]
-  end
-
-  def each
-    yield "hello there!\n"
-    sleep 5
-    yield "that is all.\n"
-  end
-end
diff --git a/test/unregistered_handler/rack/handler/unregistered.rb b/test/unregistered_handler/rack/handler/unregistered.rb
deleted file mode 100644
index e98468cc6669fa05d1106ec6d5e5b09229e3ea74..0000000000000000000000000000000000000000
--- a/test/unregistered_handler/rack/handler/unregistered.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  module Handler
-    # this class doesn't do anything, we're just seeing if we get it.
-    class Unregistered
-    end
-  end
-end
diff --git a/test/unregistered_handler/rack/handler/unregistered_long_one.rb b/test/unregistered_handler/rack/handler/unregistered_long_one.rb
deleted file mode 100644
index 87c6c25431f90fa5a5128c14fcf178682be1509f..0000000000000000000000000000000000000000
--- a/test/unregistered_handler/rack/handler/unregistered_long_one.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Rack
-  module Handler
-    # this class doesn't do anything, we're just seeing if we get it.
-    class UnregisteredLongOne
-    end
-  end
-end