summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--lib/rack/deflater.rb43
-rw-r--r--lib/rack/etag.rb2
-rw-r--r--lib/rack/multipart/parser.rb4
-rw-r--r--lib/rack/nulllogger.rb23
-rw-r--r--lib/rack/request.rb10
-rw-r--r--lib/rack/session/abstract/id.rb3
-rw-r--r--lib/rack/utils.rb14
-rw-r--r--test/multipart/ie116
-rw-r--r--test/spec_deflater.rb431
-rw-r--r--test/spec_etag.rb4
-rw-r--r--test/spec_multipart.rb22
-rw-r--r--test/spec_request.rb10
-rw-r--r--test/spec_utils.rb10
13 files changed, 382 insertions, 200 deletions
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index 2e55f97e..9df510bd 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -17,19 +17,26 @@ module Rack
   # directive of 'no-transform' is present, or when the response status
   # code is one that doesn't allow an entity body.
   class Deflater
-    def initialize(app)
+    ##
+    # Creates Rack::Deflater middleware.
+    #
+    # [app] rack app instance
+    # [options] hash of deflater options, i.e.
+    #           'if' - a lambda enabling / disabling deflation based on returned boolean value
+    #                  e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.length > 512 }
+    #           'include' - a list of content types that should be compressed
+    def initialize(app, options = {})
       @app = app
+
+      @condition = options[:if]
+      @compressible_types = options[:include]
     end
 
     def call(env)
       status, headers, body = @app.call(env)
       headers = Utils::HeaderHash.new(headers)
 
