actionpack 7.2.2.1 → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +228 -101
  3. data/README.rdoc +1 -1
  4. data/lib/abstract_controller/base.rb +1 -12
  5. data/lib/abstract_controller/collector.rb +1 -1
  6. data/lib/abstract_controller/helpers.rb +1 -1
  7. data/lib/abstract_controller/rendering.rb +0 -1
  8. data/lib/action_controller/base.rb +1 -1
  9. data/lib/action_controller/form_builder.rb +3 -3
  10. data/lib/action_controller/metal/allow_browser.rb +11 -1
  11. data/lib/action_controller/metal/conditional_get.rb +5 -1
  12. data/lib/action_controller/metal/data_streaming.rb +4 -2
  13. data/lib/action_controller/metal/instrumentation.rb +1 -2
  14. data/lib/action_controller/metal/live.rb +59 -11
  15. data/lib/action_controller/metal/params_wrapper.rb +3 -3
  16. data/lib/action_controller/metal/rate_limiting.rb +13 -4
  17. data/lib/action_controller/metal/redirecting.rb +4 -3
  18. data/lib/action_controller/metal/renderers.rb +2 -3
  19. data/lib/action_controller/metal/rendering.rb +1 -1
  20. data/lib/action_controller/metal/request_forgery_protection.rb +3 -1
  21. data/lib/action_controller/metal/streaming.rb +5 -84
  22. data/lib/action_controller/metal/strong_parameters.rb +277 -92
  23. data/lib/action_controller/railtie.rb +6 -7
  24. data/lib/action_controller/renderer.rb +0 -1
  25. data/lib/action_controller/test_case.rb +12 -2
  26. data/lib/action_dispatch/constants.rb +6 -0
  27. data/lib/action_dispatch/http/cache.rb +27 -10
  28. data/lib/action_dispatch/http/content_security_policy.rb +14 -1
  29. data/lib/action_dispatch/http/mime_negotiation.rb +8 -3
  30. data/lib/action_dispatch/http/param_builder.rb +186 -0
  31. data/lib/action_dispatch/http/param_error.rb +26 -0
  32. data/lib/action_dispatch/http/permissions_policy.rb +2 -0
  33. data/lib/action_dispatch/http/query_parser.rb +53 -0
  34. data/lib/action_dispatch/http/request.rb +64 -19
  35. data/lib/action_dispatch/http/response.rb +49 -14
  36. data/lib/action_dispatch/http/url.rb +2 -2
  37. data/lib/action_dispatch/journey/formatter.rb +8 -3
  38. data/lib/action_dispatch/journey/gtg/transition_table.rb +4 -4
  39. data/lib/action_dispatch/journey/parser.rb +99 -196
  40. data/lib/action_dispatch/journey/scanner.rb +44 -42
  41. data/lib/action_dispatch/middleware/cookies.rb +4 -2
  42. data/lib/action_dispatch/middleware/debug_exceptions.rb +19 -4
  43. data/lib/action_dispatch/middleware/debug_view.rb +0 -5
  44. data/lib/action_dispatch/middleware/exception_wrapper.rb +3 -9
  45. data/lib/action_dispatch/middleware/executor.rb +5 -2
  46. data/lib/action_dispatch/middleware/public_exceptions.rb +5 -1
  47. data/lib/action_dispatch/middleware/request_id.rb +2 -1
  48. data/lib/action_dispatch/middleware/ssl.rb +13 -3
  49. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +0 -3
  50. data/lib/action_dispatch/railtie.rb +8 -0
  51. data/lib/action_dispatch/request/session.rb +1 -0
  52. data/lib/action_dispatch/request/utils.rb +9 -3
  53. data/lib/action_dispatch/routing/inspector.rb +1 -1
  54. data/lib/action_dispatch/routing/mapper.rb +96 -67
  55. data/lib/action_dispatch/routing/polymorphic_routes.rb +2 -2
  56. data/lib/action_dispatch/routing/route_set.rb +21 -10
  57. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  58. data/lib/action_dispatch/system_testing/browser.rb +12 -21
  59. data/lib/action_dispatch/testing/assertion_response.rb +1 -1
  60. data/lib/action_dispatch/testing/assertions/response.rb +12 -2
  61. data/lib/action_dispatch/testing/assertions/routing.rb +16 -12
  62. data/lib/action_dispatch/testing/integration.rb +20 -10
  63. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  64. data/lib/action_dispatch/testing/test_process.rb +1 -2
  65. data/lib/action_dispatch.rb +6 -4
  66. data/lib/action_pack/gem_version.rb +4 -4
  67. metadata +16 -38
  68. data/lib/action_dispatch/journey/parser.y +0 -50
  69. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -53,12 +53,53 @@ module ActionController
