actionpack 7.2.3 → 8.0.5

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +198 -119
  3. data/lib/abstract_controller/rendering.rb +0 -1
  4. data/lib/action_controller/base.rb +1 -1
  5. data/lib/action_controller/form_builder.rb +3 -3
  6. data/lib/action_controller/metal/allow_browser.rb +11 -1
  7. data/lib/action_controller/metal/conditional_get.rb +5 -1
  8. data/lib/action_controller/metal/data_streaming.rb +4 -2
  9. data/lib/action_controller/metal/instrumentation.rb +1 -2
  10. data/lib/action_controller/metal/live.rb +58 -10
  11. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  12. data/lib/action_controller/metal/renderers.rb +2 -3
  13. data/lib/action_controller/metal/streaming.rb +5 -84
  14. data/lib/action_controller/metal/strong_parameters.rb +277 -89
  15. data/lib/action_controller/railtie.rb +6 -7
  16. data/lib/action_controller/test_case.rb +12 -2
  17. data/lib/action_dispatch/http/cache.rb +27 -10
  18. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  19. data/lib/action_dispatch/http/param_builder.rb +186 -0
  20. data/lib/action_dispatch/http/param_error.rb +26 -0
  21. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  22. data/lib/action_dispatch/http/query_parser.rb +53 -0
  23. data/lib/action_dispatch/http/request.rb +60 -16
  24. data/lib/action_dispatch/http/response.rb +34 -13
  25. data/lib/action_dispatch/journey/parser.rb +99 -196
  26. data/lib/action_dispatch/journey/scanner.rb +44 -42
  27. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  28. data/lib/action_dispatch/middleware/debug_exceptions.rb +17 -4
  29. data/lib/action_dispatch/middleware/debug_view.rb +0 -5
  30. data/lib/action_dispatch/middleware/exception_wrapper.rb +0 -6
  31. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  32. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  33. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
  34. data/lib/action_dispatch/railtie.rb +8 -0
  35. data/lib/action_dispatch/request/session.rb +1 -0
  36. data/lib/action_dispatch/request/utils.rb +9 -3
  37. data/lib/action_dispatch/routing/inspector.rb +1 -1
  38. data/lib/action_dispatch/routing/mapper.rb +91 -62
  39. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  40. data/lib/action_dispatch/routing/route_set.rb +21 -10
  41. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  42. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  43. data/lib/action_dispatch/testing/assertions/response.rb +12 -2
  44. data/lib/action_dispatch/testing/assertions/routing.rb +16 -12
  45. data/lib/action_dispatch/testing/integration.rb +18 -7
  46. data/lib/action_dispatch.rb +6 -4
  47. data/lib/action_pack/gem_version.rb +3 -3
  48. metadata +15 -48
  49. data/lib/action_dispatch/journey/parser.y +0 -50
  50. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -9,6 +9,8 @@ module ActionDispatch
9
9
  HTTP_IF_MODIFIED_SINCE = "HTTP_IF_MODIFIED_SINCE"
10
10
  HTTP_IF_NONE_MATCH = "HTTP_IF_NONE_MATCH"
11
11
 
12
+ mattr_accessor :strict_freshness, default: false
13
+
12
14
  def if_modified_since
13
15
  if since = get_header(HTTP_IF_MODIFIED_SINCE)
14
16
  Time.rfc2822(since) rescue nil
@@ -34,19 +36,32 @@ module ActionDispatch
34
36
  end
35
37
  end
36
38
 
37
- # Check response freshness (`Last-Modified` and ETag) against request
38
- # `If-Modified-Since` and `If-None-Match` conditions. If both headers are
39
- # supplied, both must match, or the request is not considered fresh.
39
+ # Check response freshness (`Last-Modified` and `ETag`) against request
40
+ # `If-Modified-Since` and `If-None-Match` conditions.
41
+ # If both headers are supplied, based on configuration, either `ETag` is preferred over `Last-Modified`
42
+ # or both are considered equally. You can adjust the preference with
43
+ # `config.action_dispatch.strict_freshness`.
44
+ # Reference: http://tools.ietf.org/html/rfc7232#section-6
40
45
  def fresh?(response)
41
- last_modified = if_modified_since
42
- etag = if_none_match
46
+ if Request.strict_freshness
47
+ if if_none_match
48
+ etag_matches?(response.etag)
49
+ elsif if_modified_since
50
+ not_modified?(response.last_modified)
51
+ else
52
+ false
53
+ end
54
+ else
55
+ last_modified = if_modified_since
56
+ etag = if_none_match
43
57
 