-      # Skip compressing empty entity body responses and responses with
-      # no-transform set.
-      if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
-          headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
-         (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
+      unless should_deflate?(env, status, headers, body)
         return [status, headers, body]
       end
 
@@ -58,9 +65,9 @@ module Rack
       when "identity"
         [status, headers, body]
       when nil
-        body.close if body.respond_to?(:close)
         message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
-        [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, [message]]
+        bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
+        [406, {"Content-Type" => "text/plain", "Content-Length" => message.length.to_s}, bp]
       end
     end
 
@@ -124,5 +131,25 @@ module Rack
         @body.close if @body.respond_to?(:close)
       end
     end
+
+    private
+
+    def should_deflate?(env, status, headers, body)
+      # Skip compressing empty entity body responses and responses with
+      # no-transform set.
+      if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
+          headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
+         (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
+        return false
+      end
+
+      # Skip if @compressible_types are given and does not include request's content type
+      return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
+
+      # Skip if @condition lambda is given and evaluates to false
+      return false if @condition && !@condition.call(env, status, headers, body)
+
+      true
+    end
   end
 end
diff --git a/lib/rack/etag.rb b/lib/rack/etag.rb
index 99a1a4c0..fefe671f 100644
--- a/lib/rack/etag.rb
+++ b/lib/rack/etag.rb
@@ -28,7 +28,7 @@ module Rack
         body = Rack::BodyProxy.new(new_body) do
           original_body.close if original_body.respond_to?(:close)
         end
-        headers['ETag'] = %("#{digest}") if digest
+        headers['ETag'] = %(W/"#{digest}") if digest
       end
 
       unless headers['Cache-Control']
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index 324c7d8c..22f9734b 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -116,10 +116,6 @@ module Rack
               name = filename
             end
 
-            if filename && filename.empty?
-              filename = name
-            end
-
             if filename
               (@env['rack.tempfiles'] ||= []) << body = Tempfile.new("RackMultipart")
               body.binmode  if body.respond_to?(:binmode)
diff --git a/lib/rack/nulllogger.rb b/lib/rack/nulllogger.rb
index 77fb637d..2d5a2c97 100644
--- a/lib/rack/nulllogger.rb
+++ b/lib/rack/nulllogger.rb
@@ -9,10 +9,29 @@ module Rack
       @app.call(env)
     end
 
-    def info(progname = nil, &block);  end
+    def info(progname = nil, &block); end
     def debug(progname = nil, &block); end
-    def warn(progname = nil, &block);  end
+    def warn(progname = nil, &block); end
     def error(progname = nil, &block); end
     def fatal(progname = nil, &block); end
+    def unknown(progname = nil, &block); end
+    def info? ;  end
+    def debug? ; end
+    def warn? ;  end
+    def error? ; end
+    def fatal? ; end
+    def level ; end
+    def progname ; end
+    def datetime_format ; end
+    def formatter ; end
+    def sev_threshold ; end
+    def level=(level); end
+    def progname=(progname); end
+    def datetime_format=(datetime_format); end
+    def formatter=(formatter); end
+    def sev_threshold=(sev_threshold); end
+    def close ; end
+    def add(severity, message = nil, progname = nil, &block); end
+    def <<(msg); end
   end
 end
diff --git a/lib/rack/request.rb b/lib/rack/request.rb
index 52ea652c..4f038384 100644
--- a/lib/rack/request.rb
+++ b/lib/rack/request.rb
@@ -148,7 +148,8 @@ module Rack
     # for form-data / param parsing.
     FORM_DATA_MEDIA_TYPES = [
       'application/x-www-form-urlencoded',
-      'multipart/form-data'
+      'multipart/form-data',
+      'application/json'
     ]
 
     # The set of media-types. Requests that do not indicate
@@ -366,7 +367,12 @@ module Rack
       end
 
       def parse_query(qs)
-        Utils.parse_nested_query(qs, '&')
+        case media_type
+        when 'application/json'
+          (qs && qs != '') ? ::Rack::Utils::OkJson.decode(qs) : {}
+        else
+          Utils.parse_nested_query(qs, '&')
+        end
       end
 
       def parse_multipart(env)
diff --git a/lib/rack/session/abstract/id.rb b/lib/rack/session/abstract/id.rb
index e9edeb7f..62bdb04b 100644
--- a/lib/rack/session/abstract/id.rb
+++ b/lib/rack/session/abstract/id.rb
@@ -310,7 +310,7 @@ module Rack
         end
 
         def force_options?(options)
-          options.values_at(:renew, :drop, :defer, :expire_after).any?
+          options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
         end
 
         def security_matches?(env, options)
@@ -347,6 +347,7 @@ module Rack
             cookie = Hash.new
             cookie[:value] = data
             cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
+            cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
             set_cookie(env, headers, cookie.merge!(options))
           end
 
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 53303995..42b2f5a0 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -178,12 +178,12 @@ module Rack
       when Hash
         value.map { |k, v|
           build_nested_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
-        }.join("&")
-      when String
+        }.reject(&:empty?).join('&')
+      when nil
+        prefix
+      else
         raise ArgumentError, "value must be a Hash" if prefix.nil?
         "#{prefix}=#{escape(value)}"
-      else
-        prefix
       end
     end
     module_function :build_nested_query
@@ -267,9 +267,9 @@ module Rack
     def set_cookie_header!(header, key, value)
       case value
       when Hash
-        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]
+        domain  = "; domain="  + value[:domain]       if value[:domain]
+        path    = "; path="    + value[:path]         if value[:path]
+        max_age = "; max-age=" + value[:max_age].to_s if value[:max_age]
         # There is an RFC mess in the area of date formatting for Cookies. Not
         # only are there contradicting RFCs and examples within RFC text, but
         # there are also numerous conflicting names of fields and partially
diff --git a/test/multipart/ie11 b/test/multipart/ie11
deleted file mode 100644
index 6c3da47f..00000000
--- a/test/multipart/ie11
+++ /dev/null
@@ -1,6 +0,0 @@
---AaB03x
-Content-Disposition: form-data; name="files"; filename=""
-Content-Type: text/plain
-
-contents
---AaB03x--
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 6f5137ca..1e921eff 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -6,199 +6,334 @@ require 'rack/mock'
 require 'zlib'
 
 describe Rack::Deflater do