53
53
  # response.headers["Last-Modified"] = Time.now.httpdate # Add this line if your Rack version is 2.2.x
54
54
  # ...
55
55
  # end
56
+ #
57
+ # ## Streaming and Execution State
58
+ #
59
+ # When streaming, the action is executed in a separate thread. By default, this thread
60
+ # shares execution state from the parent thread.
61
+ #
62
+ # You can configure which execution state keys should be excluded from being shared
63
+ # using the `config.action_controller.live_streaming_excluded_keys` configuration:
64
+ #
65
+ # # config/application.rb
66
+ # config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
67
+ #
68
+ # This is useful when using ActionController::Live inside a `connected_to` block. For example,
69
+ # if the parent request is reading from a replica using `connected_to(role: :reading)`, you may
70
+ # want the streaming thread to use its own connection context instead of inheriting the read-only
71
+ # context:
72
+ #
73
+ # # Without configuration, streaming thread inherits read-only connection
74
+ # ActiveRecord::Base.connected_to(role: :reading) do
75
+ # @posts = Post.all
76
+ # render stream: true # Streaming thread cannot write to database
77
+ # end
78
+ #
79
+ # # With configuration, streaming thread gets fresh connection context
80
+ # # config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]
81
+ # ActiveRecord::Base.connected_to(role: :reading) do
82
+ # @posts = Post.all
83
+ # render stream: true # Streaming thread can write to database if needed
84
+ # end
85
+ #
86
+ # Common keys you might want to exclude:
87
+ # - `:active_record_connected_to_stack` - Database connection routing and roles
88
+ # - `:active_record_prohibit_shard_swapping` - Shard swapping restrictions
89
+ #
90
+ # By default, no keys are excluded to maintain backward compatibility.
56
91
  module Live
57
92
  extend ActiveSupport::Concern
58
93
 
94
+ mattr_accessor :live_streaming_excluded_keys, default: []
95
+
96
+ included do
97
+ class_attribute :live_streaming_excluded_keys, instance_accessor: false, default: Live.live_streaming_excluded_keys
98
+ end
99
+
59
100
  module ClassMethods
60
101
  def make_response!(request)
61
- if request.get_header("HTTP_VERSION") == "HTTP/1.0"
102
+ if (request.get_header("SERVER_PROTOCOL") || request.get_header("HTTP_VERSION")) == "HTTP/1.0"
62
103
  super
63
104
  else
64
105
  Live::Response.new.tap do |res|
@@ -171,12 +212,6 @@ module ActionController
171
212
  @ignore_disconnect = false
172
213
  end
173
214
 
174
- # ActionDispatch::Response delegates #to_ary to the internal
175
- # ActionDispatch::Response::Buffer, defining #to_ary is an indicator that the
176
- # response body can be buffered and/or cached by Rack middlewares, this is not
177
- # the case for Live responses so we undefine it for this Buffer subclass.
178
- undef_method :to_ary
179
-
180
215
  def write(string)
181
216
  unless @response.committed?
182
217
  @response.headers["Cache-Control"] ||= "no-cache"
@@ -288,7 +323,7 @@ module ActionController
288
323
  # Since we're processing the view in a different thread, copy the thread locals
289
324
  # from the main thread to the child thread. :'(
