rack 2.1.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +377 -16
  3. data/CONTRIBUTING.md +144 -0
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +328 -0
  6. data/SPEC.rdoc +365 -0
  7. data/lib/rack/auth/abstract/handler.rb +3 -1
  8. data/lib/rack/auth/abstract/request.rb +2 -2
  9. data/lib/rack/auth/basic.rb +4 -7
  10. data/lib/rack/bad_request.rb +8 -0
  11. data/lib/rack/body_proxy.rb +34 -12
  12. data/lib/rack/builder.rb +162 -59
  13. data/lib/rack/cascade.rb +24 -10
  14. data/lib/rack/common_logger.rb +43 -28
  15. data/lib/rack/conditional_get.rb +30 -25
  16. data/lib/rack/constants.rb +66 -0
  17. data/lib/rack/content_length.rb +10 -16
  18. data/lib/rack/content_type.rb +9 -7
  19. data/lib/rack/deflater.rb +78 -50
  20. data/lib/rack/directory.rb +86 -63
  21. data/lib/rack/etag.rb +14 -22
  22. data/lib/rack/events.rb +18 -17
  23. data/lib/rack/files.rb +99 -61
  24. data/lib/rack/head.rb +8 -9
  25. data/lib/rack/headers.rb +238 -0
  26. data/lib/rack/lint.rb +868 -642
  27. data/lib/rack/lock.rb +2 -6
  28. data/lib/rack/logger.rb +3 -0
  29. data/lib/rack/media_type.rb +9 -4
  30. data/lib/rack/method_override.rb +6 -2
  31. data/lib/rack/mime.rb +14 -5
  32. data/lib/rack/mock.rb +1 -253
  33. data/lib/rack/mock_request.rb +171 -0
  34. data/lib/rack/mock_response.rb +124 -0
  35. data/lib/rack/multipart/generator.rb +15 -8
  36. data/lib/rack/multipart/parser.rb +238 -107
  37. data/lib/rack/multipart/uploaded_file.rb +17 -7
  38. data/lib/rack/multipart.rb +54 -42
  39. data/lib/rack/null_logger.rb +9 -0
  40. data/lib/rack/query_parser.rb +87 -105
  41. data/lib/rack/recursive.rb +3 -1
  42. data/lib/rack/reloader.rb +0 -4
  43. data/lib/rack/request.rb +366 -135
  44. data/lib/rack/response.rb +186 -68
  45. data/lib/rack/rewindable_input.rb +24 -6
  46. data/lib/rack/runtime.rb +8 -7
  47. data/lib/rack/sendfile.rb +29 -27
  48. data/lib/rack/show_exceptions.rb +27 -12
  49. data/lib/rack/show_status.rb +21 -13
  50. data/lib/rack/static.rb +19 -12
  51. data/lib/rack/tempfile_reaper.rb +14 -5
  52. data/lib/rack/urlmap.rb +5 -6
  53. data/lib/rack/utils.rb +274 -260
  54. data/lib/rack/version.rb +21 -0
  55. data/lib/rack.rb +18 -103
  56. metadata +25 -52
  57. data/README.rdoc +0 -262
  58. data/Rakefile +0 -123
  59. data/SPEC +0 -263
  60. data/bin/rackup +0 -5
  61. data/contrib/rack.png +0 -0
  62. data/contrib/rack.svg +0 -150
  63. data/contrib/rack_logo.svg +0 -164
  64. data/contrib/rdoc.css +0 -412
  65. data/example/lobster.ru +0 -6
  66. data/example/protectedlobster.rb +0 -16
  67. data/example/protectedlobster.ru +0 -10
  68. data/lib/rack/auth/digest/md5.rb +0 -131
  69. data/lib/rack/auth/digest/nonce.rb +0 -54
  70. data/lib/rack/auth/digest/params.rb +0 -54
  71. data/lib/rack/auth/digest/request.rb +0 -43
  72. data/lib/rack/chunked.rb +0 -92
  73. data/lib/rack/core_ext/regexp.rb +0 -14
  74. data/lib/rack/file.rb +0 -8
  75. data/lib/rack/handler/cgi.rb +0 -62
  76. data/lib/rack/handler/fastcgi.rb +0 -102
  77. data/lib/rack/handler/lsws.rb +0 -63
  78. data/lib/rack/handler/scgi.rb +0 -73
  79. data/lib/rack/handler/thin.rb +0 -38
  80. data/lib/rack/handler/webrick.rb +0 -122
  81. data/lib/rack/handler.rb +0 -104
  82. data/lib/rack/lobster.rb +0 -72
  83. data/lib/rack/server.rb +0 -467
  84. data/lib/rack/session/abstract/id.rb +0 -528
  85. data/lib/rack/session/cookie.rb +0 -205
  86. data/lib/rack/session/memcache.rb +0 -10
  87. data/lib/rack/session/pool.rb +0 -85
  88. data/rack.gemspec +0 -44
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
3
+ require_relative 'constants'
4
+ require_relative 'utils'
5
+ require_relative 'body_proxy'
4
6
 