-  def deflater(app)
-    Rack::Lint.new Rack::Deflater.new(app)
-  end
 
-  def build_response(status, body, accept_encoding, headers = {})
-    body = [body]  if body.respond_to? :to_str
+  def build_response(status, body, accept_encoding, options = {})
+    body = [body] if body.respond_to? :to_str
     app = lambda do |env|
-      res = [status, {}, body]
-      res[1]["Content-Type"] = "text/plain" unless res[0] == 304
+      res = [status, options['response_headers'] || {}, body]
+      res[1]['Content-Type'] = 'text/plain' unless res[0] == 304
       res
     end
-    request = Rack::MockRequest.env_for("", headers.merge("HTTP_ACCEPT_ENCODING" => accept_encoding))
-    response = deflater(app).call(request)
 
-    return response
-  end
+    request = Rack::MockRequest.env_for('', (options['request_headers'] || {}).merge('HTTP_ACCEPT_ENCODING' => accept_encoding))
+    deflater = Rack::Lint.new Rack::Deflater.new(app, options['deflater_options'] || {})
 
-  def inflate(buf)
-    inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
-    inflater.inflate(buf) << inflater.finish
+    deflater.call(request)
   end
 
-  should "be able to deflate bodies that respond to each" do
-    body = Object.new
-    class << body; def each; yield("foo"); yield("bar"); end; end
-
-    response = build_response(200, body, "deflate")
-
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "deflate",
-      "Vary" => "Accept-Encoding",
-      "Content-Type" => "text/plain"
-    })
-    buf = ''
-    response[2].each { |part| buf << part }
-    inflate(buf).should.equal("foobar")
-  end
-
-  should "flush deflated chunks to the client as they become ready" do
-    body = Object.new
-    class << body; def each; yield("foo"); yield("bar"); end; end
+  ##
+  # Constructs response object and verifies if it yields right results
+  #
+  # [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.
+  #                  '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.
+  #           'app_status' - what status dummy app should return (may be changed by deflater at some point)
+  #           'app_body' - what body dummy app should return (may be changed by deflater at some point)
+  #           'request_headers' - extra reqest headers to be sent
+  #           'response_headers' - extra response headers to be returned
+  #           'deflater_options' - options passed to deflater middleware
+  # [block] useful for doing some extra verification
+  def verify(expected_status, expected_body, accept_encoding, options = {}, &block)
+    accept_encoding, expected_encoding = if accept_encoding.kind_of?(Hash)
+      [accept_encoding.keys.first, accept_encoding.values.first]
+    else
+      [accept_encoding, accept_encoding.dup]
+    end
 
-    response = build_response(200, body, "deflate")
+    # build response
+    status, headers, body = build_response(
+      options['app_status'] || expected_status,
+      options['app_body'] || expected_body,
+      accept_encoding,
+      options
+    )
+
+    # verify status
+    status.should.equal(expected_status)
+
+    # verify body
+    unless options['skip_body_verify']
+      body_text = ''
+      body.each { |part| body_text << part }
+
+      deflated_body = case expected_encoding
+      when 'deflate'
+        inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+        inflater.inflate(body_text) << inflater.finish
+      when 'gzip'
+        io = StringIO.new(body_text)
+        gz = Zlib::GzipReader.new(io)
+        tmp = gz.read
+        gz.close
+        tmp
+      else
+        body_text
+      end
+
+      deflated_body.should.equal(expected_body)
+    end
 
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "deflate",
-      "Vary" => "Accept-Encoding",
-      "Content-Type" => "text/plain"
-    })
-    buf = []
-    inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
-    response[2].each { |part| buf << inflater.inflate(part) }
-    buf << inflater.finish
-    buf.delete_if { |part| part.empty? }
-    buf.join.should.equal("foobar")
+    # yield full response verification
+    yield(status, headers, body) if block_given?
   end
 
