summary refs log tree commit
diff options
context:
space:
mode:
-rw-r--r--README.rdoc3
-rw-r--r--lib/rack/common_logger.rb7
-rw-r--r--lib/rack/deflater.rb31
-rw-r--r--lib/rack/lock.rb15
-rw-r--r--lib/rack/multipart/parser.rb16
-rw-r--r--lib/rack/utils.rb6
-rw-r--r--rack.gemspec4
-rw-r--r--test/spec_deflater.rb43
-rw-r--r--test/spec_lock.rb12
9 files changed, 103 insertions, 34 deletions
diff --git a/README.rdoc b/README.rdoc
index d87bc82e..8c1e2f01 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -41,6 +41,7 @@ These frameworks include Rack adapters in their distributions:
 * Coset
 * Espresso
 * Halcyon
+* Hanami
 * Mack
 * Maveric
 * Merb
@@ -296,7 +297,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 == Links
 
-Rack:: <http://rack.github.io/>
+Rack:: <https://rack.github.io/>
 Official Rack repositories:: <https://github.com/rack>
 Rack Bug Tracking:: <https://github.com/rack/rack/issues>
 rack-devel mailing list:: <https://groups.google.com/group/rack-devel>
diff --git a/lib/rack/common_logger.rb b/lib/rack/common_logger.rb
index ae410430..7855f0c3 100644
--- a/lib/rack/common_logger.rb
+++ b/lib/rack/common_logger.rb
@@ -29,7 +29,7 @@ module Rack
     end
 
     def call(env)
-      began_at = Time.now
+      began_at = Utils.clock_time
       status, header, body = @app.call(env)
       header = Utils::HeaderHash.new(header)
       body = BodyProxy.new(body) { log(env, status, header, began_at) }
@@ -39,20 +39,19 @@ module Rack
     private
 
     def log(env, status, header, began_at)
-      now = Time.now
       length = extract_content_length(header)
 
       msg = FORMAT % [
         env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
         env["REMOTE_USER"] || "-",
-        now.strftime("%d/%b/%Y:%H:%M:%S %z"),
+        Time.now.strftime("%d/%b/%Y:%H:%M:%S %z"),
         env[REQUEST_METHOD],
         env[PATH_INFO],
         env[QUERY_STRING].empty? ? "" : "?#{env[QUERY_STRING]}",
         env[HTTP_VERSION],
         status.to_s[0..3],
         length,
-        now - began_at ]
+        Utils.clock_time - began_at ]
 
       logger = @logger || env[RACK_ERRORS]
       # Standard library logger doesn't support write but it supports << which actually
diff --git a/lib/rack/deflater.rb b/lib/rack/deflater.rb
index 46d5b20a..abea9dec 100644
--- a/lib/rack/deflater.rb
+++ b/lib/rack/deflater.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
 require "zlib"
 require "time"  # for Time.httpdate
 require 'rack/utils'
@@ -23,11 +24,16 @@ module Rack
     #           'if' - a lambda enabling / disabling deflation based on returned boolean value
     #                  e.g use Rack::Deflater, :if => lambda { |env, status, headers, body| body.map(&:bytesize).reduce(0, :+) > 512 }
     #           'include' - a list of content types that should be compressed
+    #           'sync' - determines if the stream is going to be flused after every chunk.
+    #                    Flushing after every chunk reduces latency for
+    #                    time-sensitive streaming applications, but hurts
+    #                    compression and throughput. Defaults to `true'.
     def initialize(app, options = {})
       @app = app
 
       @condition = options[:if]
       @compressible_types = options[:include]
+      @sync = options[:sync] == false ? false : true
     end
 
     def call(env)
@@ -52,33 +58,33 @@ module Rack
       case encoding
       when "gzip"
         headers['Content-Encoding'] = "gzip"
-        headers.delete(CONTENT_LENGTH)
-        mtime = headers.key?("Last-Modified") ?
-          Time.httpdate(headers["Last-Modified"]) : Time.now
-        [status, headers, GzipStream.new(body, mtime)]
+        headers.delete('Content-Length')
+        mtime = headers["Last-Modified"]
+        mtime = Time.httpdate(mtime).to_i if mtime
+        [status, headers, GzipStream.new(body, mtime, @sync)]
       when "identity"
         [status, headers, body]
       when nil
         message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
         bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