5
7
  module Rack
6
8
 
7
- # Middleware that enables conditional GET using If-None-Match and
8
- # If-Modified-Since. The application should set either or both of the
9
- # Last-Modified or Etag response headers according to RFC 2616. When
9
+ # Middleware that enables conditional GET using if-none-match and
10
+ # if-modified-since. The application should set either or both of the
11
+ # last-modified or etag response headers according to RFC 2616. When
10
12
  # either of the conditions is met, the response body is set to be zero
11
13
  # length and the response status is set to 304 Not Modified.
12
14
  #
@@ -21,21 +23,22 @@ module Rack
21
23
  @app = app
22
24
  end
23
25
 
26
+ # Return empty 304 response if the response has not been
27
+ # modified since the last request.
24
28
  def call(env)
25
29
  case env[REQUEST_METHOD]
26
30
  when "GET", "HEAD"
27
- status, headers, body = @app.call(env)
28
- headers = Utils::HeaderHash.new(headers)
31
+ status, headers, body = response = @app.call(env)
32
+
29
33
  if status == 200 && fresh?(env, headers)
30
- status = 304
34
+ response[0] = 304
31
35
  headers.delete(CONTENT_TYPE)
32
36
  headers.delete(CONTENT_LENGTH)
33
- original_body = body
34
- body = Rack::BodyProxy.new([]) do
35
- original_body.close if original_body.respond_to?(:close)
37
+ response[2] = Rack::BodyProxy.new([]) do
38
+ body.close if body.respond_to?(:close)
36
39
  end
37
40
  end
38
- [status, headers, body]
41
+ response
39
42
  else
40
43
  @app.call(env)
41
44
  end
@@ -43,28 +46,32 @@ module Rack
43
46
 
44
47
  private
45
48
 
49
+ # Return whether the response has not been modified since the
50
+ # last request.
46
51
  def fresh?(env, headers)
47
- modified_since = env['HTTP_IF_MODIFIED_SINCE']
48
- none_match = env['HTTP_IF_NONE_MATCH']
49
-
50
- return false unless modified_since || none_match
51
-
52
- success = true
53
- success &&= modified_since?(to_rfc2822(modified_since), headers) if modified_since
54
- success &&= etag_matches?(none_match, headers) if none_match
55
- success
52
+ # if-none-match has priority over if-modified-since per RFC 7232
53
+ if none_match = env['HTTP_IF_NONE_MATCH']
54
+ etag_matches?(none_match, headers)
55
+ elsif (modified_since = env['HTTP_IF_MODIFIED_SINCE']) && (modified_since = to_rfc2822(modified_since))
56
+ modified_since?(modified_since, headers)
57
+ end
56
58
  end
57
59
 
60
+ # Whether the etag response header matches the if-none-match request header.
61
+ # If so, the request has not been modified.
58
62
  def etag_matches?(none_match, headers)
59
- etag = headers['ETag'] and etag == none_match
63
+ headers[ETAG] == none_match
60
64
  end
61
65
 
66
+ # Whether the last-modified response header matches the if-modified-since
67
+ # request header. If so, the request has not been modified.
62
68
  def modified_since?(modified_since, headers)