-  # TODO: This is really just a special case of the above...
-  should "be able to deflate String bodies" do
-    response = build_response(200, "Hello world!", "deflate")
+  should 'be able to deflate bodies that respond to each' do
+    app_body = Object.new
+    class << app_body; def each; yield('foo'); yield('bar'); end; end
 
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "deflate",
-      "Vary" => "Accept-Encoding",
-      "Content-Type" => "text/plain"
-    })
-    buf = ''
-    response[2].each { |part| buf << part }
-    inflate(buf).should.equal("Hello world!")
+    verify(200, 'foobar', 'deflate', { 'app_body' => app_body }) do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'deflate',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+    end
   end
 
-  should "be able to gzip bodies that respond to each" do
-    body = Object.new
-    class << body; def each; yield("foo"); yield("bar"); end; end
+  should 'flush deflated chunks to the client as they become ready' do
+    app_body = Object.new
+    class << app_body; def each; yield('foo'); yield('bar'); end; end
 
-    response = build_response(200, body, "gzip")
+    verify(200, app_body, 'deflate', { 'skip_body_verify' => true }) do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'deflate',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
 
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "gzip",
-      "Vary" => "Accept-Encoding",
-      "Content-Type" => "text/plain"
-    })
+      buf = []
+      inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
+      body.each { |part| buf << inflater.inflate(part) }
+      buf << inflater.finish
 
-    buf = ''
-    response[2].each { |part| buf << part }
-    io = StringIO.new(buf)
-    gz = Zlib::GzipReader.new(io)
-    gz.read.should.equal("foobar")
-    gz.close
+      buf.delete_if { |part| part.empty? }.join.should.equal('foobar')
+    end
   end
 
-  should "flush gzipped chunks to the client as they become ready" do
-    body = Object.new
-    class << body; def each; yield("foo"); yield("bar"); end; end
+  # TODO: This is really just a special case of the above...
+  should 'be able to deflate String bodies' do
+    verify(200, 'Hello world!', 'deflate') do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'deflate',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+    end
+  end
 
-    response = build_response(200, body, "gzip")
+  should 'be able to gzip bodies that respond to each' do
+    app_body = Object.new
+    class << app_body; def each; yield('foo'); yield('bar'); end; end
 
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "gzip",
-      "Vary" => "Accept-Encoding",
-      "Content-Type" => "text/plain"
-    })
-    buf = []
-    inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
-    response[2].each { |part| buf << inflater.inflate(part) }
-    buf << inflater.finish
-    buf.delete_if { |part| part.empty? }
-    buf.join.should.equal("foobar")
+    verify(200, 'foobar', 'gzip', { 'app_body' => app_body }) do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+    end
   end
 
-  should "be able to fallback to no deflation" do
-    response = build_response(200, "Hello world!", "superzip")
+  should 'flush gzipped chunks to the client as they become ready' do
+    app_body = Object.new
+    class << app_body; def each; yield('foo'); yield('bar'); end; end
+
+    verify(200, app_body, 'gzip', { 'skip_body_verify' => true }) do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+
+      buf = []
+      inflater = Zlib::Inflate.new(Zlib::MAX_WBITS + 32)
+      body.each { |part| buf << inflater.inflate(part) }
+      buf << inflater.finish
 
-    response[0].should.equal(200)
-    response[1].should.equal({ "Vary" => "Accept-Encoding", "Content-Type" => "text/plain" })
-    response[2].to_enum.to_a.should.equal(["Hello world!"])
+      buf.delete_if { |part| part.empty? }.join.should.equal('foobar')
+    end
   end
 