290
325
  locals.each { |k, v| t2[k] = v }
291
- ActiveSupport::IsolatedExecutionState.share_with(t1)
326
+ ActiveSupport::IsolatedExecutionState.share_with(t1, except: self.class.live_streaming_excluded_keys)
292
327
 
293
328
  begin
294
329
  super(name)
@@ -307,6 +342,9 @@ module ActionController
307
342
  error = e
308
343
  end
309
344
  ensure
345
+ ActiveSupport::IsolatedExecutionState.clear
346
+ clean_up_thread_locals(locals, t2)
347
+
310
348
  @_response.commit!
311
349
  end
312
350
  end
@@ -328,7 +366,8 @@ module ActionController
328
366
  # or other running data where you don't want the entire file buffered in memory
329
367
  # first. Similar to send_data, but where the data is generated live.
330
368
  #
331
- # Options:
369
+ # #### Options:
370
+ #
332
371
  # * `:filename` - suggests a filename for the browser to use.
333
372
  # * `:type` - specifies an HTTP content type. You can specify either a string
334
373
  # or a symbol for a registered type with `Mime::Type.register`, for example
@@ -371,11 +410,20 @@ module ActionController
371
410
  # data from the response bodies. Nobody should call this method except in Rails
372
411
  # internals. Seriously!
373
412
  def new_controller_thread # :nodoc:
374
- Thread.new {
413
+ ActionController::Live.live_thread_pool_executor.post do
375
414
  t2 = Thread.current
376
415
  t2.abort_on_exception = true
377
416
  yield
378
- }
417
+ end
418
+ end
419
+
420
+ # Ensure we clean up any thread locals we copied so that the thread can reused.
421
+ def clean_up_thread_locals(locals, thread) # :nodoc:
422
+ locals.each { |k, _| thread[k] = nil }
423
+ end
424
+
425
+ def self.live_thread_pool_executor
426
+ @live_thread_pool_executor ||= Concurrent::CachedThreadPool.new(name: "action_controller.live")
379
427
  end
380
428
 
381
429
  def log_error(exception)
@@ -198,14 +198,14 @@ module ActionController
198
198
  # # enables the parameter wrapper for XML format
199
199
  #
200
200
  # wrap_parameters :person
201
- # # wraps parameters into +params[:person]+ hash
201
+ # # wraps parameters into params[:person] hash
202
202
  #
203
203
  # wrap_parameters Person
204
204
  # # wraps parameters by determining the wrapper key from Person class
205
- # # (+person+, in this case) and the list of attribute names
205
+ # # (:person, in this case) and the list of attribute names
206
206
  #
207
207
  # wrap_parameters include: [:username, :title]
208
- # # wraps only +:username+ and +:title+ attributes from parameters.
208
+ # # wraps only :username and :title attributes from parameters.
209
209
  #
210
210
  # wrap_parameters false
211
211
  # # disables parameters wrapping for this controller altogether.
@@ -29,6 +29,9 @@ module ActionController # :nodoc:
29
29
  # datastore as your general caches, you can pass a custom store in the `store`
30
30
  # parameter.
31
31
  #
32
+ # If you want to use multiple rate limits per controller, you need to give each of
33
+ # them an explicit name via the `name:` option.
34
+ #
32
35
  # Examples:
33
36
  #
34
37
  # class SessionsController < ApplicationController
@@ -44,14 +47,20 @@ module ActionController # :nodoc:
44
47
  # RATE_LIMIT_STORE = ActiveSupport::Cache::RedisCacheStore.new(url: ENV["REDIS_URL"])
45
48
  # rate_limit to: 10, within: 3.minutes, store: RATE_LIMIT_STORE
46
49
  # end
47
- def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, **options)
48
- before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store) }, **options
50
+ #
51
+ # class SessionsController < ApplicationController
52
+ # rate_limit to: 3, within: 2.seconds, name: "short-term"
53
+ # rate_limit to: 10, within: 5.minutes, name: "long-term"
54
+ # end
55
+ def rate_limit(to:, within:, by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: cache_store, name: nil, **options)
56
+ before_action -> { rate_limiting(to: to, within: within, by: by, with: with, store: store, name: name) }, **options
49
57
  end
