actionpack 8.1.0.beta1 → 8.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +105 -1
  3. data/lib/abstract_controller/base.rb +2 -1
  4. data/lib/abstract_controller/helpers.rb +1 -1
  5. data/lib/action_controller/api.rb +1 -0
  6. data/lib/action_controller/base.rb +1 -0
  7. data/lib/action_controller/log_subscriber.rb +11 -3
  8. data/lib/action_controller/metal/live.rb +9 -18
  9. data/lib/action_controller/metal/rate_limiting.rb +9 -3
  10. data/lib/action_controller/metal/redirecting.rb +45 -9
  11. data/lib/action_controller/railtie.rb +25 -2
  12. data/lib/action_controller/structured_event_subscriber.rb +112 -0
  13. data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
  14. data/lib/action_dispatch/http/url.rb +11 -11
  15. data/lib/action_dispatch/journey/gtg/transition_table.rb +9 -7
  16. data/lib/action_dispatch/log_subscriber.rb +7 -3
  17. data/lib/action_dispatch/middleware/remote_ip.rb +9 -4
  18. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -0
  19. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
  20. data/lib/action_dispatch/railtie.rb +4 -0
  21. data/lib/action_dispatch/routing/inspector.rb +79 -59
  22. data/lib/action_dispatch/routing/mapper.rb +4 -2
  23. data/lib/action_dispatch/routing/redirection.rb +10 -7
  24. data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
  25. data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
  26. data/lib/action_dispatch/testing/integration.rb +3 -4
  27. data/lib/action_dispatch/testing/request_encoder.rb +9 -9
  28. data/lib/action_dispatch.rb +8 -0
  29. data/lib/action_pack/gem_version.rb +1 -1
  30. metadata +12 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed45caa459109b7c6009c7b63f8be21dfe1965dc7bc745e8e7588579a9384391
4
- data.tar.gz: aabc44311112b16f6e9b8c3f34f391d1fc1abe91cb1b0df7e920dd0068cbc8b0
3
+ metadata.gz: 8a6a9e8f62251d8a2effa940639912fb8780489767c229427b614b7832cfa9db
4
+ data.tar.gz: 4a1143a6c62f275e05c6ed9d8f19f0361ca7a2c208045421f9e0d7e5f7e0403b
5
5
  SHA512:
6
- metadata.gz: 05dc45165c2451cf7a0bd23c7b5baf55ba3e970cde0877211b208f227b98e538237c373135f5d8c3ba905c18af5834d69086ab8e223a11f7d77fed3f0e067746
7
- data.tar.gz: 43cba6f2e00ce49bc0b4ef8172eb26b6571325dcfa5c8b5deebcec90cfad8f6d8d7d0804459dc29e5fd36073aa8f1b09348999e6258197360be3306d9c41134e
6
+ metadata.gz: 9aaa8fb203ce8b597633893fa89e55948012c7c9b8d0f8e73296fe589933dc31c047d11ff7170b905cef1e023ba5a9afb1f9c165c574bee54f90f73b5a707b31
7
+ data.tar.gz: db8d534af910c0bf937de64d457c24e20739c2e814b95363c9fae5726f723e7c2fc416aa94fc90754929f899095a6bdbc7f4bc9ef752010e49844cb7362d1dc7
data/CHANGELOG.md CHANGED
@@ -1,4 +1,108 @@
1
- ## Rails 8.1.0.beta1 (September 04, 2025) ##
1
+ ## Rails 8.1.0 (October 22, 2025) ##
2
+
3
+ * Submit test requests using `as: :html` with `Content-Type: x-www-form-urlencoded`
4
+
5
+ *Sean Doyle*
6
+
7
+ * Add link-local IP ranges to `ActionDispatch::RemoteIp` default proxies.
8
+
9
+ Link-local addresses (`169.254.0.0/16` for IPv4 and `fe80::/10` for IPv6)
10
+ are now included in the default trusted proxy list, similar to private IP ranges.
11
+
12
+ *Adam Daniels*
13
+
14
+ * `remote_ip` will no longer ignore IPs in X-Forwarded-For headers if they
15
+ are accompanied by port information.
16
+
17
+ *Duncan Brown*, *Prevenios Marinos*, *Masafumi Koba*, *Adam Daniels*
18
+
19
+ * Add `action_dispatch.verbose_redirect_logs` setting that logs where redirects were called from.
20
+
21
+ Similar to `active_record.verbose_query_logs` and `active_job.verbose_enqueue_logs`, this adds a line in your logs that shows where a redirect was called from.
22
+
23
+ Example:
24
+
25
+ ```
26
+ Redirected to http://localhost:3000/posts/1
27
+ ↳ app/controllers/posts_controller.rb:32:in `block (2 levels) in create'
28
+ ```
29
+
30
+ *Dennis Paagman*
31
+
32
+ * Add engine route filtering and better formatting in `bin/rails routes`.
33
+
34
+ Allow engine routes to be filterable in the routing inspector, and
35
+ improve formatting of engine routing output.
36
+
37
+ Before:
38
+ ```
39
+ > bin/rails routes -e engine_only
40
+ No routes were found for this grep pattern.
41
+ For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html.
42
+ ```
43
+
44
+ After:
45
+ ```
46
+ > bin/rails routes -e engine_only
47
+ Routes for application:
48
+ No routes were found for this grep pattern.
49
+ For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html.
50
+
51
+ Routes for Test::Engine:
52
+ Prefix Verb URI Pattern Controller#Action
53
+ engine GET /engine_only(.:format) a#b
54
+ ```
55
+
56
+ *Dennis Paagman*, *Gannon McGibbon*
57
+
58
+ * Add structured events for Action Pack and Action Dispatch:
59
+ - `action_dispatch.redirect`
60
+ - `action_controller.request_started`
61
+ - `action_controller.request_completed`
62
+ - `action_controller.callback_halted`
63
+ - `action_controller.rescue_from_handled`
64
+ - `action_controller.file_sent`
65
+ - `action_controller.redirected`
66
+ - `action_controller.data_sent`
67
+ - `action_controller.unpermitted_parameters`
68
+ - `action_controller.fragment_cache`
69
+
70
+ *Adrianna Chang*
71
+
72
+ * URL helpers for engines mounted at the application root handle `SCRIPT_NAME` correctly.
73
+
74
+ Fixed an issue where `SCRIPT_NAME` is not applied to paths generated for routes in an engine
75
+ mounted at "/".
76
+
77
+ *Mike Dalessio*
78
+
79
+ * Update `ActionController::Metal::RateLimiting` to support passing method names to `:by` and `:with`
80
+
81
+ ```ruby
82
+ class SignupsController < ApplicationController
83
+ rate_limit to: 10, within: 1.minute, with: :redirect_with_flash
84
+
85
+ private
86
+ def redirect_with_flash
87
+ redirect_to root_url, alert: "Too many requests!"
88
+ end
89
+ end
90
+ ```
91
+
92
+ *Sean Doyle*
93
+
94
+ * Optimize `ActionDispatch::Http::URL.build_host_url` when protocol is included in host.
95
+
96
+ When using URL helpers with a host that includes the protocol (e.g., `{ host: "https://example.com" }`),
97
+ skip unnecessary protocol normalization and string duplication since the extracted protocol is already
98
+ in the correct format. This eliminates 2 string allocations per URL generation and provides a ~10%
99
+ performance improvement for this case.
100
+
101
+ *Joshua Young*, *Hartley McGuire*
102
+
103
+ * Allow `action_controller.logger` to be disabled by setting it to `nil` or `false` instead of always defaulting to `Rails.logger`.
104
+
105
+ *Roberto Miranda*
2
106
 
3
107
  * Remove deprecated support to a route to multiple paths.
4
108
 
@@ -97,7 +97,8 @@ module AbstractController
97
97
  methods = public_instance_methods(true) - internal_methods
98
98
  # Be sure to include shadowed public instance methods of this class.
99
99
  methods.concat(public_instance_methods(false))
100
- methods.map!(&:to_s)
100
+ methods.reject! { |m| m.start_with?("_") }
101
+ methods.map!(&:name)
101
102
  methods.to_set
102
103
  end
103
104
  end
@@ -90,7 +90,7 @@ module AbstractController
90
90
  #--
91
91
  # Implemented by Resolution#modules_for_helpers.
92
92
 
93
- # :method: # all_helpers_from_path
93
+ # :method: all_helpers_from_path
94
94
  # :call-seq: all_helpers_from_path(path)
95
95
  #
96
96
  # Returns a list of helper names in a given path.
@@ -5,6 +5,7 @@
5
5
  require "action_view"
6
6
  require "action_controller"
7
7
  require "action_controller/log_subscriber"
8
+ require "action_controller/structured_event_subscriber"
8
9
 
9
10
  module ActionController
10
11
  # # Action Controller API
@@ -4,6 +4,7 @@
4
4
 
5
5
  require "action_view"
6
6
  require "action_controller/log_subscriber"
7
+ require "action_controller/structured_event_subscriber"
7
8
  require "action_controller/metal/params_wrapper"
8
9
 
9
10
  module ActionController
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # :markup: markdown
4
-
5
3
  module ActionController
6
- class LogSubscriber < ActiveSupport::LogSubscriber
4
+ class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
7
5
  INTERNAL_PARAMS = %w(controller action format _method only_path)
8
6
 
7
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
8
+
9
9
  def start_processing(event)
10
10
  return unless logger.info?
11
11
 
@@ -63,6 +63,10 @@ module ActionController
63
63
 
64
64
  def redirect_to(event)
65
65
  info { "Redirected to #{event.payload[:location]}" }
66
+
67
+ if ActionDispatch.verbose_redirect_logs && (source = redirect_source_location)
68
+ info { "↳ #{source}" }
69
+ end
66
70
  end
67
71
  subscribe_log_level :redirect_to, :info
68
72
 
@@ -97,6 +101,10 @@ module ActionController
97
101
  def logger
98
102
  ActionController::Base.logger
99
103
  end
104
+
105
+ def redirect_source_location
106
+ backtrace_cleaner.first_clean_frame
107
+ end
100
108
  end
101
109
  end
102
110
 
@@ -133,15 +133,16 @@ module ActionController
133
133
  private
134
134
  def perform_write(json, options)
135
135
  current_options = @options.merge(options).stringify_keys
136
-
136
+ event = +""
137
137
  PERMITTED_OPTIONS.each do |option_name|
138
138
  if (option_value = current_options[option_name])
139
- @stream.write "#{option_name}: #{option_value}\n"
139
+ event << "#{option_name}: #{option_value}\n"
140
140
  end
141
141
  end
142
142
 
143
143
  message = json.gsub("\n", "\ndata: ")
144
- @stream.write "data: #{message}\n\n"
144
+ event << "data: #{message}\n\n"
145
+ @stream.write event
145
146
  end
146
147
  end
147
148
 
@@ -236,12 +237,7 @@ module ActionController
236
237
 
237
238
  private
238
239
  def each_chunk(&block)
239
- loop do
240
- str = nil
241
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
242
- str = @buf.pop
243
- end
244
- break unless str
240
+ while str = @buf.pop
245
241
  yield str
246
242
  end
247
243
  end
@@ -275,16 +271,14 @@ module ActionController
275
271
  # This processes the action in a child thread. It lets us return the response
276
272
  # code and headers back up the Rack stack, and still process the body in
277
273
  # parallel with sending data to the client.
278
- new_controller_thread {
274
+ new_controller_thread do
279
275
  ActiveSupport::Dependencies.interlock.running do
280
276
  t2 = Thread.current
281
277
 
282
278
  # Since we're processing the view in a different thread, copy the thread locals
283
279
  # from the main thread to the child thread. :'(
284
280
  locals.each { |k, v| t2[k] = v }
285
- ActiveSupport::IsolatedExecutionState.share_with(t1)
286
-
287
- begin
281
+ ActiveSupport::IsolatedExecutionState.share_with(t1) do
288
282
  super(name)
289
283
  rescue => e
290
284
  if @_response.committed?
@@ -301,18 +295,15 @@ module ActionController
301
295
  error = e
302
296
  end
303
297
  ensure
304
- ActiveSupport::IsolatedExecutionState.clear
305
298
  clean_up_thread_locals(locals, t2)
306
299
 
307
300
  @_response.commit!
308
301
  end
309
302
  end
310
- }
311
-
312
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
313
- @_response.await_commit
314
303
  end
315
304
 
305
+ @_response.await_commit
306
+
316
307
  raise error if error
317
308
  end
318
309
 
@@ -45,7 +45,12 @@ module ActionController # :nodoc:
45
45
  #
46
46
  # class SignupsController < ApplicationController
47
47
  # rate_limit to: 1000, within: 10.seconds,
48
- # by: -> { request.domain }, with: -> { redirect_to busy_controller_url, alert: "Too many signups on domain!" }, only: :new
48
+ # by: -> { request.domain }, with: :redirect_to_busy, only: :new
49
+ #
50
+ # private
51
+ # def redirect_to_busy
52
+ # redirect_to busy_controller_url, alert: "Too many signups on domain!"
53
+ # end
49
54
  # end
50
55
  #
51
56
  # class APIController < ApplicationController
@@ -65,7 +70,8 @@ module ActionController # :nodoc:
65
70
 
66
71
  private
67
72
  def rate_limiting(to:, within:, by:, with:, store:, name:, scope:)
68
- by = instance_exec(&by)
73
+ by = by.is_a?(Symbol) ? send(by) : instance_exec(&by)
74
+
69
75
  cache_key = ["rate-limit", scope, name, by].compact.join(":")
70
76
  count = store.increment(cache_key, 1, expires_in: within)
71
77
  if count && count > to
@@ -78,7 +84,7 @@ module ActionController # :nodoc:
78
84
  name: name,
79
85
  scope: scope,
80
86
  cache_key: cache_key) do
81
- instance_exec(&with)
87
+ with.is_a?(Symbol) ? send(with) : instance_exec(&with)
82
88
  end
83
89
  end
84
90
  end
@@ -11,10 +11,23 @@ module ActionController
11
11
 
12
12
  class UnsafeRedirectError < StandardError; end
13
13
 
14
+ class OpenRedirectError < UnsafeRedirectError
15
+ def initialize(location)
16
+ super("Unsafe redirect to #{location.to_s.truncate(100).inspect}, pass allow_other_host: true to redirect anyway.")
17
+ end
18
+ end
19
+
20
+ class PathRelativeRedirectError < UnsafeRedirectError
21
+ def initialize(url)
22
+ super("Path relative URL redirect detected: #{url.inspect}")
23
+ end
24
+ end
25
+
14
26
  ILLEGAL_HEADER_VALUE_REGEX = /[\x00-\x08\x0A-\x1F]/
15
27
 
16
28
  included do
17
29
  mattr_accessor :raise_on_open_redirects, default: false
30
+ mattr_accessor :action_on_open_redirect, default: :log
18
31
  mattr_accessor :action_on_path_relative_redirect, default: :log
19
32
  class_attribute :_allowed_redirect_hosts, :allowed_redirect_hosts_permissions, instance_accessor: false, instance_predicate: false
20
33
  singleton_class.alias_method :allowed_redirect_hosts, :_allowed_redirect_hosts
@@ -95,16 +108,17 @@ module ActionController
95
108
  # ### Open Redirect protection
96
109
  #
97
110
  # By default, Rails protects against redirecting to external hosts for your
98
- # app's safety, so called open redirects. Note: this was a new default in Rails
99
- # 7.0, after upgrading opt-in by uncommenting the line with
100
- # `raise_on_open_redirects` in
101
- # `config/initializers/new_framework_defaults_7_0.rb`
111
+ # app's safety, so called open redirects.
102
112
  #
103
113
  # Here #redirect_to automatically validates the potentially-unsafe URL:
104
114
  #
105
115
  # redirect_to params[:redirect_url]
106
116
  #
107
- # Raises UnsafeRedirectError in the case of an unsafe redirect.
117
+ # The `action_on_open_redirect` configuration option controls the behavior when an unsafe
118
+ # redirect is detected:
119
+ # * `:log` - Logs a warning but allows the redirect
120
+ # * `:notify` - Sends an ActiveSupport notification for monitoring
121
+ # * `:raise` - Raises an UnsafeRedirectError
108
122
  #
109
123
  # To allow any external redirects pass `allow_other_host: true`, though using a
110
124
  # user-provided param in that case is unsafe.
@@ -137,7 +151,7 @@ module ActionController
137
151
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
138
152
  raise AbstractController::DoubleRenderError if response_body
139
153
 
140
- allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
154
+ allow_other_host = response_options.delete(:allow_other_host)
141
155
 
142
156
  proposed_status = _extract_redirect_to_status(options, response_options)
143
157
 
@@ -244,7 +258,9 @@ module ActionController
244
258
 
245
259
  private
246
260
  def _allow_other_host
247
- !raise_on_open_redirects
261
+ return false if raise_on_open_redirects
262
+
263
+ action_on_open_redirect != :raise
248
264
  end
249
265
 
250
266
  def _extract_redirect_to_status(options, response_options)
@@ -258,10 +274,30 @@ module ActionController
258
274
  end
259
275
 
260
276
  def _enforce_open_redirect_protection(location, allow_other_host:)
277
+ # Explictly allowed other host or host is in allow list allow redirect
261
278
  if allow_other_host || _url_host_allowed?(location)
262
279
  location
280
+ # Explicitly disallowed other host
281
+ elsif allow_other_host == false
282
+ raise OpenRedirectError.new(location)
283
+ # Configuration disallows other hosts
284
+ elsif !_allow_other_host
285
+ raise OpenRedirectError.new(location)
286
+ # Log but allow redirect
287
+ elsif action_on_open_redirect == :log
288
+ logger.warn "Open redirect to #{location.inspect} detected" if logger
289
+ location
290
+ # Notify but allow redirect
291
+ elsif action_on_open_redirect == :notify
292
+ ActiveSupport::Notifications.instrument("open_redirect.action_controller",
293
+ location: location,
294
+ request: request,
295
+ stack_trace: caller,
296
+ )
297
+ location
298
+ # Fall through, should not happen but raise for safety
263
299
  else
264
- raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
300
+ raise OpenRedirectError.new(location)
265
301
  end
266
302
  end
267
303
 
@@ -301,7 +337,7 @@ module ActionController
301
337
  stack_trace: caller
302
338
  )
303
339
  when :raise
304
- raise UnsafeRedirectError, message
340
+ raise PathRelativeRedirectError.new(url)
305
341
  end
306
342
  end
307
343
  end
@@ -12,7 +12,7 @@ require "action_view/railtie"
12
12
  module ActionController
13
13
  class Railtie < Rails::Railtie # :nodoc:
14
14
  config.action_controller = ActiveSupport::OrderedOptions.new
15
- config.action_controller.raise_on_open_redirects = false
15
+ config.action_controller.action_on_open_redirect = :log
16
16
  config.action_controller.action_on_path_relative_redirect = :log
17
17
  config.action_controller.log_query_tags_around_actions = true
18
18
  config.action_controller.wrap_parameters_by_default = false
@@ -57,7 +57,8 @@ module ActionController
57
57
  paths = app.config.paths
58
58
  options = app.config.action_controller
59
59
 
60
- options.logger ||= Rails.logger
60
+ options.logger = options.fetch(:logger, Rails.logger)
61
+
61
62
  options.cache_store ||= Rails.cache
62
63
 
63
64
  options.javascripts_dir ||= paths["public/javascripts"].first
@@ -103,6 +104,22 @@ module ActionController
103
104
  end
104
105
  end
105
106
 
107
+ initializer "action_controller.open_redirects" do |app|
108
+ ActiveSupport.on_load(:action_controller, run_once: true) do
109
+ if app.config.action_controller.has_key?(:raise_on_open_redirects)
110
+ ActiveSupport.deprecator.warn(<<~MSG.squish)
111
+ `raise_on_open_redirects` is deprecated and will be removed in a future Rails version.
112
+ Use `config.action_controller.action_on_open_redirect = :raise` instead.
113
+ MSG
114
+
115
+ # Fallback to the default behavior in case of `load_default` set `action_on_open_redirect`, but apps set `raise_on_open_redirects`.
116
+ if app.config.action_controller.raise_on_open_redirects == false && app.config.action_controller.action_on_open_redirect == :raise
117
+ self.action_on_open_redirect = :log
118
+ end
119
+ end
120
+ end
121
+ end
122
+
106
123
  initializer "action_controller.query_log_tags" do |app|
107
124
  query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
108
125
  app.config.active_record.query_log_tags_enabled &&
@@ -135,5 +152,11 @@ module ActionController
135
152
  ActionController::TestCase.executor_around_each_request = app.config.active_support.executor_around_test_case
136
153
  end
137
154
  end
155
+
156
+ initializer "action_controller.backtrace_cleaner" do
157
+ ActiveSupport.on_load(:action_controller) do
158
+ ActionController::LogSubscriber.backtrace_cleaner = Rails.backtrace_cleaner
159
+ end
160
+ end
138
161
  end
139
162
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionController
4
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
5
+ INTERNAL_PARAMS = %w(controller action format _method only_path)
6
+
7
+ def start_processing(event)
8
+ payload = event.payload
9
+ params = {}
10
+ payload[:params].each_pair do |k, v|
11
+ params[k] = v unless INTERNAL_PARAMS.include?(k)
12
+ end
13
+ format = payload[:format]
14
+ format = format.to_s.upcase if format.is_a?(Symbol)
15
+ format = "*/*" if format.nil?
16
+
17
+ emit_event("action_controller.request_started",
18
+ controller: payload[:controller],
19
+ action: payload[:action],
20
+ format:,
21
+ params:,
22
+ )
23
+ end
24
+
25
+ def process_action(event)
26
+ payload = event.payload
27
+ status = payload[:status]
28
+
29
+ if status.nil? && (exception_class_name = payload[:exception]&.first)
30
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
31
+ end
32
+
33
+ emit_event("action_controller.request_completed", {
34
+ controller: payload[:controller],
35
+ action: payload[:action],
36
+ status: status,
37
+ **additions_for(payload),
38
+ duration_ms: event.duration.round(2),
39
+ gc_time_ms: event.gc_time.round(1),
40
+ }.compact)
41
+ end
42
+
43
+ def halted_callback(event)
44
+ emit_event("action_controller.callback_halted", filter: event.payload[:filter])
45
+ end
46
+
47
+ def rescue_from_callback(event)
48
+ exception = event.payload[:exception]
49
+ emit_event("action_controller.rescue_from_handled",
50
+ exception_class: exception.class.name,
51
+ exception_message: exception.message,
52
+ exception_backtrace: exception.backtrace&.first&.delete_prefix("#{Rails.root}/")
53
+ )
54
+ end
55
+
56
+ def send_file(event)
57
+ emit_event("action_controller.file_sent", path: event.payload[:path], duration_ms: event.duration.round(1))
58
+ end
59
+
60
+ def redirect_to(event)
61
+ emit_event("action_controller.redirected", location: event.payload[:location])
62
+ end
63
+
64
+ def send_data(event)
65
+ emit_event("action_controller.data_sent", filename: event.payload[:filename], duration_ms: event.duration.round(1))
66
+ end
67
+
68
+ def unpermitted_parameters(event)
69
+ unpermitted_keys = event.payload[:keys]
70
+ context = event.payload[:context]
71
+
72
+ emit_debug_event("action_controller.unpermitted_parameters",
73
+ unpermitted_keys:,
74
+ context: context.except(:request)
75
+ )
76
+ end
77
+ debug_only :unpermitted_parameters
78
+
79
+ def write_fragment(event)
80
+ fragment_cache(__method__, event)
81
+ end
82
+
83
+ def read_fragment(event)
84
+ fragment_cache(__method__, event)
85
+ end
86
+
87
+ def exist_fragment?(event)
88
+ fragment_cache(__method__, event)
89
+ end
90
+
91
+ def expire_fragment(event)
92
+ fragment_cache(__method__, event)
93
+ end
94
+
95
+ private
96
+ def fragment_cache(method_name, event)
97
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
98
+
99
+ emit_event("action_controller.fragment_cache",
100
+ method: "#{method_name}",
101
+ key: key,
102
+ duration_ms: event.duration.round(1)
103
+ )
104
+ end
105
+
106
+ def additions_for(payload)
107
+ payload.slice(:view_runtime, :db_runtime, :queries_count, :cached_queries_count)
108
+ end
109
+ end
110
+ end
111
+
112
+ ActionController::StructuredEventSubscriber.attach_to :action_controller
@@ -91,7 +91,49 @@ module ActionDispatch
91
91
  end
92
92
  end
93
93
 
94
- # Sets the variant for template.
94
+ # Sets the \variant for the response template.
95
+ #
96
+ # When determining which template to render, Action View will incorporate
97
+ # all variants from the request. For example, if an
98
+ # `ArticlesController#index` action needs to respond to
99
+ # `request.variant = [:ios, :turbo_native]`, it will render the
100
+ # first template file it can find in the following list:
101
+ #
102
+ # - `app/views/articles/index.html+ios.erb`
103
+ # - `app/views/articles/index.html+turbo_native.erb`
104
+ # - `app/views/articles/index.html.erb`
105
+ #
106
+ # Variants add context to the requests that views render appropriately.
107
+ # Variant names are arbitrary, and can communicate anything from the
108
+ # request's platform (`:android`, `:ios`, `:linux`, `:macos`, `:windows`)
109
+ # to its browser (`:chrome`, `:edge`, `:firefox`, `:safari`), to the type
110
+ # of user (`:admin`, `:guest`, `:user`).
111
+ #
112
+ # Note: Adding many new variant templates with similarities to existing
113
+ # template files can make maintaining your view code more difficult.
114
+ #
115
+ # #### Parameters
116
+ #
117
+ # * `variant` - a symbol name or an array of symbol names for variants
118
+ # used to render the response template
119
+ #
120
+ # #### Examples
121
+ #
122
+ # class ApplicationController < ActionController::Base
123
+ # before_action :determine_variants
124
+ #
125
+ # private
126
+ # def determine_variants
127
+ # variants = []
128
+ #
129
+ # # some code to determine the variant(s) to use
130
+ #
131
+ # variants << :ios if request.user_agent.include?("iOS")
132
+ # variants << :turbo_native if request.user_agent.include?("Turbo Native")
133
+ #
134
+ # request.variant = variants
135
+ # end
136
+ # end
95
137
  def variant=(variant)
96
138
  variant = Array(variant)
97
139
 
@@ -102,6 +144,18 @@ module ActionDispatch
102
144
  end
103
145
  end
104
146
 
147
+ # Returns the \variant for the response template as an instance of
148
+ # ActiveSupport::ArrayInquirer.
149
+ #
150
+ # request.variant = :phone
151
+ # request.variant.phone? # => true
152
+ # request.variant.tablet? # => false
153
+ #
154
+ # request.variant = [:phone, :tablet]
155
+ # request.variant.phone? # => true
156
+ # request.variant.desktop? # => false
157
+ # request.variant.any?(:phone, :desktop) # => true
158
+ # request.variant.any?(:desktop, :watch) # => false
105
159
  def variant
106
160
  @variant ||= ActiveSupport::ArrayInquirer.new
107
161
  end