-  should "be able to skip when there is no response entity body" do
-    response = build_response(304, [], "gzip")
+  should 'be able to fallback to no deflation' do
+    verify(200, 'Hello world!', 'superzip') do |status, headers, body|
+      headers.should.equal({
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+    end
+  end
 
-    response[0].should.equal(304)
-    response[1].should.equal({})
-    response[2].to_enum.to_a.should.equal([])
+  should 'be able to skip when there is no response entity body' do
+    verify(304, '', { 'gzip' => nil }, { 'app_body' => [] }) do |status, headers, body|
+      headers.should.equal({})
+    end
   end
 
-  should "handle the lack of an acceptable encoding" do
-    response1 = build_response(200, "Hello world!", "identity;q=0", "PATH_INFO" => "/")
-    response1[0].should.equal(406)
-    response1[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "71"})
-    response1[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource / could not be found."])
+  should 'handle the lack of an acceptable encoding' do
+    app_body = 'Hello world!'
+    not_found_body1 = 'An acceptable encoding for the requested resource / could not be found.'
+    not_found_body2 = 'An acceptable encoding for the requested resource /foo/bar could not be found.'
+    options1 = {
+      'app_status' => 200,
+      'app_body' => app_body,
+      'request_headers' => {
+        'PATH_INFO' => '/'
+      }
+    }
+    options2 = {
+      'app_status' => 200,
+      'app_body' => app_body,
+      'request_headers' => {
+        'PATH_INFO' => '/foo/bar'
+      }
+    }
+
+    verify(406, not_found_body1, 'identity;q=0', options1) do |status, headers, body|
+      headers.should.equal({
+        'Content-Type' => 'text/plain',
+        'Content-Length' => not_found_body1.length.to_s
+      })
+    end
 
-    response2 = build_response(200, "Hello world!", "identity;q=0", "SCRIPT_NAME" => "/foo", "PATH_INFO" => "/bar")
-    response2[0].should.equal(406)
-    response2[1].should.equal({"Content-Type" => "text/plain", "Content-Length" => "78"})
-    response2[2].to_enum.to_a.should.equal(["An acceptable encoding for the requested resource /foo/bar could not be found."])
+    verify(406, not_found_body2, 'identity;q=0', options2) do |status, headers, body|
+      headers.should.equal({
+        'Content-Type' => 'text/plain',
+        'Content-Length' => not_found_body2.length.to_s
+      })
+    end
   end
 
-  should "handle gzip response with Last-Modified header" do
+  should 'handle gzip response with Last-Modified header' do
     last_modified = Time.now.httpdate
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain',
+        'Last-Modified' => last_modified
+      }
+    }
+
+    verify(200, 'Hello World!', 'gzip', options) do |status, headers, body|
+      headers.should.equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Last-Modified' => last_modified,
+        'Content-Type' => 'text/plain'
+      })
+    end
+  end
 
-    app = lambda { |env| [200, { "Content-Type" => "text/plain", "Last-Modified" => last_modified }, ["Hello World!"]] }
-    request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
-    response = deflater(app).call(request)
+  should 'do nothing when no-transform Cache-Control directive present' do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain',
+        'Cache-Control' => 'no-transform'
+      }
+    }
+    verify(200, 'Hello World!', { 'gzip' => nil }, options) do |status, headers, body|
+      headers.should.not.include 'Content-Encoding'
+    end
+  end
+
+  should 'do nothing when Content-Encoding already present' do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain',
+        'Content-Encoding' => 'gzip'
+      }
+    }
+    verify(200, 'Hello World!', { 'gzip' => nil }, options)
+  end
 
-    response[0].should.equal(200)
-    response[1].should.equal({
-      "Content-Encoding" => "gzip",
-      "Vary" => "Accept-Encoding",
-      "Last-Modified" => last_modified,
-      "Content-Type" => "text/plain"
-    })
+  should 'deflate when Content-Encoding is identity' do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain',
+        'Content-Encoding' => 'identity'
+      }
+    }
+    verify(200, 'Hello World!', 'deflate', options)
+  end
 
-    buf = ''
-    response[2].each { |part| buf << part }
-    io = StringIO.new(buf)
-    gz = Zlib::GzipReader.new(io)
-    gz.read.should.equal("Hello World!")
-    gz.close
+  should "deflate if content-type matches :include" do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain'
+      },
+      'deflater_options' => {
+        :include => %w(text/plain)
+      }
+    }
+    verify(200, 'Hello World!', 'gzip', options)
   end
 