-        [406, {CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s}, bp]
+        [406, {'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s}, bp]
       end
     end
 
     class GzipStream
-      def initialize(body, mtime)
+      def initialize(body, mtime, sync)
+        @sync = sync
         @body = body
         @mtime = mtime
-        @closed = false
       end
 
       def each(&block)
         @writer = block
         gzip  =::Zlib::GzipWriter.new(self)
-        gzip.mtime = @mtime
+        gzip.mtime = @mtime if @mtime
         @body.each { |part|
           gzip.write(part)
-          gzip.flush
+          gzip.flush if @sync
         }
       ensure
         gzip.close
@@ -90,9 +96,8 @@ module Rack
       end
 
       def close
-        return if @closed
-        @closed = true
         @body.close if @body.respond_to?(:close)
+        @body = nil
       end
     end
 
@@ -102,13 +107,13 @@ module Rack
       # Skip compressing empty entity body responses and responses with
       # no-transform set.
       if Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status) ||
-          headers[CACHE_CONTROL].to_s =~ /\bno-transform\b/ ||
+          headers['Cache-Control'].to_s =~ /\bno-transform\b/ ||
          (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
         return false
       end
 
       # Skip if @compressible_types are given and does not include request's content type
-      return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
+      return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
 
       # Skip if @condition lambda is given and evaluates to false
       return false if @condition && !@condition.call(env, status, headers, body)
diff --git a/lib/rack/lock.rb b/lib/rack/lock.rb
index 923dca59..b5a41e8e 100644
--- a/lib/rack/lock.rb
+++ b/lib/rack/lock.rb
@@ -11,12 +11,21 @@ module Rack
 
     def call(env)
       @mutex.lock
+      @env = env
+      @old_rack_multithread = env[RACK_MULTITHREAD]
       begin
-        response = @app.call(env.merge(RACK_MULTITHREAD => false))
-        returned = response << BodyProxy.new(response.pop) { @mutex.unlock }
+        response = @app.call(env.merge!(RACK_MULTITHREAD => false))
+        returned = response << BodyProxy.new(response.pop) { unlock }
       ensure
-        @mutex.unlock unless returned
+        unlock unless returned
       end
     end
+
+    private
+
+    def unlock
+      @mutex.unlock
+      @env[RACK_MULTITHREAD] = @old_rack_multithread
+    end
   end
 end
diff --git a/lib/rack/multipart/parser.rb b/lib/rack/multipart/parser.rb
index e2e821ac..c02e26f6 100644
--- a/lib/rack/multipart/parser.rb
+++ b/lib/rack/multipart/parser.rb
@@ -5,7 +5,7 @@ module Rack
     class MultipartPartLimitError < Errno::EMFILE; end
 
     class Parser
-      BUFSIZE = 16384
+      BUFSIZE = 1_048_576
       TEXT_PLAIN = "text/plain"
       TEMPFILE_FACTORY = lambda { |filename, content_type|
         Tempfile.new(["RackMultipart", ::File.extname(filename.gsub("\0".freeze, '%00'.freeze))])
@@ -170,10 +170,10 @@ module Rack
         @query_parser   = query_parser
         @params         = query_parser.make_params
         @boundary       = "--#{boundary}"
-        @boundary_size  = @boundary.bytesize + EOL.size
         @bufsize        = bufsize
 
         @rx = /(?:#{EOL})?#{Regexp.quote(@boundary)}(#{EOL}|--)/n
+        @rx_max_size = EOL.size + @boundary.bytesize + [EOL.size, '--'.size].max
         @full_boundary = @boundary
         @end_boundary = @boundary + '--'
         @state = :FAST_FORWARD
@@ -263,15 +263,17 @@ module Rack
       end
 
       def handle_mime_body
-        if @buf =~ rx
+        if i = @buf.index(rx)
           # Save the rest.
-          if i = @buf.index(rx)
-            @collector.on_mime_body @mime_index, @buf.slice!(0, i)
-            @buf.slice!(0, 2) # Remove \r\n after the content
-          end
+          @collector.on_mime_body @mime_index, @buf.slice!(0, i)
+          @buf.slice!(0, 2) # Remove \r\n after the content
           @state = :CONSUME_TOKEN
           @mime_index += 1
         else
+          # Save the read body part.
+          if @rx_max_size < @buf.size
+            @collector.on_mime_body @mime_index, @buf.slice!(0, @buf.size - @rx_max_size)
+          end
           :want_read
         end
       end
diff --git a/lib/rack/utils.rb b/lib/rack/utils.rb
index 78aed4c0..d9128e3e 100644
--- a/lib/rack/utils.rb
+++ b/lib/rack/utils.rb
@@ -486,9 +486,9 @@ module Rack
 
     # Every standard HTTP code mapped to the appropriate message.
     # Generated with:
-    # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
-    #   ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
-    #             puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
+    #   curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
+    #     ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
+    #               puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
     HTTP_STATUS_CODES = {
       100 => 'Continue',
       101 => 'Switching Protocols',
diff --git a/rack.gemspec b/rack.gemspec
index d48b5f3a..ec2b79f6 100644
--- a/rack.gemspec
+++ b/rack.gemspec
@@ -12,7 +12,7 @@ the simplest way possible, it unifies and distills the API for web
 servers, web frameworks, and software in between (the so-called
 middleware) into a single method call.
 
-Also see http://rack.github.io/.
+Also see https://rack.github.io/.
 EOF
 
   s.files           = Dir['{bin/*,contrib/*,example/*,lib/**/*,test/**/*}'] +
@@ -25,7 +25,7 @@ EOF
 
   s.author          = 'Christian Neukirchen'
   s.email           = 'chneukirchen@gmail.com'
-  s.homepage        = 'http://rack.github.io/'
+  s.homepage        = 'https://rack.github.io/'
   s.required_ruby_version = '>= 2.2.2'
 
   s.add_development_dependency 'minitest', "~> 5.0"
diff --git a/test/spec_deflater.rb b/test/spec_deflater.rb
index 0f27c859..a5e91285 100644
--- a/test/spec_deflater.rb
+++ b/test/spec_deflater.rb
@@ -44,6 +44,8 @@ describe Rack::Deflater do
       [accept_encoding, accept_encoding.dup]
     end
 
+    start = Time.now.to_i
+
     # build response
     status, headers, body = build_response(
       options['app_status'] || expected_status,
@@ -67,6 +69,13 @@ describe Rack::Deflater do
       when 'gzip'
         io = StringIO.new(body_text)
         gz = Zlib::GzipReader.new(io)
+        mtime = gz.mtime.to_i
+        if last_mod = headers['Last-Modified']
+          Time.httpdate(last_mod).to_i.must_equal mtime
+        else
+          mtime.must_be(:<=, Time.now.to_i)
+          mtime.must_be(:>=, start.to_i)
+        end
         tmp = gz.read
         gz.close
         tmp
@@ -372,4 +381,38 @@ describe Rack::Deflater do
 
     verify(200, response, 'gzip', options)
   end
+
+  it 'will honor sync: false to avoid unnecessary flushing' do
+    app_body = Object.new
+    class << app_body
+      def each
+        (0..20).each { |i| yield "hello\n".freeze }
+      end
+    end
+
+    options = {
+      'deflater_options' => { :sync => false },
+      'app_body' => app_body,
+      'skip_body_verify' => true,
+    }
+    verify(200, app_body, deflate_or_gzip, options) do |status, headers, body|
+      headers.must_equal({
+        'Content-Encoding' => 'gzip',
+        'Vary' => 'Accept-Encoding',
+        'Content-Type' => 'text/plain'
+      })
+
+      buf = ''
+      raw_bytes = 0
+      inflater = auto_inflater
+      body.each do |part|
+        raw_bytes += part.bytesize
+        buf << inflater.inflate(part)
+      end
+      buf << inflater.finish
+      expect = "hello\n" * 21
+      buf.must_equal expect
+      raw_bytes.must_be(:<, expect.bytesize)
+    end
+  end
 end
diff --git a/test/spec_lock.rb b/test/spec_lock.rb
index aa3efa54..c6f7c05e 100644
--- a/test/spec_lock.rb
+++ b/test/spec_lock.rb
@@ -147,7 +147,8 @@ describe Rack::Lock do
     }, false)
     env = Rack::MockRequest.env_for("/")
     env['rack.multithread'].must_equal true
-    app.call(env)
+    _, _, body = app.call(env)
+    body.close
     env['rack.multithread'].must_equal true
   end
 
@@ -191,4 +192,13 @@ describe Rack::Lock do
     lambda { app.call(env) }.must_raise Exception
     lock.synchronized.must_equal false
   end
+
+  it "not replace the environment" do
+    env  = Rack::MockRequest.env_for("/")
+    app  = lock_app(lambda { |inner_env| [200, {"Content-Type" => "text/plain"}, [inner_env.object_id.to_s]] })
+
+    _, _, body = app.call(env)
+
+    body.to_enum.to_a.must_equal [env.object_id.to_s]
+  end
 end