44
- return false unless last_modified || etag
58
+ return false unless last_modified || etag
45
59
 
46
- success = true
47
- success &&= not_modified?(response.last_modified) if last_modified
48
- success &&= etag_matches?(response.etag) if etag
49
- success
60
+ success = true
61
+ success &&= not_modified?(response.last_modified) if last_modified
62
+ success &&= etag_matches?(response.etag) if etag
63
+ success
64
+ end
50
65
  end
51
66
  end
52
67
 
@@ -171,6 +186,7 @@ module ActionDispatch
171
186
  PUBLIC = "public"
172
187
  PRIVATE = "private"
173
188
  MUST_REVALIDATE = "must-revalidate"
189
+ IMMUTABLE = "immutable"
174
190
 
175
191
  def handle_conditional_get!
176
192
  # Normally default cache control setting is handled by ETag middleware. But, if
@@ -221,6 +237,7 @@ module ActionDispatch
221
237
  options << MUST_REVALIDATE if control[:must_revalidate]
222
238
  options << "stale-while-revalidate=#{stale_while_revalidate.to_i}" if stale_while_revalidate
223
239
  options << "stale-if-error=#{stale_if_error.to_i}" if stale_if_error
240
+ options << IMMUTABLE if control[:immutable]
224
241
  options.concat(extras) if extras
225
242
  end
226
243
 
@@ -128,6 +128,7 @@ module ActionDispatch # :nodoc:
128
128
  MAPPINGS = {
129
129
  self: "'self'",
130
130
  unsafe_eval: "'unsafe-eval'",
131
+ wasm_unsafe_eval: "'wasm-unsafe-eval'",
131
132
  unsafe_hashes: "'unsafe-hashes'",
132
133
  unsafe_inline: "'unsafe-inline'",
133
134
  none: "'none'",
@@ -170,6 +171,8 @@ module ActionDispatch # :nodoc:
170
171
  worker_src: "worker-src"
171
172
  }.freeze
172
173
 
174
+ HASH_SOURCE_ALGORITHM_PREFIXES = ["sha256-", "sha384-", "sha512-"].freeze
175
+
173
176
  DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
174
177
 
175
178
  private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
@@ -304,7 +307,13 @@ module ActionDispatch # :nodoc:
304
307
  case source
305
308
  when Symbol
306
309
  apply_mapping(source)
307
- when String, Proc
310
+ when String
311
+ if hash_source?(source)
312
+ "'#{source}'"
313
+ else
314
+ source
315
+ end
316
+ when Proc
308
317
  source
309
318
  else
310
319
  raise ArgumentError, "Invalid content security policy source: #{source.inspect}"
@@ -373,5 +382,9 @@ module ActionDispatch # :nodoc:
373
382
  def nonce_directive?(directive, nonce_directives)
374
383
  nonce_directives.include?(directive)
375
384
  end
385
+
386
+ def hash_source?(source)
387
+ source.start_with?(*HASH_SOURCE_ALGORITHM_PREFIXES)
388
+ end
376
389
  end
