rack 3.0.15 → 3.2.6

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +368 -6
  3. data/CONTRIBUTING.md +11 -9
  4. data/README.md +103 -28
  5. data/SPEC.rdoc +206 -288
  6. data/lib/rack/auth/abstract/request.rb +2 -0
  7. data/lib/rack/auth/basic.rb +1 -2
  8. data/lib/rack/bad_request.rb +8 -0
  9. data/lib/rack/builder.rb +29 -10
  10. data/lib/rack/cascade.rb +0 -3
  11. data/lib/rack/conditional_get.rb +4 -3
  12. data/lib/rack/constants.rb +4 -0
  13. data/lib/rack/directory.rb +6 -3
  14. data/lib/rack/events.rb +21 -6
  15. data/lib/rack/files.rb +1 -1
  16. data/lib/rack/head.rb +2 -3
  17. data/lib/rack/headers.rb +86 -2
  18. data/lib/rack/lint.rb +482 -425
  19. data/lib/rack/media_type.rb +14 -10
  20. data/lib/rack/mime.rb +6 -5
  21. data/lib/rack/mock_request.rb +10 -15
  22. data/lib/rack/mock_response.rb +50 -20
  23. data/lib/rack/multipart/parser.rb +255 -76
  24. data/lib/rack/multipart/uploaded_file.rb +42 -5
  25. data/lib/rack/multipart.rb +34 -1
  26. data/lib/rack/query_parser.rb +86 -78
  27. data/lib/rack/request.rb +78 -65
  28. data/lib/rack/response.rb +28 -20
  29. data/lib/rack/rewindable_input.rb +4 -1
  30. data/lib/rack/sendfile.rb +51 -21
  31. data/lib/rack/show_exceptions.rb +10 -4
  32. data/lib/rack/show_status.rb +0 -2
  33. data/lib/rack/static.rb +7 -3
  34. data/lib/rack/utils.rb +175 -119
  35. data/lib/rack/version.rb +3 -20
  36. data/lib/rack.rb +1 -4
  37. metadata +6 -12
  38. data/lib/rack/auth/digest/md5.rb +0 -1
  39. data/lib/rack/auth/digest/nonce.rb +0 -1
  40. data/lib/rack/auth/digest/params.rb +0 -1
  41. data/lib/rack/auth/digest/request.rb +0 -1
  42. data/lib/rack/auth/digest.rb +0 -256
  43. data/lib/rack/chunked.rb +0 -120
  44. data/lib/rack/file.rb +0 -9
  45. data/lib/rack/logger.rb +0 -22
@@ -1,42 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'bad_request'
3
4
  require 'uri'
4
5
 
5
6
  module Rack
6
7
  class QueryParser
7
- DEFAULT_SEP = /[&] */n
8
- COMMON_SEP = { ";" => /[;] */n, ";," => /[;,] */n, "&" => /[&] */n }
8
+ DEFAULT_SEP = /& */n
9
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n }
9
10
 
10
11
  # ParameterTypeError is the error that is raised when incoming structural
11
12
  # parameters (parsed by parse_nested_query) contain conflicting types.
12
- class ParameterTypeError < TypeError; end
13
+ class ParameterTypeError < TypeError
14
+ include BadRequest
15
+ end
13
16
 
14
17
  # InvalidParameterError is the error that is raised when incoming structural
15
18
  # parameters (parsed by parse_nested_query) contain invalid format or byte
16
19
  # sequence.
17
- class InvalidParameterError < ArgumentError; end
20
+ class InvalidParameterError < ArgumentError
21
+ include BadRequest
22
+ end
18
23
 
19
- # ParamsTooDeepError is the error that is raised when params are recursively
20
- # nested over the specified limit.
21
- class ParamsTooDeepError < RangeError; end
24
+ # QueryLimitError is for errors raised when the query provided exceeds one
25
+ # of the query parser limits.
26
+ class QueryLimitError < RangeError
27
+ include BadRequest
28
+ end
22
29
 
