actionpack 7.2.3.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +194 -120
  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 +4 -4
  48. metadata +14 -47
  49. data/lib/action_dispatch/journey/parser.y +0 -50
  50. data/lib/action_dispatch/journey/parser_extras.rb +0 -33
@@ -53,9 +53,50 @@ 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
102
  if (request.get_header("SERVER_PROTOCOL") || request.get_header("HTTP_VERSION")) == "HTTP/1.0"
@@ -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)
@@ -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)
@@ -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]
@@ -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