377
390
  end
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class ParamBuilder
5
+ # --
6
+ # This implementation is based on Rack::QueryParser,
7
+ # Copyright (C) 2007-2021 Leah Neukirchen <http://leahneukirchen.org/infopage.html>
8
+
9
+ def self.make_default(param_depth_limit)
10
+ new param_depth_limit
11
+ end
12
+
13
+ attr_reader :param_depth_limit
14
+
15
+ def initialize(param_depth_limit)
16
+ @param_depth_limit = param_depth_limit
17
+ end
18
+
19
+ cattr_accessor :ignore_leading_brackets
20
+
21
+ LEADING_BRACKETS_COMPAT = defined?(::Rack::RELEASE) && ::Rack::RELEASE.to_s.start_with?("2.")
22
+
23
+ cattr_accessor :default
24
+ self.default = make_default(100)
25
+
26
+ class << self
27
+ delegate :from_query_string, :from_pairs, :from_hash, to: :default
28
+ end
29
+
30
+ def from_query_string(qs, separator: nil, encoding_template: nil)
31
+ from_pairs QueryParser.each_pair(qs, separator), encoding_template: encoding_template
32
+ end
33
+
34
+ def from_pairs(pairs, encoding_template: nil)
35
+ params = make_params
36
+
37
+ pairs.each do |k, v|
38
+ if Hash === v
39
+ v = ActionDispatch::Http::UploadedFile.new(v)
40
+ end
41
+
42
+ store_nested_param(params, k, v, 0, encoding_template)
43
+ end
44
+
45
+ params
46
+ rescue ArgumentError => e
47
+ raise InvalidParameterError, e.message, e.backtrace
48
+ end
49
+
50
+ def from_hash(hash, encoding_template: nil)
51
+ # Force encodings from encoding template
52
+ hash = Request::Utils::CustomParamEncoder.encode_for_template(hash, encoding_template)
53
+
54
+ # Assert valid encoding
55
+ Request::Utils.check_param_encoding(hash)
56
+
57
+ # Convert hashes to HWIA (or UploadedFile), and deep-munge nils
58
+ # out of arrays
59
+ hash = Request::Utils.normalize_encode_params(hash)
60
+
61
+ hash
62
+ end
63
+
64
+ private
65
+ def store_nested_param(params, name, v, depth, encoding_template = nil)
66
+ raise ParamsTooDeepError if depth >= param_depth_limit
67
+
68
+ if !name
69
+ # nil name, treat same as empty string (required by tests)
70
+ k = after = ""
71
+ elsif depth == 0
72
+ if ignore_leading_brackets || (ignore_leading_brackets.nil? && LEADING_BRACKETS_COMPAT)
73
+ # Rack 2 compatible behavior, ignore leading brackets
74
+ if name =~ /\A[\[\]]*([^\[\]]+)\]*/
75
+ k = $1
76
+ after = $' || ""
77
+
78
+ if !ignore_leading_brackets && (k != $& || !after.empty? && !after.start_with?("["))
79
+ ActionDispatch.deprecator.warn("Skipping over leading brackets in parameter name #{name.inspect} is deprecated and will parse differently in Rails 8.1 or Rack 3.0.")
80
+ end
81
+ else
82
+ k = name
83
+ after = ""
84
+ end
85
+ else
86
+ # Start of parsing, don't treat [] or [ at start of string specially
87
+ if start = name.index("[", 1)
88
+ # Start of parameter nesting, use part before brackets as key
89
+ k = name[0, start]
90
+ after = name[start, name.length]
91
+ else
92
+ # Plain parameter with no nesting
93
+ k = name
94
+ after = ""
95
+ end
96
+ end
97
+ elsif name.start_with?("[]")
98
+ # Array nesting
99
+ k = "[]"
100
+ after = name[2, name.length]
101
+ elsif name.start_with?("[") && (start = name.index("]", 1))
102
+ # Hash nesting, use the part inside brackets as the key
103
+ k = name[1, start - 1]
104
+ after = name[start + 1, name.length]
105
+ else
106
+ # Probably malformed input, nested but not starting with [
107
+ # treat full name as key for backwards compatibility.
108
+ k = name
109
+ after = ""
110
+ end
111
+
112
+ return if k.empty?
113
+
114
+ if depth == 0 && String === v
115
+ # We have to wait until we've found the top part of the name,
116
+ # because that's what the encoding template is configured with
117
+ if encoding_template && (designated_encoding = encoding_template[k]) && !v.frozen?
118
+ v.force_encoding(designated_encoding)
119
+ end
120
+
121
+ # ... and we can't validate the encoding until after we've
122
+ # applied any template override
123
+ unless v.valid_encoding?
124
+ raise InvalidParameterError, "Invalid encoding for parameter: #{v.scrub}"
125
+ end
126
+ end
127
+
128
+ if after == ""
129
+ if k == "[]" && depth != 0
130
+ return (v || !ActionDispatch::Request::Utils.perform_deep_munge) ? [v] : []
131
+ else
132
+ params[k] = v
133
+ end
134
+ elsif after == "["
135
+ params[name] = v
136
+ elsif after == "[]"
137
+ params[k] ||= []
138
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
139
+ params[k] << v if v || !ActionDispatch::Request::Utils.perform_deep_munge
140
+ elsif after.start_with?("[]")
141
+ # Recognize x[][y] (hash inside array) parameters
142
+ unless after[2] == "[" && after.end_with?("]") && (child_key = after[3, after.length - 4]) && !child_key.empty? && !child_key.index("[") && !child_key.index("]")
143
+ # Handle other nested array parameters
144
+ child_key = after[2, after.length]
145
+ end
146
+ params[k] ||= []
147
+ raise ParameterTypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array)
148
+ if params_hash_type?(params[k].last) && !params_hash_has_key?(params[k].last, child_key)
149
+ store_nested_param(params[k].last, child_key, v, depth + 1)
150
+ else
151
+ params[k] << store_nested_param(make_params, child_key, v, depth + 1)
152
+ end
153
+ else
154
+ params[k] ||= make_params
155
+ raise ParameterTypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params_hash_type?(params[k])
156
+ params[k] = store_nested_param(params[k], after, v, depth + 1)
157
+ end
158
+
159
+ params
160
+ end
161
+
162
+ def make_params
163
+ ActiveSupport::HashWithIndifferentAccess.new
164
+ end
165
+
166
+ def new_depth_limit(param_depth_limit)
167
+ self.class.new @params_class, param_depth_limit
168
+ end
169
+
170
+ def params_hash_type?(obj)
171
+ Hash === obj
172
+ end
173
+
174
+ def params_hash_has_key?(hash, key)
175
+ return false if key.include?("[]")
176
+
177
+ key.split(/[\[\]]+/).inject(hash) do |h, part|
178
+ next h if part == ""
179
+ return false unless params_hash_type?(h) && h.key?(part)
180
+ h[part]
181
+ end
182
+
183
+ true
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class ParamError < ActionDispatch::Http::Parameters::ParseError
5
+ def initialize(message = nil)
6
+ super
7
+ end
8
+
9
+ def self.===(other)
10
+ super || (
11
+ defined?(Rack::Utils::ParameterTypeError) && Rack::Utils::ParameterTypeError === other ||
12
+ defined?(Rack::Utils::InvalidParameterError) && Rack::Utils::InvalidParameterError === other ||
13
+ defined?(Rack::QueryParser::ParamsTooDeepError) && Rack::QueryParser::ParamsTooDeepError === other
14
+ )
15
+ end
16
+ end
17
+
18
+ class ParameterTypeError < ParamError
19
+ end
20
+
21
+ class InvalidParameterError < ParamError
22
+ end
23
+
24
+ class ParamsTooDeepError < ParamError
25
+ end
26
+ end
@@ -86,12 +86,14 @@ module ActionDispatch # :nodoc:
86
86
  ambient_light_sensor: "ambient-light-sensor",