23
- def self.make_default(_key_space_limit=(not_deprecated = true; nil), param_depth_limit)
24
- unless not_deprecated
25
- 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)
26
- end
30
+ # ParamsTooDeepError is the old name for the error that is raised when params
31
+ # are recursively nested over the specified limit. Make it the same as
32
+ # as QueryLimitError, so that code that rescues ParamsTooDeepError error
33
+ # to handle bad query strings also now handles other limits.
34
+ ParamsTooDeepError = QueryLimitError
27
35
 
28
- new Params, param_depth_limit
36
+ def self.make_default(param_depth_limit, **options)
37
+ new(Params, param_depth_limit, **options)
29
38
  end
30
39
 
31
40
  attr_reader :param_depth_limit
32
41
 
33
- def initialize(params_class, _key_space_limit=(not_deprecated = true; nil), param_depth_limit)
34
- unless not_deprecated
35
- 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)
42
+ env_int = lambda do |key, val|
43
+ if str_val = ENV[key]
44
+ begin
45
+ val = Integer(str_val, 10)
46
+ rescue ArgumentError
47
+ raise ArgumentError, "non-integer value provided for environment variable #{key}"
48
+ end
36
49
  end
37
50
 
51
+ val
52
+ end
53
+
54
+ BYTESIZE_LIMIT = env_int.call("RACK_QUERY_PARSER_BYTESIZE_LIMIT", 4194304)
55
+ private_constant :BYTESIZE_LIMIT
56
+
57
+ PARAMS_LIMIT = env_int.call("RACK_QUERY_PARSER_PARAMS_LIMIT", 4096)
58
+ private_constant :PARAMS_LIMIT
59
+
60
+ attr_reader :bytesize_limit
61
+
62
+ def initialize(params_class, param_depth_limit, bytesize_limit: BYTESIZE_LIMIT, params_limit: PARAMS_LIMIT)
38
63
  @params_class = params_class
39
64
  @param_depth_limit = param_depth_limit
65
+ @bytesize_limit = bytesize_limit
66
+ @params_limit = params_limit
40
67
  end
41
68
 
42
69
  # Stolen from Mongrel, with some small modifications:
@@ -44,14 +71,9 @@ module Rack
44
71
  # to parse cookies by changing the characters used in the second parameter
45
72
  # (which defaults to '&').
46
73
  def parse_query(qs, separator = nil, &unescaper)
47
- unescaper ||= method(:unescape)
48
-
49
74
  params = make_params
50
75
 
51
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
52
- next if p.empty?
53
- k, v = p.split('=', 2).map!(&unescaper)
54
-
76
+ each_query_pair(qs, separator, unescaper) do |k, v|
55
77
  if cur = params[k]
56
78
  if cur.class == Array
57
79
  params[k] << v
@@ -66,6 +88,19 @@ module Rack
66
88
  return params.to_h
67
89
  end
68
90
 
91
+ # Parses a query string by breaking it up at the '&', returning all key-value
92
+ # pairs as an array of [key, value] arrays. Unlike parse_query, this preserves
93
+ # all duplicate keys rather than collapsing them.
94
+ def parse_query_pairs(qs, separator = nil)
95
+ pairs = []
96
+
97
+ each_query_pair(qs, separator) do |k, v|
98
+ pairs << [k, v]
99
+ end
100
+
101
+ pairs
102
+ end
103
+
69
104
  # parse_nested_query expands a query string into structural types. Supported
70
105
  # types are Arrays, Hashes and basic value types. It is possible to supply
71
106
  # query strings with parameters of conflicting types, in this case a
@@ -74,17 +109,11 @@ module Rack
74
109
  def parse_nested_query(qs, separator = nil)
75
110
  params = make_params
76
111
 
77
- unless qs.nil? || qs.empty?
78
- (qs || '').split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP).each do |p|
79
- k, v = p.split('=', 2).map! { |s| unescape(s) }
80
-
81
- _normalize_params(params, k, v, 0)
82
- end
112
+ each_query_pair(qs, separator) do |k, v|
113
+ _normalize_params(params, k, v, 0)
83
114
  end
84
115
 
85
116
  return params.to_h