50
58
  end
51
59
 
52
60
  private
53
- def rate_limiting(to:, within:, by:, with:, store:)
54
- count = store.increment("rate-limit:#{controller_path}:#{instance_exec(&by)}", 1, expires_in: within)
61
+ def rate_limiting(to:, within:, by:, with:, store:, name:)
62
+ cache_key = ["rate-limit", controller_path, name, instance_exec(&by)].compact.join(":")
63
+ count = store.increment(cache_key, 1, expires_in: within)
55
64
  if count && count > to
56
65
  ActiveSupport::Notifications.instrument("rate_limit.action_controller", request: request) do
57
66
  instance_exec(&with)
@@ -106,13 +106,14 @@ module ActionController
106
106
 
107
107
  allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
108
108
 
109
- self.status = _extract_redirect_to_status(options, response_options)
109
+ proposed_status = _extract_redirect_to_status(options, response_options)
110
110
 
111
111
  redirect_to_location = _compute_redirect_to_location(request, options)
112
112
  _ensure_url_is_http_header_safe(redirect_to_location)
113
113
 
114
114
  self.location = _enforce_open_redirect_protection(redirect_to_location, allow_other_host: allow_other_host)
115
115
  self.response_body = ""
116
+ self.status = proposed_status
116
117
  end
117
118
 
118
119
  # Soft deprecated alias for #redirect_back_or_to where the `fallback_location`
@@ -211,9 +212,9 @@ module ActionController
211
212
 
212
213
  def _extract_redirect_to_status(options, response_options)
213
214
  if options.is_a?(Hash) && options.key?(:status)
214
- Rack::Utils.status_code(options.delete(:status))
215
+ ActionDispatch::Response.rack_status_code(options.delete(:status))
215
216
  elsif response_options.key?(:status)
216
- Rack::Utils.status_code(response_options[:status])
217
+ ActionDispatch::Response.rack_status_code(response_options[:status])
217
218
  else
218
219
  302
219
220
  end
@@ -2,8 +2,6 @@
2
2
 
3
3
  # :markup: markdown
4
4
 
5
- require "set"
6
-
7
5
  module ActionController
8
6
  # See Renderers.add
9
7
  def self.add_renderer(key, &block)
@@ -154,7 +152,8 @@ module ActionController
154
152
  end
155
153
 
156
154
  add :json do |json, options|
157
- json = json.to_json(options) unless json.kind_of?(String)
155
+ json_options = options.except(:callback, :content_type, :status)
156
+ json = json.to_json(json_options) unless json.kind_of?(String)
158
157
 
159
158
  if options[:callback].present?
160
159
  if media_type.nil? || media_type == Mime[:json]
@@ -232,7 +232,7 @@ module ActionController
232
232
  end
233
233
 
234
234
  if options[:status]
235
- options[:status] = Rack::Utils.status_code(options[:status])
235
+ options[:status] = ActionDispatch::Response.rack_status_code(options[:status])
236
236
  end
237
237
 
238
238
  super
@@ -142,6 +142,7 @@ module ActionController # :nodoc:
142
142
  #
143
143
  #
144
144
  # Built-in unverified request handling methods are:
145
+ #
145
146
  # * `:exception` - Raises ActionController::InvalidAuthenticityToken
146
147
  # exception.
147
148
  # * `:reset_session` - Resets the session.
@@ -170,6 +171,7 @@ module ActionController # :nodoc:
170
171
  #
171
172
  #
172
173
  # Built-in session token strategies are:
174
+ #
173
175
  # * `:session` - Store the CSRF token in the session. Used as default if
174
176
  # `:store` option is not specified.
175
177
  # * `:cookie` - Store the CSRF token in an encrypted cookie.
@@ -498,7 +500,7 @@ module ActionController # :nodoc:
498
500
  # Checks the client's masked token to see if it matches the session token.