63
- last_modified = to_rfc2822(headers['Last-Modified']) and
64
- modified_since and
69
+ last_modified = to_rfc2822(headers['last-modified']) and
65
70
  modified_since >= last_modified
66
71
  end
67
72
 
73
+ # Return a Time object for the given string (which should be in RFC2822
74
+ # format), or nil if the string cannot be parsed.
68
75
  def to_rfc2822(since)
69
76
  # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
70
77
  # anything shorter is invalid, this avoids exceptions for common cases
@@ -73,8 +80,6 @@ module Rack
73
80
  # NOTE: there is no trivial way to write this in a non exception way
74
81
  # _rfc2822 returns a hash but is not that usable
75
82
  Time.rfc2822(since) rescue nil
76
- else
77
- nil
78
83
  end
79
84
  end
80
85
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ # Request env keys
5
+ HTTP_HOST = 'HTTP_HOST'
6
+ HTTP_PORT = 'HTTP_PORT'
7
+ HTTPS = 'HTTPS'
8
+ PATH_INFO = 'PATH_INFO'
9
+ REQUEST_METHOD = 'REQUEST_METHOD'
10
+ REQUEST_PATH = 'REQUEST_PATH'
11
+ SCRIPT_NAME = 'SCRIPT_NAME'
12
+ QUERY_STRING = 'QUERY_STRING'
13
+ SERVER_PROTOCOL = 'SERVER_PROTOCOL'
14
+ SERVER_NAME = 'SERVER_NAME'
15
+ SERVER_PORT = 'SERVER_PORT'
16
+ HTTP_COOKIE = 'HTTP_COOKIE'
17
+
18
+ # Response Header Keys
19
+ CACHE_CONTROL = 'cache-control'
20
+ CONTENT_LENGTH = 'content-length'
21
+ CONTENT_TYPE = 'content-type'
22
+ ETAG = 'etag'
23
+ EXPIRES = 'expires'
24
+ SET_COOKIE = 'set-cookie'
25
+
26
+ # HTTP method verbs
27
+ GET = 'GET'
28
+ POST = 'POST'
29
+ PUT = 'PUT'
30
+ PATCH = 'PATCH'
31
+ DELETE = 'DELETE'
32
+ HEAD = 'HEAD'
33
+ OPTIONS = 'OPTIONS'
34
+ CONNECT = 'CONNECT'
35
+ LINK = 'LINK'
36
+ UNLINK = 'UNLINK'
37
+ TRACE = 'TRACE'
38
+
39
+ # Rack environment variables
40
+ RACK_VERSION = 'rack.version'
41
+ RACK_TEMPFILES = 'rack.tempfiles'
42
+ RACK_EARLY_HINTS = 'rack.early_hints'
43
+ RACK_ERRORS = 'rack.errors'
44
+ RACK_LOGGER = 'rack.logger'
45
+ RACK_INPUT = 'rack.input'
46
+ RACK_SESSION = 'rack.session'
47
+ RACK_SESSION_OPTIONS = 'rack.session.options'
48
+ RACK_SHOWSTATUS_DETAIL = 'rack.showstatus.detail'
49
+ RACK_URL_SCHEME = 'rack.url_scheme'
50
+ RACK_HIJACK = 'rack.hijack'
51
+ RACK_IS_HIJACK = 'rack.hijack?'
52
+ RACK_RECURSIVE_INCLUDE = 'rack.recursive.include'
53
+ RACK_MULTIPART_BUFFER_SIZE = 'rack.multipart.buffer_size'
54
+ RACK_MULTIPART_TEMPFILE_FACTORY = 'rack.multipart.tempfile_factory'
55
+ RACK_RESPONSE_FINISHED = 'rack.response_finished'
56
+ RACK_REQUEST_FORM_INPUT = 'rack.request.form_input'
57
+ RACK_REQUEST_FORM_HASH = 'rack.request.form_hash'
58
+ RACK_REQUEST_FORM_PAIRS = 'rack.request.form_pairs'
59
+ RACK_REQUEST_FORM_VARS = 'rack.request.form_vars'
60
+ RACK_REQUEST_FORM_ERROR = 'rack.request.form_error'
61
+ RACK_REQUEST_COOKIE_HASH = 'rack.request.cookie_hash'
62
+ RACK_REQUEST_COOKIE_STRING = 'rack.request.cookie_string'
63
+ RACK_REQUEST_QUERY_HASH = 'rack.request.query_hash'
64
+ RACK_REQUEST_QUERY_STRING = 'rack.request.query_string'
65
+ RACK_METHODOVERRIDE_ORIGINAL_METHOD = 'rack.methodoverride.original_method'
66
+ end
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
4
- require 'rack/body_proxy'
3
+ require_relative 'constants'
4
+ require_relative 'utils'
5
5
 