-  should "do nothing when no-transform Cache-Control directive present" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Cache-Control' => 'no-transform'}, ['Hello World!']] }
-    request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
-    response = deflater(app).call(request)
+  should "deflate if content-type is included it :include" do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain; charset=us-ascii'
+      },
+      'deflater_options' => {
+        :include => %w(text/plain)
+      }
+    }
+    verify(200, 'Hello World!', 'gzip', options)
+  end
 
-    response[0].should.equal(200)
-    response[1].should.not.include "Content-Encoding"
-    response[2].to_enum.to_a.join.should.equal("Hello World!")
+  should "not deflate if content-type is not set but given in :include" do
+    options = {
+      'deflater_options' => {
+        :include => %w(text/plain)
+      }
+    }
+    verify(304, 'Hello World!', { 'gzip' => nil }, options)
   end
 
-  should "do nothing when Content-Encoding already present" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Encoding' => 'gzip'}, ['Hello World!']] }
-    request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "gzip")
-    response = deflater(app).call(request)
+  should "not deflate if content-type do not match :include" do
+    options = {
+      'response_headers' => {
+        'Content-Type' => 'text/plain'
+      },
+      'deflater_options' => {
+        :include => %w(text/json)
+      }
+    }
+    verify(200, 'Hello World!', { 'gzip' => nil }, options)
+  end
 
-    response[0].should.equal(200)
-    response[2].to_enum.to_a.join.should.equal("Hello World!")
+  should "deflate response if :if lambda evaluates to true" do
+    options = {
+      'deflater_options' => {
+        :if => lambda { |env, status, headers, body| true }
+      }
+    }
+    verify(200, 'Hello World!', 'deflate', options)
   end
 
-  should "deflate when Content-Encoding is identity" do
-    app = lambda { |env| [200, {'Content-Type' => 'text/plain', 'Content-Encoding' => 'identity'}, ['Hello World!']] }
-    request = Rack::MockRequest.env_for("", "HTTP_ACCEPT_ENCODING" => "deflate")
-    response = deflater(app).call(request)
+  should "not deflate if :if lambda evaluates to false" do
+    options = {
+      'deflater_options' => {
+        :if => lambda { |env, status, headers, body| false }
+      }
+    }
+    verify(200, 'Hello World!', { 'gzip' => nil }, options)
+  end
 
-    response[0].should.equal(200)
-    buf = ''
-    response[2].each { |part| buf << part }
-    inflate(buf).should.equal("Hello World!")
+  should "check for Content-Length via :if" do
+    body = 'Hello World!'
+    body_len = body.length
+    options = {
+      'response_headers' => {
+        'Content-Length' => body_len.to_s
+      },
+      'deflater_options' => {
+        :if => lambda { |env, status, headers, body|
+          headers['Content-Length'].to_i >= body_len
+        }
+      }
+    }
+
+    verify(200, body, 'gzip', options)
   end
 end
diff --git a/test/spec_etag.rb b/test/spec_etag.rb
index b8b8b637..c075d9d0 100644
--- a/test/spec_etag.rb
+++ b/test/spec_etag.rb
@@ -21,13 +21,13 @@ describe Rack::ETag do
   should "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'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
+    response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
   end
 
   should "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'].should.equal "\"65a8e27d8879283831b664bd8b7f0ad4\""
+    response[1]['ETag'].should.equal "W/\"65a8e27d8879283831b664bd8b7f0ad4\""
   end
 
   should "set Cache-Control to 'max-age=0, private, must-revalidate' (default) if none is set" do
diff --git a/test/spec_multipart.rb b/test/spec_multipart.rb
index 959a5cff..2acb6e0d 100644
--- a/test/spec_multipart.rb
+++ b/test/spec_multipart.rb
@@ -230,15 +230,12 @@ describe Rack::Multipart do
     params["files"][:tempfile].read.should.equal "contents"
   end
 