499
501
  # Essentially the inverse of `masked_authenticity_token`.
500
502
  def valid_authenticity_token?(session, encoded_masked_token) # :doc:
501
- if encoded_masked_token.nil? || encoded_masked_token.empty? || !encoded_masked_token.is_a?(String)
503
+ if !encoded_masked_token.is_a?(String) || encoded_masked_token.empty?
502
504
  return false
503
505
  end
504
506
 
@@ -165,95 +165,16 @@ module ActionController # :nodoc:
165
165
  #
166
166
  # ## Web server support
167
167
  #
168
- # Not all web servers support streaming out-of-the-box. You need to check the
169
- # instructions for each of them.
170
- #
171
- # #### Unicorn
172
- #
173
- # Unicorn supports streaming but it needs to be configured. For this, you need
174
- # to create a config file as follow:
175
- #
176
- # # unicorn.config.rb
177
- # listen 3000, tcp_nopush: false
178
- #
179
- # And use it on initialization:
180
- #
181
- # unicorn_rails --config-file unicorn.config.rb
182
- #
183
- # You may also want to configure other parameters like `:tcp_nodelay`.
184
- #
185
- # For more information, please check the
186
- # [documentation](https://bogomips.org/unicorn/Unicorn/Configurator.html#method-
187
- # i-listen).
188
- #
189
- # If you are using Unicorn with NGINX, you may need to tweak NGINX. Streaming
190
- # should work out of the box on Rainbows.
191
- #
192
- # #### Passenger
193
- #
194
- # Phusion Passenger with NGINX, offers two streaming mechanisms out of the box.
195
- #
196
- # 1. NGINX response buffering mechanism which is dependent on the value of
197
- # `passenger_buffer_response` option (default is "off").
198
- # 2. Passenger buffering system which is always 'on' irrespective of the value
199
- # of `passenger_buffer_response`.
200
- #
201
- #
202
- # When `passenger_buffer_response` is turned "on", then streaming would be done
203
- # at the NGINX level which waits until the application is done sending the
204
- # response back to the client.
205
- #
206
- # For more information, please check the [documentation]
207
- # (https://www.phusionpassenger.com/docs/references/config_reference/nginx/#passenger_buffer_response).
168
+ # Rack 3+ compatible servers all support streaming.
208
169
  module Streaming
209
- class Body # :nodoc:
210
- TERM = "\r\n"
211
- TAIL = "0#{TERM}"
212
-
213
- # Store the response body to be chunked.
214
- def initialize(body)
215
- @body = body
216
- end
217
-
218
- # For each element yielded by the response body, yield the element in chunked
219
- # encoding.
220
- def each(&block)
221
- term = TERM
222
- @body.each do |chunk|
223
- size = chunk.bytesize
224
- next if size == 0
225
-
226
- yield [size.to_s(16), term, chunk.b, term].join
227
- end
228
- yield TAIL
229
- yield term
230
- end
231
-
232
- # Close the response body if the response body supports it.
233
- def close
234
- @body.close if @body.respond_to?(:close)
235
- end
236
- end
237
-
238
170
  private
239
- # Set proper cache control and transfer encoding when streaming
240
- def _process_options(options)
241
- super
242
- if options[:stream]
243
- if request.version == "HTTP/1.0"
244
- options.delete(:stream)
245
- else
246
- headers["Cache-Control"] ||= "no-cache"
247
- headers["Transfer-Encoding"] = "chunked"
248
- headers.delete("Content-Length")
249
- end
250
- end
251
- end
252
-
253
171
  # Call render_body if we are streaming instead of usual `render`.
254
172
  def _render_template(options)
255
173
  if options.delete(:stream)
256
- Body.new view_renderer.render_body(view_context, options)
174
+ # It shouldn't be necessary to set this.
175
+ headers["cache-control"] ||= "no-cache"
176
+
177
+ view_renderer.render_body(view_context, options)
257
178
  else
258
179
  super
259
180
  end