87
87
  autoplay: "autoplay",
88
88
  camera: "camera",
89
+ display_capture: "display-capture",
89
90
  encrypted_media: "encrypted-media",
90
91
  fullscreen: "fullscreen",
91
92
  geolocation: "geolocation",
92
93
  gyroscope: "gyroscope",
93
94
  hid: "hid",
94
95
  idle_detection: "idle-detection",
96
+ keyboard_map: "keyboard-map",
95
97
  magnetometer: "magnetometer",
96
98
  microphone: "microphone",
97
99
  midi: "midi",
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "rack"
5
+
6
+ module ActionDispatch
7
+ class QueryParser
8
+ DEFAULT_SEP = /& */n
9
+ COMPAT_SEP = /[&;] */n
10
+ COMMON_SEP = { ";" => /; */n, ";," => /[;,] */n, "&" => /& */n, "&;" => /[&;] */n }
11
+
12
+ cattr_accessor :strict_query_string_separator
13
+
14
+ SEMICOLON_COMPAT = defined?(::Rack::QueryParser::DEFAULT_SEP) && ::Rack::QueryParser::DEFAULT_SEP.to_s.include?(";")
15
+
16
+ #--
17
+ # Note this departs from WHATWG's specified parsing algorithm by
18
+ # giving a nil value for keys that do not use '='. Callers that need
19
+ # the standard's interpretation can use `v.to_s`.
20
+ def self.each_pair(s, separator = nil)
21
+ return enum_for(:each_pair, s, separator) unless block_given?
22
+
23
+ s ||= ""
24
+
25
+ splitter =
26
+ if separator
27
+ COMMON_SEP[separator] || /[#{separator}] */n
28
+ elsif strict_query_string_separator
29
+ DEFAULT_SEP
30
+ elsif SEMICOLON_COMPAT && s.include?(";")
31
+ if strict_query_string_separator.nil?
32
+ ActionDispatch.deprecator.warn("Using semicolon as a query string separator is deprecated and will not be supported in Rails 8.1 or Rack 3.0. Use `&` instead.")
33
+ end
34
+ COMPAT_SEP
35
+ else
36
+ DEFAULT_SEP
37
+ end
38
+
39
+ s.split(splitter).each do |part|
40
+ next if part.empty?
41
+
42
+ k, v = part.split("=", 2)
43
+
44
+ k = URI.decode_www_form_component(k)
45
+ v &&= URI.decode_www_form_component(v)
46
+
47
+ yield k, v
48
+ end
49
+
50
+ nil
51
+ end
52
+ end
53
+ end
@@ -55,12 +55,17 @@ module ActionDispatch
55
55
  METHOD
56
56
  end
57
57
 
58
+ TRANSFER_ENCODING = "HTTP_TRANSFER_ENCODING" # :nodoc:
59
+
58
60
  def self.empty
59
61
  new({})
60
62
  end
61
63
 
62
64
  def initialize(env)
63
65
  super
66
+
67
+ @rack_request = Rack::Request.new(env)
68
+
64
69
  @method = nil
65
70
  @request_method = nil
66
71
  @remote_ip = nil
@@ -69,6 +74,8 @@ module ActionDispatch
69
74
  @ip = nil
70
75
  end
71
76
 
77
+ attr_reader :rack_request
78
+
72
79
  def commit_cookie_jar! # :nodoc:
73
80
  end
74
81
 
@@ -283,7 +290,7 @@ module ActionDispatch
283
290
 
284
291
  # Returns the content length of the request as an integer.
285
292
  def content_length
286
- return raw_post.bytesize if headers.key?("Transfer-Encoding")
293
+ return raw_post.bytesize if has_header?(TRANSFER_ENCODING)
287
294
  super.to_i
288
295
  end
289
296
 
@@ -387,15 +394,12 @@ module ActionDispatch
387
394
  # Override Rack's GET method to support indifferent access.
388
395
  def GET
389
396
  fetch_header("action_dispatch.request.query_parameters") do |k|
390
- rack_query_params = super || {}
391
- controller = path_parameters[:controller]
392
- action = path_parameters[:action]
393
- rack_query_params = Request::Utils.set_binary_encoding(self, rack_query_params, controller, action)
394
- # Check for non UTF-8 parameter values, which would cause errors later
395
- Request::Utils.check_param_encoding(rack_query_params)
396
- set_header k, Request::Utils.normalize_encode_params(rack_query_params)
397
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
398
+ rack_query_params = ActionDispatch::ParamBuilder.from_query_string(rack_request.query_string, encoding_template: encoding_template)
399
+
400
+ set_header k, rack_query_params
397
401
  end
398
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError => e
402
+ rescue ActionDispatch::ParamError => e
399
403
  raise ActionController::BadRequest.new("Invalid query parameters: #{e.message}")
400
404
  end
401
405
  alias :query_parameters :GET
@@ -403,18 +407,54 @@ module ActionDispatch
403
407
  # Override Rack's POST method to support indifferent access.
404
408
  def POST
405
409
  fetch_header("action_dispatch.request.request_parameters") do
406
- pr = parse_formatted_parameters(params_parsers) do |params|
407
- super || {}
410
+ encoding_template = Request::Utils::CustomParamEncoder.action_encoding_template(self, path_parameters[:controller], path_parameters[:action])
411
+
412
+ param_list = nil
413
+ pr = parse_formatted_parameters(params_parsers) do
414
+ if param_list = request_parameters_list
415
+ ActionDispatch::ParamBuilder.from_pairs(param_list, encoding_template: encoding_template)
416
+ else
417
+ # We're not using a version of Rack that provides raw form
418
+ # pairs; we must use its hash (and thus post-process it below).
419
+ fallback_request_parameters
420
+ end
408
421
  end
409
- pr = Request::Utils.set_binary_encoding(self, pr, path_parameters[:controller], path_parameters[:action])
410
- Request::Utils.check_param_encoding(pr)
411
- self.request_parameters = Request::Utils.normalize_encode_params(pr)
422
+
423
+ # If the request body was parsed by a custom parser like JSON
424
+ # (and thus the above block was not run), we need to
425
+ # post-process the result hash.
426
+ if param_list.nil?
427
+ pr = ActionDispatch::ParamBuilder.from_hash(pr, encoding_template: encoding_template)
428
+ end
429
+
430
+ self.request_parameters = pr
412
431
  end
413
- rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError, Rack::QueryParser::ParamsTooDeepError, EOFError => e
432
+ rescue ActionDispatch::ParamError, EOFError => e
414
433
  raise ActionController::BadRequest.new("Invalid request parameters: #{e.message}")
415
434
  end
416
435
  alias :request_parameters :POST
417
436
 
437
+ def request_parameters_list
438
+ # We don't use Rack's parse result, but we must call it so Rack
439
+ # can populate the rack.request.* keys we need.
440
+ rack_post = rack_request.POST
441
+
442
+ if form_pairs = get_header("rack.request.form_pairs")
443
+ # Multipart
444
+ form_pairs
445
+ elsif form_vars = get_header("rack.request.form_vars")
446
+ # URL-encoded
447
+ ActionDispatch::QueryParser.each_pair(form_vars)
448
+ elsif rack_post && !rack_post.empty?
449
+ # It was multipart, but Rack did not preserve a pair list
450
+ # (probably too old). Flat parameter list is not available.
451
+ nil
452
+ else
453
+ # No request body, or not a format Rack knows
454
+ []
455
+ end
456
+ end
457
+
418
458
  # Returns the authorization header regardless of whether it was specified
419
459
  # directly or through one of the proxy alternatives.
420
460
  def authorization
@@ -469,7 +509,7 @@ module ActionDispatch
469
509
  def read_body_stream
470
510
  if body_stream
471
511
  reset_stream(body_stream) do
472
- if headers.key?("Transfer-Encoding")
512
+ if has_header?(TRANSFER_ENCODING)
473
513
  body_stream.read # Read body stream until EOF if "Transfer-Encoding" is present
474
514
  else
475
515
  body_stream.read(content_length)
@@ -491,6 +531,10 @@ module ActionDispatch
491
531
  yield
492
532
  end
493
533
  end
534
+
535
+ def fallback_request_parameters
536
+ rack_request.POST
537
+ end
494
538
  end
495
539
  end
496
540
 
@@ -119,10 +119,22 @@ module ActionDispatch # :nodoc:
119
119
  @str_body = nil
120
120
  end
121
121
 
122
+ BODY_METHODS = { to_ary: true }
123
+
124
+ def respond_to?(method, include_private = false)
125
+ if BODY_METHODS.key?(method)
126
+ @buf.respond_to?(method)
127
+ else
128
+ super
129
+ end
130
+ end
131
+
122
132
  def to_ary
123
- @buf.respond_to?(:to_ary) ?
124
- @buf.to_ary :
125
- @buf.each
133
+ if @str_body
134
+ [body]
135
+ else
136
+ @buf = @buf.to_ary
137
+ end
126
138
  end
127
139
 
128
140
  def body
@@ -342,7 +354,13 @@ module ActionDispatch # :nodoc:
342
354
  # Returns the content of the response as a string. This contains the contents of
343
355
  # any calls to `render`.
344
356
  def body
345
- @stream.body
357
+ if @stream.respond_to?(:to_ary)
358
+ @stream.to_ary.join
359
+ elsif @stream.respond_to?(:body)
360
+ @stream.body
361
+ else
362
+ @stream
363
+ end
346
364
  end
347
365
 
348
366
  def write(string)
@@ -351,11 +369,16 @@ module ActionDispatch # :nodoc:
351
369
 
352
370
  # Allows you to manually set or override the response body.
353
371
  def body=(body)
354
- if body.respond_to?(:to_path)
355
- @stream = body
356
- else
357
- synchronize do
358
- @stream = build_buffer self, munge_body_object(body)
372
+ # Prevent ActionController::Metal::Live::Response from committing the response prematurely.
373
+ synchronize do
374
+ if body.respond_to?(:to_str)
375
+ @stream = build_buffer(self, [body])
376
+ elsif body.respond_to?(:to_path)
377
+ @stream = body
378
+ elsif body.respond_to?(:to_ary)
379
+ @stream = build_buffer(self, body)
380
+ else
381
+ @stream = body
359
382
  end
360
383
  end
361
384
  end
@@ -496,10 +519,6 @@ module ActionDispatch # :nodoc:
496
519
  Buffer.new response, body
497
520
  end
498
521
 
499
- def munge_body_object(body)
500
- body.respond_to?(:each) ? body : [body]
501
- end
502
-
503
522
  def assign_default_content_type_and_charset!
504
523
  return if media_type
505
524
 
@@ -513,6 +532,8 @@ module ActionDispatch # :nodoc:
513
532
  @response = response
514
533
  end
515
534
 
535
+ attr :response
536
+
516
537
  def close
517
538
  # Rack "close" maps to Response#abort, and **not** Response#close (which is used
518
539
  # when the controller's finished writing)