6
6
  module Rack
7
7
 
8
- # Sets the Content-Length header on responses with fixed-length bodies.
8
+ # Sets the content-length header on responses that do not specify
9
+ # a content-length or transfer-encoding header. Note that this
10
+ # does not fix responses that have an invalid content-length
11
+ # header specified.
9
12
  class ContentLength
10
13
  include Rack::Utils
11
14
 
@@ -14,26 +17,17 @@ module Rack
14
17
  end
15
18
 
16
19
  def call(env)
17
- status, headers, body = @app.call(env)
18
- headers = HeaderHash.new(headers)
20
+ status, headers, body = response = @app.call(env)
19
21
 
20
22
  if !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
21
23
  !headers[CONTENT_LENGTH] &&
22
- !headers[TRANSFER_ENCODING] &&
23
24
  body.respond_to?(:to_ary)
24
25
 
25
- obody = body
26
- body, length = [], 0
27
- obody.each { |part| body << part; length += part.bytesize }
28
-
29
- body = BodyProxy.new(body) do
30
- obody.close if obody.respond_to?(:close)
31
- end
32
-
33
- headers[CONTENT_LENGTH] = length.to_s
26
+ response[2] = body = body.to_ary
27
+ headers[CONTENT_LENGTH] = body.sum(&:bytesize).to_s
34
28
  end
35
29
 
36
- [status, headers, body]
30
+ response
37
31
  end
38
32
  end
39
33
  end
@@ -1,31 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rack/utils'
3
+ require_relative 'constants'
4
+ require_relative 'utils'
4
5
 
5
6
  module Rack
6
7
 
7
- # Sets the Content-Type header on responses which don't have one.
8
+ # Sets the content-type header on responses which don't have one.
8
9
  #
9
10
  # Builder Usage:
10
11
  # use Rack::ContentType, "text/plain"
11
12
  #
12
- # When no content type argument is provided, "text/html" is assumed.
13
+ # When no content type argument is provided, "text/html" is the
14
+ # default.
13
15
  class ContentType
14
16
  include Rack::Utils
15
17
 
16
18
  def initialize(app, content_type = "text/html")
17
- @app, @content_type = app, content_type
19
+ @app = app
20
+ @content_type = content_type
18
21
  end
19
22
 
20
23
  def call(env)
21
- status, headers, body = @app.call(env)
22
- headers = Utils::HeaderHash.new(headers)
24
+ status, headers, _ = response = @app.call(env)
23
25
 
24
26
  unless STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i)
25
27
  headers[CONTENT_TYPE] ||= @content_type
26
28
  end
27
29
 
28
- [status, headers, body]
30
+ response
29
31
  end
30
32
  end
31
33
  end
data/lib/rack/deflater.rb CHANGED
@@ -2,51 +2,52 @@
2
2
 
3
3
  require "zlib"
4
4
  require "time" # for Time.httpdate
5
- require 'rack/utils'
6
5
 
7
- require_relative 'core_ext/regexp'
6
+ require_relative 'constants'
7
+ require_relative 'utils'
8
+ require_relative 'request'
9
+ require_relative 'body_proxy'
8
10
 
9
11
  module Rack
10
- # This middleware enables compression of http responses.
12
+ # This middleware enables content encoding of http responses,
13
+ # usually for purposes of compression.
11
14
  #
12
- # Currently supported compression algorithms:
15
+ # Currently supported encodings:
13
16
  #
14
- # * gzip
15
- # * identity (no transformation)
17
+ # * gzip
18
+ # * identity (no transformation)
16
19
  #