86
- rescue ArgumentError => e
87
- raise InvalidParameterError, e.message, e.backtrace
88
117
  end
89
118
 
90
119
  # normalize_params recursively expands parameters into structural types. If
@@ -190,63 +219,42 @@ module Rack
190
219
  true
191
220
  end
192
221
 
193
- def unescape(string, encoding = Encoding::UTF_8)
194
- URI.decode_www_form_component(string, encoding)
195
- end
196
-
197
- class Params
198
- def initialize
199
- @size = 0
200
- @params = {}
201
- end
222
+ def each_query_pair(qs, separator, unescaper = nil)
223
+ return if !qs || qs.empty?
202
224
 
203
- def [](key)
204
- @params[key]
225
+ if qs.bytesize > @bytesize_limit
226
+ raise QueryLimitError, "total query size exceeds limit (#{@bytesize_limit})"
205
227
  end
206
228
 
207
- def []=(key, value)
208
- @params[key] = value
209
- end
229
+ pairs = qs.split(separator ? (COMMON_SEP[separator] || /[#{separator}] */n) : DEFAULT_SEP, @params_limit + 1)
210
230
 
211
- def key?(key)
212
- @params.key?(key)
231
+ if pairs.size > @params_limit
232
+ param_count = pairs.size + pairs.last.count(separator || "&")
233
+ raise QueryLimitError, "total number of query parameters (#{param_count}) exceeds limit (#{@params_limit})"
213
234
  end
214
235
 
215
- # Recursively unwraps nested `Params` objects and constructs an object
216
- # of the same shape, but using the objects' internal representations
217
- # (Ruby hashes) in place of the objects. The result is a hash consisting
218
- # purely of Ruby primitives.
219
- #
220
- # Mutation warning!
221
- #
222
- # 1. This method mutates the internal representation of the `Params`
223
- # objects in order to save object allocations.
224
- #
225
- # 2. The value you get back is a reference to the internal hash
226
- # representation, not a copy.
227
- #
228
- # 3. Because the `Params` object's internal representation is mutable
229
- # through the `#[]=` method, it is not thread safe. The result of
230
- # getting the hash representation while another thread is adding a
231
- # key to it is non-deterministic.
232
- #
233
- def to_h
234
- @params.each do |key, value|
235
- case value
236
- when self
237
- # Handle circular references gracefully.
238
- @params[key] = @params
239
- when Params
240
- @params[key] = value.to_h
241
- when Array
242
- value.map! { |v| v.kind_of?(Params) ? v.to_h : v }
243
- else
244
- # Ignore anything that is not a `Params` object or
245
- # a collection that can contain one.
246
- end
236
+ if unescaper
237
+ pairs.each do |p|
238
+ next if p.empty?
239
+ k, v = p.split('=', 2).map!(&unescaper)
240
+ yield k, v
241
+ end
242
+ else
243
+ pairs.each do |p|
244
+ next if p.empty?
245
+ k, v = p.split('=', 2).map! { |s| unescape(s) }
246
+ yield k, v
247
247
  end
248
- @params
249
248
  end
249
+ rescue ArgumentError => e
250
+ raise InvalidParameterError, e.message, e.backtrace
251
+ end
252
+
253
+ def unescape(string, encoding = Encoding::UTF_8)
254
+ URI.decode_www_form_component(string, encoding)
255
+ end
256
+
257
+ class Params < Hash
250
258
  alias_method :to_params_hash, :to_h
251
259
  end
252
260
  end
data/lib/rack/request.rb CHANGED
@@ -61,9 +61,14 @@ module Rack
61
61
 
62
62
  def initialize(env)
63
63
  @env = env
64
+ @ip = nil
64
65
  @params = nil
65
66
  end
66
67
 
68
+ def ip
69
+ @ip ||= super
70
+ end
71
+
67
72
  def params
68
73
  @params ||= super
69
74
  end
@@ -398,8 +403,8 @@ module Rack
398
403
  return forwarded.last
399
404
  end
400
405
  when :x_forwarded
401
- if value = get_header(HTTP_X_FORWARDED_HOST)
402
- return wrap_ipv6(split_header(value).last)
406
+ if (value = get_header(HTTP_X_FORWARDED_HOST)) && (x_forwarded_host = split_header(value).last)
407
+ return wrap_ipv6(x_forwarded_host)
403
408
  end
404
409
  end
405
410
  end
@@ -413,10 +418,9 @@ module Rack
413
418
 
414
419
  def ip
415
420
  remote_addresses = split_header(get_header('REMOTE_ADDR'))
416
- external_addresses = reject_trusted_ip_addresses(remote_addresses)
417
421
 
418
- unless external_addresses.empty?
419
- return external_addresses.last
422
+ remote_addresses.reverse_each do |ip|
423
+ return ip unless trusted_proxy?(ip)
420
424
  end
421
425
 
422
426
  if (forwarded_for = self.forwarded_for) && !forwarded_for.empty?
@@ -424,7 +428,10 @@ module Rack
424
428
  # So we reject all the trusted addresses (proxy*) and return the
425
429
  # last client. Or if we trust everyone, we just return the first
426
430
  # address.
427
- return reject_trusted_ip_addresses(forwarded_for).last || forwarded_for.first
431
+ forwarded_for.reverse_each do |ip|
432
+ return ip unless trusted_proxy?(ip)
433
+ end
434
+ return forwarded_for.first
428
435
  end
429
436
 
430
437
  # If all the addresses are trusted, and we aren't forwarded, just return
@@ -482,56 +489,45 @@ module Rack
482
489
 
483
490
  # Returns the data received in the query string.
484
491
  def GET
485
- if get_header(RACK_REQUEST_QUERY_STRING) == query_string
486
- get_header(RACK_REQUEST_QUERY_HASH)
487
- else
488
- query_hash = parse_query(query_string, '&')
489
- set_header(RACK_REQUEST_QUERY_STRING, query_string)
490
- set_header(RACK_REQUEST_QUERY_HASH, query_hash)
491
- end
492
+ get_header(RACK_REQUEST_QUERY_HASH) || set_header(RACK_REQUEST_QUERY_HASH, parse_query(query_string, '&'))
492
493
  end
493
494
 
494
- # Returns the data received in the request body.
495
+ # Returns the form data pairs received in the request body.
495
496
  #
496
497
  # This method support both application/x-www-form-urlencoded and
497
498
  # multipart/form-data.
498
- def POST
499
- if error = get_header(RACK_REQUEST_FORM_ERROR)
499
+ def form_pairs
500
+ if pairs = get_header(RACK_REQUEST_FORM_PAIRS)
501
+ return pairs
502
+ elsif error = get_header(RACK_REQUEST_FORM_ERROR)
500
503
  raise error.class, error.message, cause: error.cause
501
504
  end
502
505
 
503
506
  begin
504
507
  rack_input = get_header(RACK_INPUT)
505
508
 
506
- # If the form hash was already memoized:
507
- if form_hash = get_header(RACK_REQUEST_FORM_HASH)
508
- # And it was memoized from the same input:
509
- if get_header(RACK_REQUEST_FORM_INPUT).equal?(rack_input)
510
- return form_hash
511
- end
512
- end
513
-
514
509
  # Otherwise, figure out how to parse the input:
515
510
  if rack_input.nil?
516
- set_header RACK_REQUEST_FORM_INPUT, nil
517
- set_header(RACK_REQUEST_FORM_HASH, {})
511
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
518
512
  elsif form_data? || parseable_data?
519
- unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
520
- form_vars = get_header(RACK_INPUT).read
513
+ if pairs = Rack::Multipart.parse_multipart(env, Rack::Multipart::ParamList)
514
+ set_header RACK_REQUEST_FORM_PAIRS, pairs
515
+ else
516
+ # Add 2 bytes. One to check whether it is over the limit, and a second
517
+ # in case the slice! call below removes the last byte
518
+ # If read returns nil, use the empty string
519
+ form_vars = get_header(RACK_INPUT).read(query_parser.bytesize_limit + 2) || ''
521
520
 
522
521
  # Fix for Safari Ajax postings that always append \0
523
522
  # form_vars.sub!(/\0\z/, '') # performance replacement:
524
523
  form_vars.slice!(-1) if form_vars.end_with?("\0")
525
524
 
526
525
  set_header RACK_REQUEST_FORM_VARS, form_vars
527
- set_header RACK_REQUEST_FORM_HASH, parse_query(form_vars, '&')
526
+ pairs = query_parser.parse_query_pairs(form_vars, '&')
527
+ set_header(RACK_REQUEST_FORM_PAIRS, pairs)
528
528
  end
529
-
530
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
531
- get_header RACK_REQUEST_FORM_HASH
532
529
  else
533
- set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT)
534
- set_header(RACK_REQUEST_FORM_HASH, {})
530
+ set_header(RACK_REQUEST_FORM_PAIRS, [])
535
531
  end
536
532
  rescue => error
537
533
  set_header(RACK_REQUEST_FORM_ERROR, error)
@@ -539,6 +535,21 @@ module Rack
539
535
  end
540
536
  end
541
537
 
538
+ # Returns the data received in the request body.
539
+ #
540
+ # This method support both application/x-www-form-urlencoded and
541
+ # multipart/form-data.
542
+ def POST
543
+ if form_hash = get_header(RACK_REQUEST_FORM_HASH)
544
+ return form_hash
545
+ elsif error = get_header(RACK_REQUEST_FORM_ERROR)
546
+ raise error.class, error.message, cause: error.cause
547
+ end
548
+
549
+ pairs = form_pairs
550
+ set_header RACK_REQUEST_FORM_HASH, expand_param_pairs(pairs)
551
+ end
552
+
542
553
  # The union of GET and POST data.
543
554
  #
544
555
  # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
@@ -546,6 +557,10 @@ module Rack
546
557
  self.GET.merge(self.POST)
547
558
  end
548
559
 
560
+ # Allow overriding the query parser that the receiver will use.
561
+ # By default Rack::Utils.default_query_parser is used.
562
+ attr_writer :query_parser
563
+
549
564
  # Destructively update a parameter, whether it's in GET and/or POST. Returns nil.
550
565
  #
551
566
  # The parameter is updated wherever it was previous defined, so GET, POST, or both. If it wasn't previously defined, it's inserted into GET.
@@ -605,27 +620,6 @@ module Rack
605
620
  Rack::Request.ip_filter.call(ip)
606
621
  end
607
622
 
608
- # shortcut for <tt>request.params[key]</tt>
609
- def [](key)
610
- warn("Request#[] is deprecated and will be removed in a future version of Rack. Please use request.params[] instead", uplevel: 1)
611
-
612
- params[key.to_s]
613
- end
614
-
615
- # shortcut for <tt>request.params[key] = value</tt>
616
- #
617
- # Note that modifications will not be persisted in the env. Use update_param or delete_param if you want to destructively modify params.
618
- def []=(key, value)
619
- warn("Request#[]= is deprecated and will be removed in a future version of Rack. Please use request.params[]= instead", uplevel: 1)
620
-
621
- params[key.to_s] = value
622
- end
623
-
624
- # like Hash#values_at
625
- def values_at(*keys)
626
- keys.map { |key| params[key] }
627
- end
628
-
629
623
  private
630
624
 
631
625
  def default_session; {}; end
@@ -645,14 +639,26 @@ module Rack
645
639
  end
646
640
 
647
641
  def parse_http_accept_header(header)
648
- header.to_s.split(",").each(&:strip!).map do |part|
649
- attribute, parameters = part.split(";", 2).each(&:strip!)
642
+ # It would be nice to use filter_map here, but it's Ruby 2.7+
643
+ parts = header.to_s.split(',')
644
+
645
+ parts.map! do |part|
646
+ part.strip!
647
+ next if part.empty?
648
+
649
+ attribute, parameters = part.split(';', 2)
650
+ attribute.strip!
651
+ parameters&.strip!
650
652
  quality = 1.0
651
653
  if parameters and /\Aq=([\d.]+)/ =~ parameters
652
654
  quality = $1.to_f
653
655
  end
654
656
  [attribute, quality]
655
657
  end
658
+
659
+ parts.compact!
660
+
661
+ parts
656
662
  end
657
663
 
658
664
  # Get an array of values set in the RFC 7239 `Forwarded` request header.
@@ -661,7 +667,7 @@ module Rack
661
667
  end
662
668
 
663
669
  def query_parser
664
- Utils.default_query_parser
670
+ @query_parser || Utils.default_query_parser
665
671
  end
666
672
 
667
673
  def parse_query(qs, d = '&')
@@ -669,11 +675,22 @@ module Rack
669
675
  end
670
676
 
671
677
  def parse_multipart
678
+ warn "Rack::Request#parse_multipart is deprecated and will be removed in a future version of Rack.", uplevel: 1
672
679
  Rack::Multipart.extract_multipart(self, query_parser)
673
680
  end
674
681
 
682
+ def expand_param_pairs(pairs, query_parser = query_parser())
683
+ params = query_parser.make_params
684
+
685
+ pairs.each do |k, v|
686
+ query_parser.normalize_params(params, k, v)
687
+ end
688
+
689
+ params.to_params_hash
690
+ end
691
+
675
692
  def split_header(value)
676
- value ? value.strip.split(/[,\s]+/) : []
693
+ value ? value.strip.split(/[, \t]+/) : []
677
694
  end
678
695
 
679
696
  # ipv6 extracted from resolv stdlib, simplified
@@ -706,8 +723,8 @@ module Rack
706
723
  # Match IPv6 as a string of hex digits and colons in square brackets
707
724
  \[(?<address>#{ipv6})\]
708
725
  |
709
- # Match any other printable string (except square brackets) as a hostname
710
- (?<address>[[[:graph:]&&[^\[\]]]]*?)
726
+ # Match characters allowed by RFC 3986 Section 3.2.2
727
+ (?<address>[-a-zA-Z0-9._~%!$&'()*+,;=]*?)
711
728
  )
712
729
  (:(?<port>\d+))?
713
730
  \z
@@ -721,10 +738,6 @@ module Rack
721
738
  return match[:host], match[:address], match[:port]&.to_i
722
739
  end
723
740
 
724
- def reject_trusted_ip_addresses(ip_addresses)
725
- ip_addresses.reject { |ip| trusted_proxy?(ip) }
726
- end
727
-
728
741
  FORWARDED_SCHEME_HEADERS = {
729
742
  proto: HTTP_X_FORWARDED_PROTO,
730
743
  scheme: HTTP_X_FORWARDED_SCHEME
data/lib/rack/response.rb CHANGED
@@ -31,13 +31,6 @@ module Rack
31
31
  attr_accessor :length, :status, :body
32
32
  attr_reader :headers
33
33
 
34
- # Deprecated, use headers instead.
35
- def header
36
- warn 'Rack::Response#header is deprecated and will be removed in Rack 3.1, use #headers instead', uplevel: 1
37
-
38
- headers
39
- end
40
-
41
34
  # Initialize the response object with the specified +body+, +status+
42
35
  # and +headers+.
43
36
  #
@@ -62,7 +55,7 @@ module Rack
62
55
  @status = status.to_i
63
56
 
64
57
  unless headers.is_a?(Hash)
65
- warn "Providing non-hash headers to Rack::Response is deprecated and will be removed in Rack 3.1", uplevel: 1
58
+ raise ArgumentError, "Headers must be a Hash!"
66
59
  end
67
60
 
68
61
  @headers = Headers.new
@@ -79,7 +72,8 @@ module Rack
79
72
  if body.nil?
80
73
  @body = []
81
74
  @buffered = true
82
- @length = 0
75
+ # Body is unspecified - it may be a buffered response, or it may be a HEAD response.
76
+ @length = nil
83
77
  elsif body.respond_to?(:to_str)
84
78
  @body = [body]
85
79
  @buffered = true
@@ -87,7 +81,7 @@ module Rack
87
81
  else
88
82
  @body = body
89
83
  @buffered = nil # undetermined as of yet.
90
- @length = 0
84
+ @length = nil
91
85
  end
92
86
 
93
87
  yield self if block_given?
@@ -106,7 +100,7 @@ module Rack
106
100
  # The response body is an enumerable body and it is not allowed to have an entity body.
107
101
  @body.respond_to?(:each) && STATUS_WITH_NO_ENTITY_BODY[@status]
108
102
  end
109
-
103
+
110
104
  # Generate a response array consistent with the requirements of the SPEC.
111
105
  # @return [Array] a 3-tuple suitable of `[status, headers, body]`
112
106
  # which is suitable to be returned from the middleware `#call(env)` method.
@@ -118,9 +112,14 @@ module Rack
118
112
  return [@status, @headers, []]
119
113
  else
120
114
  if block_given?
115
+ # We don't add the content-length here as the user has provided a block that can #write additional chunks to the body.
121
116
  @block = block
122
117
  return [@status, @headers, self]
123
118
  else
119
+ # If we know the length of the body, set the content-length header... except if we are chunked? which is a legacy special case where the body might already be encoded and thus the actual encoded body length and the content-length are likely to be different.
120
+ if @length && !chunked?
121
+ @headers[CONTENT_LENGTH] = @length.to_s
122
+ end
124
123
  return [@status, @headers, @body]
125
124
  end
126
125
  end
@@ -138,7 +137,9 @@ module Rack
138
137
  end
139
138
  end
140
139
 
141
- # Append to body and update content-length.
140
+ # Append a chunk to the response body.
141
+ #
142
+ # Converts the response into a buffered response if it wasn't already.
142
143
  #
143
144
  # NOTE: Do not mix #write and direct #body access!
144
145
  #
@@ -320,29 +321,34 @@ module Rack
320
321
 
321
322
  protected
322
323
 
324
+ # Convert the body of this response into an internally buffered Array if possible.
325
+ #
326
+ # `@buffered` is a ternary value which indicates whether the body is buffered. It can be:
327
+ # * `nil` - The body has not been buffered yet.
328
+ # * `true` - The body is buffered as an Array instance.
329
+ # * `false` - The body is not buffered and cannot be buffered.
330
+ #
331
+ # @return [Boolean] whether the body is buffered as an Array instance.
323
332
  def buffered_body!
324
333
  if @buffered.nil?
325
334
  if @body.is_a?(Array)
326
335
  # The user supplied body was an array:
327
336
  @body = @body.compact
328
- @body.each do |part|
329
- @length += part.to_s.bytesize
330
- end
331
-
337
+ @length = @body.sum{|part| part.bytesize}
332
338
  @buffered = true
333
339
  elsif @body.respond_to?(:each)
334
340
  # Turn the user supplied body into a buffered array:
335
341
  body = @body
336
342
  @body = Array.new
343
+ @buffered = true
337
344
 
338
345
  body.each do |part|
339
346
  @writer.call(part.to_s)
340
347
  end
341
348
 
342
349
  body.close if body.respond_to?(:close)
343
-
344
- @buffered = true
345
350
  else
351
+ # We don't know how to buffer the user-supplied body:
346
352
  @buffered = false
347
353
  end
348
354
  end
@@ -351,11 +357,13 @@ module Rack
351
357
  end
352
358
 
353
359
  def append(chunk)
360
+ chunk = chunk.dup unless chunk.frozen?
354
361
  @body << chunk
355
362
 
356
- unless chunked?
363
+ if @length
357
364
  @length += chunk.bytesize
358
- set_header(CONTENT_LENGTH, @length.to_s)
365
+ elsif @buffered
366
+ @length = chunk.bytesize
359
367
  end
360
368
 
361
369
  return chunk
@@ -21,7 +21,10 @@ module Rack
21
21
  end
22
22
 
23
23
  def call(env)
24
- env[RACK_INPUT] = RewindableInput.new(env[RACK_INPUT])
24
+ if (input = env[RACK_INPUT])
25
+ env[RACK_INPUT] = RewindableInput.new(input)
26
+ end
27
+
25
28
  @app.call(env)
26
29
  end
27
30
  end