-  # n.b. this case used to be "do not include", but because ie11 now does this
-  # for selected files, that can no longer be done. It was decided that not
-  # losing data is better, and no browser is documented with this behavior.
-  should "include file params if no file was selected" do
+  should "not include file params if no file was selected" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:none))
     params = Rack::Multipart.parse_multipart(env)
     params["submit-name"].should.equal "Larry"
-    params["files"].should.not.equal nil
-    params.keys.should.include "files"
+    params["files"].should.equal nil
+    params.keys.should.not.include "files"
   end
 
   should "parse multipart/mixed" do
@@ -270,19 +267,6 @@ describe Rack::Multipart do
     params["files"][:tempfile].read.should.equal "contents"
   end
 
-  should "parse IE11 multipart upload" do
-    env = Rack::MockRequest.env_for("/", multipart_fixture(:ie11))
-    params = Rack::Multipart.parse_multipart(env)
-    params["files"][:type].should.equal "text/plain"
-    params["files"][:filename].should.equal "files"
-    params["files"][:head].should.equal "Content-Disposition: form-data; " +
-      "name=\"files\"; " +
-      'filename=""' +
-      "\r\nContent-Type: text/plain\r\n"
-    params["files"][:name].should.equal "files"
-    params["files"][:tempfile].read.should.equal "contents"
-  end
-
   should "parse filename and modification param" do
     env = Rack::MockRequest.env_for("/", multipart_fixture(:filename_and_modification_param))
     params = Rack::Multipart.parse_multipart(env)
diff --git a/test/spec_request.rb b/test/spec_request.rb
index 8a2b4760..c4a7400a 100644
--- a/test/spec_request.rb
+++ b/test/spec_request.rb
@@ -267,6 +267,16 @@ describe Rack::Request do
     input.read.should.equal "foo=bar&quux=bla"
   end
 
+  should "accept application/json content type" do
+    input = StringIO.new('{"foo":"bar","quxx":"bla"}')
+    req = Rack::Request.new \
+      Rack::MockRequest.env_for("/",
+        "CONTENT_TYPE" => 'application/json',
+        :input => input)
+    req.params.should.equal "foo" => "bar", "quxx" => "bla"
+    input.read.should.equal '{"foo":"bar","quxx":"bla"}'
+  end
+
   should "clean up Safari's ajax POST body" do
     req = Rack::Request.new \
       Rack::MockRequest.env_for("/",
diff --git a/test/spec_utils.rb b/test/spec_utils.rb
index 4b989db7..06ed5636 100644
--- a/test/spec_utils.rb
+++ b/test/spec_utils.rb
@@ -248,6 +248,8 @@ describe Rack::Utils do
 
     Rack::Utils.build_nested_query("foo" => "1", "bar" => "2").
       should.be equal_query_to("foo=1&bar=2")
+    Rack::Utils.build_nested_query("foo" => 1, "bar" => 2).
+      should.be equal_query_to("foo=1&bar=2")
     Rack::Utils.build_nested_query("my weird field" => "q1!2\"'w$5&7/z8)?").
       should.be equal_query_to("my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F")
 
@@ -257,6 +259,14 @@ describe Rack::Utils do
       should.equal "foo[]="
     Rack::Utils.build_nested_query("foo" => ["bar"]).
       should.equal "foo[]=bar"
+    Rack::Utils.build_nested_query('foo' => []).
+      should.equal ''
+    Rack::Utils.build_nested_query('foo' => {}).
+      should.equal ''
+    Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => []).
+      should.equal 'foo=bar'
+    Rack::Utils.build_nested_query('foo' => 'bar', 'baz' => {}).
+      should.equal 'foo=bar'
 
     # The ordering of the output query string is unpredictable with 1.8's
     # unordered hash. Test that build_nested_query performs the inverse