17
- # The middleware automatically detects when compression is supported
18
- # and allowed. For example no transformation is made when a cache
19
- # directive of 'no-transform' is present, or when the response status
20
- # code is one that doesn't allow an entity body.
20
+ # This middleware automatically detects when encoding is supported
21
+ # and allowed. For example no encoding is made when a cache
22
+ # directive of 'no-transform' is present, when the response status
23
+ # code is one that doesn't allow an entity body, or when the body
24
+ # is empty.
25
+ #
26
+ # Note that despite the name, Deflater does not support the +deflate+
27
+ # encoding.
21
28
  class Deflater
22
- using ::Rack::RegexpExtensions
23
-
24
- ##
25
- # Creates Rack::Deflater middleware.
29
+ # Creates Rack::Deflater middleware. Options:
26
30
  #
27
- # [app] rack app instance
28
- # [options] hash of deflater options, i.e.
29
- # 'if' - a lambda enabling / disabling deflation based on returned boolean value
30
- # e.g use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }
31
- # 'include' - a list of content types that should be compressed
32
- # 'sync' - determines if the stream is going to be flushed after every chunk.
33
- # Flushing after every chunk reduces latency for
34
- # time-sensitive streaming applications, but hurts
35
- # compression and throughput. Defaults to `true'.
31
+ # :if :: a lambda enabling / disabling deflation based on returned boolean value
32
+ # (e.g <tt>use Rack::Deflater, :if => lambda { |*, body| sum=0; body.each { |i| sum += i.length }; sum > 512 }</tt>).
33
+ # However, be aware that calling `body.each` inside the block will break cases where `body.each` is not idempotent,
34
+ # such as when it is an +IO+ instance.
35
+ # :include :: a list of content types that should be compressed. By default, all content types are compressed.
36
+ # :sync :: determines if the stream is going to be flushed after every chunk. Flushing after every chunk reduces
37
+ # latency for time-sensitive streaming applications, but hurts compression and throughput.
38
+ # Defaults to +true+.
36
39
  def initialize(app, options = {})
37
40
  @app = app
38
-
39
41
  @condition = options[:if]
40
42
  @compressible_types = options[:include]
41
- @sync = options[:sync] == false ? false : true
43
+ @sync = options.fetch(:sync, true)
42
44
  end
43
45
 
44
46
  def call(env)
45
- status, headers, body = @app.call(env)
46
- headers = Utils::HeaderHash.new(headers)
47
+ status, headers, body = response = @app.call(env)
47
48
 
48
49
  unless should_deflate?(env, status, headers, body)
49
- return [status, headers, body]
50
+ return response
50
51
  end
51
52
 
52
53
  request = Request.new(env)
@@ -55,75 +56,102 @@ module Rack
55
56
  request.accept_encoding)
56
57
 
57
58
  # Set the Vary HTTP header.
58
- vary = headers["Vary"].to_s.split(",").map(&:strip)
59
- unless vary.include?("*") || vary.include?("Accept-Encoding")
60
- headers["Vary"] = vary.push("Accept-Encoding").join(",")
59
+ vary = headers["vary"].to_s.split(",").map(&:strip)
60
+ unless vary.include?("*") || vary.any?{|v| v.downcase == 'accept-encoding'}
61
+ headers["vary"] = vary.push("Accept-Encoding").join(",")
61
62
  end
62
63
 
63
64
  case encoding
64
65
  when "gzip"
65
- headers['Content-Encoding'] = "gzip"
66
- headers.delete('Content-Length')
67
- mtime = headers["Last-Modified"]
66
+ headers['content-encoding'] = "gzip"
67
+ headers.delete(CONTENT_LENGTH)
68
+ mtime = headers["last-modified"]
68
69
  mtime = Time.httpdate(mtime).to_i if mtime
69
- [status, headers, GzipStream.new(body, mtime, @sync)]
70
+ response[2] = GzipStream.new(body, mtime, @sync)
71
+ response
70
72
  when "identity"
71
- [status, headers, body]
72
- when nil
73
+ response
74
+ else # when nil
75
+ # Only possible encoding values here are 'gzip', 'identity', and nil
73
76
  message = "An acceptable encoding for the requested resource #{request.fullpath} could not be found."
74
77
  bp = Rack::BodyProxy.new([message]) { body.close if body.respond_to?(:close) }
75
- [406, { 'Content-Type' => "text/plain", 'Content-Length' => message.length.to_s }, bp]
78
+ [406, { CONTENT_TYPE => "text/plain", CONTENT_LENGTH => message.length.to_s }, bp]
76
79
  end
77
80
  end
78
81
 
82
+ # Body class used for gzip encoded responses.
79
83
  class GzipStream
84
+
85
+ BUFFER_LENGTH = 128 * 1_024
86
+
87
+ # Initialize the gzip stream. Arguments:
88
+ # body :: Response body to compress with gzip
89
+ # mtime :: The modification time of the body, used to set the
90
+ # modification time in the gzip header.
91
+ # sync :: Whether to flush each gzip chunk as soon as it is ready.
80
92
  def initialize(body, mtime, sync)
81
- @sync = sync
82
93
  @body = body
83
94
  @mtime = mtime
95
+ @sync = sync
84
96
  end
85
97
 
98
+ # Yield gzip compressed strings to the given block.
86
99
  def each(&block)
87
100
  @writer = block
88
101
  gzip = ::Zlib::GzipWriter.new(self)
89
102
  gzip.mtime = @mtime if @mtime
90
- @body.each { |part|
91
- len = gzip.write(part)
92
- # Flushing empty parts would raise Zlib::BufError.
93
- gzip.flush if @sync && len > 0
94
- }
103
+ # @body.each is equivalent to @body.gets (slow)
104
+ if @body.is_a? ::File # XXX: Should probably be ::IO
105
+ while part = @body.read(BUFFER_LENGTH)
106
+ gzip.write(part)
107
+ gzip.flush if @sync
108
+ end
109
+ else
110
+ @body.each { |part|
111
+ # Skip empty strings, as they would result in no output,
112
+ # and flushing empty parts would raise Zlib::BufError.
113
+ next if part.empty?
114
+ gzip.write(part)
115
+ gzip.flush if @sync
116
+ }
117
+ end
95
118
  ensure
96
- gzip.close
97
- @writer = nil
119
+ gzip.finish
98
120
  end
99
121
 
122
+ # Call the block passed to #each with the gzipped data.
100
123
  def write(data)
101
124
  @writer.call(data)
102
125
  end
103
126
 
127
+ # Close the original body if possible.
104
128
  def close
105
129
  @body.close if @body.respond_to?(:close)
106
- @body = nil
107
130
  end
108
131
  end
109
132
 
110
133
  private
111
134
 
135
+ # Whether the body should be compressed.
112
136
  def should_deflate?(env, status, headers, body)
113
137
  # Skip compressing empty entity body responses and responses with
114
138
  # no-transform set.
115
139
  if Utils::STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) ||
116
- /\bno-transform\b/.match?(headers['Cache-Control'].to_s) ||
117
- (headers['Content-Encoding'] && headers['Content-Encoding'] !~ /\bidentity\b/)
140
+ /\bno-transform\b/.match?(headers[CACHE_CONTROL].to_s) ||
141
+ headers['content-encoding']&.!~(/\bidentity\b/)
118
142
  return false
119
143
  end
120
144
 
121
145
  # Skip if @compressible_types are given and does not include request's content type
122
- return false if @compressible_types && !(headers.has_key?('Content-Type') && @compressible_types.include?(headers['Content-Type'][/[^;]*/]))
146
+ return false if @compressible_types && !(headers.has_key?(CONTENT_TYPE) && @compressible_types.include?(headers[CONTENT_TYPE][/[^;]*/]))
123
147
 
124
148
  # Skip if @condition lambda is given and evaluates to false
125
149
  return false if @condition && !@condition.call(env, status, headers, body)
126
150
 
151
+ # No point in compressing empty body, also handles usage with
152
+ # Rack::Sendfile.
153
+ return false if headers[CONTENT_LENGTH] == '0'
154
+
127
155
  true
128
156
  end
129
157
  end