actionpack 8.1.0.beta1 → 8.1.0.rc1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed45caa459109b7c6009c7b63f8be21dfe1965dc7bc745e8e7588579a9384391
4
- data.tar.gz: aabc44311112b16f6e9b8c3f34f391d1fc1abe91cb1b0df7e920dd0068cbc8b0
3
+ metadata.gz: c8381d10158212e4c059184fe5d92666b06c59d245fb71694323238d90eba282
4
+ data.tar.gz: 0dc561b01b220cee074c0278ce18630f60772998a20804314efff2531ae0c4fb
5
5
  SHA512:
6
- metadata.gz: 05dc45165c2451cf7a0bd23c7b5baf55ba3e970cde0877211b208f227b98e538237c373135f5d8c3ba905c18af5834d69086ab8e223a11f7d77fed3f0e067746
7
- data.tar.gz: 43cba6f2e00ce49bc0b4ef8172eb26b6571325dcfa5c8b5deebcec90cfad8f6d8d7d0804459dc29e5fd36073aa8f1b09348999e6258197360be3306d9c41134e
6
+ metadata.gz: 901e992f68e437ee2b51ae434592ae48e1c1f90928d3274db316618abbd036ab5e019e03a94826ba6e9f1d7c84fbff1e8f3080e46a39eb5336c615f317bb1058
7
+ data.tar.gz: 56cf6df4674a40dfd5d1c5084ae3d65566d72f3db8e0b848217fceb2d3a5e0b488ac22f87fe457507d66346c330b9946b641117667ea7308854d0029a0e7ef8a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,105 @@
1
+ ## Rails 8.1.0.rc1 (October 15, 2025) ##
2
+
3
+ * Add link-local IP ranges to `ActionDispatch::RemoteIp` default proxies.
4
+
5
+ Link-local addresses (`169.254.0.0/16` for IPv4 and `fe80::/10` for IPv6)
6
+ are now included in the default trusted proxy list, similar to private IP ranges.
7
+
8
+ *Adam Daniels*
9
+
10
+ * `remote_ip` will no longer ignore IPs in X-Forwarded-For headers if they
11
+ are accompanied by port information.
12
+
13
+ *Duncan Brown*, *Prevenios Marinos*, *Masafumi Koba*, *Adam Daniels*
14
+
15
+ * Add `action_dispatch.verbose_redirect_logs` setting that logs where redirects were called from.
16
+
17
+ 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.
18
+
19
+ Example:
20
+
21
+ ```
22
+ Redirected to http://localhost:3000/posts/1
23
+ ↳ app/controllers/posts_controller.rb:32:in `block (2 levels) in create'
24
+ ```
25
+
26
+ *Dennis Paagman*
27
+
28
+ * Add engine route filtering and better formatting in `bin/rails routes`.
29
+
30
+ Allow engine routes to be filterable in the routing inspector, and
31
+ improve formatting of engine routing output.
32
+
33
+ Before:
34
+ ```
35
+ > bin/rails routes -e engine_only
36
+ No routes were found for this grep pattern.
37
+ For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html.
38
+ ```
39
+
40
+ After:
41
+ ```
42
+ > bin/rails routes -e engine_only
43
+ Routes for application:
44
+ No routes were found for this grep pattern.
45
+ For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html.
46
+
47
+ Routes for Test::Engine:
48
+ Prefix Verb URI Pattern Controller#Action
49
+ engine GET /engine_only(.:format) a#b
50
+ ```
51
+
52
+ *Dennis Paagman*, *Gannon McGibbon*
53
+
54
+ * Add structured events for Action Pack and Action Dispatch:
55
+ - `action_dispatch.redirect`
56
+ - `action_controller.request_started`
57
+ - `action_controller.request_completed`
58
+ - `action_controller.callback_halted`
59
+ - `action_controller.rescue_from_handled`
60
+ - `action_controller.file_sent`
61
+ - `action_controller.redirected`
62
+ - `action_controller.data_sent`
63
+ - `action_controller.unpermitted_parameters`
64
+ - `action_controller.fragment_cache`
65
+
66
+ *Adrianna Chang*
67
+
68
+ * URL helpers for engines mounted at the application root handle `SCRIPT_NAME` correctly.
69
+
70
+ Fixed an issue where `SCRIPT_NAME` is not applied to paths generated for routes in an engine
71
+ mounted at "/".
72
+
73
+ *Mike Dalessio*
74
+
75
+ * Update `ActionController::Metal::RateLimiting` to support passing method names to `:by` and `:with`
76
+
77
+ ```ruby
78
+ class SignupsController < ApplicationController
79
+ rate_limit to: 10, within: 1.minute, with: :redirect_with_flash
80
+
81
+ private
82
+ def redirect_with_flash
83
+ redirect_to root_url, alert: "Too many requests!"
84
+ end
85
+ end
86
+ ```
87
+
88
+ *Sean Doyle*
89
+
90
+ * Optimize `ActionDispatch::Http::URL.build_host_url` when protocol is included in host.
91
+
92
+ When using URL helpers with a host that includes the protocol (e.g., `{ host: "https://example.com" }`),
93
+ skip unnecessary protocol normalization and string duplication since the extracted protocol is already
94
+ in the correct format. This eliminates 2 string allocations per URL generation and provides a ~10%
95
+ performance improvement for this case.
96
+
97
+ *Joshua Young*, *Hartley McGuire*
98
+
99
+ * Allow `action_controller.logger` to be disabled by setting it to `nil` or `false` instead of always defaulting to `Rails.logger`.
100
+
101
+ *Roberto Miranda*
102
+
1
103
  ## Rails 8.1.0.beta1 (September 04, 2025) ##
2
104
 
3
105
  * Remove deprecated support to a route to multiple paths.
@@ -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
@@ -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
@@ -104,7 +117,11 @@ module ActionController
104
117
  #
105
118
  # redirect_to params[:redirect_url]
106
119
  #
107
- # Raises UnsafeRedirectError in the case of an unsafe redirect.
120
+ # The `action_on_open_redirect` configuration option controls the behavior when an unsafe
121
+ # redirect is detected:
122
+ # * `:log` - Logs a warning but allows the redirect
123
+ # * `:notify` - Sends an ActiveSupport notification for monitoring
124
+ # * `:raise` - Raises an UnsafeRedirectError
108
125
  #
109
126
  # To allow any external redirects pass `allow_other_host: true`, though using a
110
127
  # user-provided param in that case is unsafe.
@@ -137,7 +154,7 @@ module ActionController
137
154
  raise ActionControllerError.new("Cannot redirect to nil!") unless options
138
155
  raise AbstractController::DoubleRenderError if response_body
139
156
 
140
- allow_other_host = response_options.delete(:allow_other_host) { _allow_other_host }
157
+ allow_other_host = response_options.delete(:allow_other_host)
141
158
 
142
159
  proposed_status = _extract_redirect_to_status(options, response_options)
143
160
 
@@ -244,7 +261,9 @@ module ActionController
244
261
 
245
262
  private
246
263
  def _allow_other_host
247
- !raise_on_open_redirects
264
+ return false if raise_on_open_redirects
265
+
266
+ action_on_open_redirect != :raise
248
267
  end
249
268
 
250
269
  def _extract_redirect_to_status(options, response_options)
@@ -258,10 +277,30 @@ module ActionController
258
277
  end
259
278
 
260
279
  def _enforce_open_redirect_protection(location, allow_other_host:)
280
+ # Explictly allowed other host or host is in allow list allow redirect
261
281
  if allow_other_host || _url_host_allowed?(location)
262
282
  location
283
+ # Explicitly disallowed other host
284
+ elsif allow_other_host == false
285
+ raise OpenRedirectError.new(location)
286
+ # Configuration disallows other hosts
287
+ elsif !_allow_other_host
288
+ raise OpenRedirectError.new(location)
289
+ # Log but allow redirect
290
+ elsif action_on_open_redirect == :log
291
+ logger.warn "Open redirect to #{location.inspect} detected" if logger
292
+ location
293
+ # Notify but allow redirect
294
+ elsif action_on_open_redirect == :notify
295
+ ActiveSupport::Notifications.instrument("open_redirect.action_controller",
296
+ location: location,
297
+ request: request,
298
+ stack_trace: caller,
299
+ )
300
+ location
301
+ # Fall through, should not happen but raise for safety
263
302
  else
264
- raise UnsafeRedirectError, "Unsafe redirect to #{location.truncate(100).inspect}, pass allow_other_host: true to redirect anyway."
303
+ raise OpenRedirectError.new(location)
265
304
  end
266
305
  end
267
306
 
@@ -301,7 +340,7 @@ module ActionController
301
340
  stack_trace: caller
302
341
  )
303
342
  when :raise
304
- raise UnsafeRedirectError, message
343
+ raise PathRelativeRedirectError.new(url)
305
344
  end
306
345
  end
307
346
  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,107 @@
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
+ duration_ms: event.duration.round(2),
38
+ gc_time_ms: event.gc_time.round(1),
39
+ }.compact)
40
+ end
41
+
42
+ def halted_callback(event)
43
+ emit_event("action_controller.callback_halted", filter: event.payload[:filter])
44
+ end
45
+
46
+ def rescue_from_callback(event)
47
+ exception = event.payload[:exception]
48
+ emit_event("action_controller.rescue_from_handled",
49
+ exception_class: exception.class.name,
50
+ exception_message: exception.message,
51
+ exception_backtrace: exception.backtrace&.first&.delete_prefix("#{Rails.root}/")
52
+ )
53
+ end
54
+
55
+ def send_file(event)
56
+ emit_event("action_controller.file_sent", path: event.payload[:path], duration_ms: event.duration.round(1))
57
+ end
58
+
59
+ def redirect_to(event)
60
+ emit_event("action_controller.redirected", location: event.payload[:location])
61
+ end
62
+
63
+ def send_data(event)
64
+ emit_event("action_controller.data_sent", filename: event.payload[:filename], duration_ms: event.duration.round(1))
65
+ end
66
+
67
+ def unpermitted_parameters(event)
68
+ unpermitted_keys = event.payload[:keys]
69
+ context = event.payload[:context]
70
+
71
+ emit_debug_event("action_controller.unpermitted_parameters",
72
+ unpermitted_keys:,
73
+ context: context.except(:request)
74
+ )
75
+ end
76
+ debug_only :unpermitted_parameters
77
+
78
+ def write_fragment(event)
79
+ fragment_cache(__method__, event)
80
+ end
81
+
82
+ def read_fragment(event)
83
+ fragment_cache(__method__, event)
84
+ end
85
+
86
+ def exist_fragment?(event)
87
+ fragment_cache(__method__, event)
88
+ end
89
+
90
+ def expire_fragment(event)
91
+ fragment_cache(__method__, event)
92
+ end
93
+
94
+ private
95
+ def fragment_cache(method_name, event)
96
+ key = ActiveSupport::Cache.expand_cache_key(event.payload[:key] || event.payload[:path])
97
+
98
+ emit_event("action_controller.fragment_cache",
99
+ method: "#{method_name}",
100
+ key: key,
101
+ duration_ms: event.duration.round(1)
102
+ )
103
+ end
104
+ end
105
+ end
106
+
107
+ 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
@@ -202,24 +202,24 @@ module ActionDispatch
202
202
 
203
203
  def build_host_url(host, port, protocol, options, path)
204
204
  if match = host.match(HOST_REGEXP)
205
- protocol ||= match[1] unless protocol == false
206
- host = match[2]
207
- port = match[3] unless options.key? :port
205
+ protocol_from_host = match[1] if protocol.nil?
206
+ host = match[2]
207
+ port = match[3] unless options.key? :port
208
208
  end
209
209
 
210
- protocol = normalize_protocol protocol
210
+ protocol = protocol_from_host || normalize_protocol(protocol).dup
211
211
  host = normalize_host(host, options)
212
+ port = normalize_port(port, protocol)
212
213
 
213
- result = protocol.dup
214
+ result = protocol
214
215
 
215
216
  if options[:user] && options[:password]
216
217
  result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
217
218
  end
218
219
 
219
220
  result << host
220
- normalize_port(port, protocol) { |normalized_port|
221
- result << ":#{normalized_port}"
222
- }
221
+
222
+ result << ":" << port.to_s if port
223
223
 
224
224
  result.concat path
225
225
  end
@@ -265,11 +265,11 @@ module ActionDispatch
265
265
  return unless port
266
266
 
267
267
  case protocol
268
- when "//" then yield port
268
+ when "//" then port
269
269
  when "https://"
270
- yield port unless port.to_i == 443
270
+ port unless port.to_i == 443
271
271
  else
272
- yield port unless port.to_i == 80
272
+ port unless port.to_i == 80
273
273
  end
274
274
  end
275
275
  end
@@ -13,7 +13,6 @@ module ActionDispatch
13
13
  attr_reader :memos
14
14
 
15
15
  DEFAULT_EXP = /[^.\/?]+/
16
- DEFAULT_EXP_ANCHORED = /\A#{DEFAULT_EXP}\Z/
17
16
 
18
17
  def initialize
19
18
  @stdparam_states = {}
@@ -111,10 +110,10 @@ module ActionDispatch
111
110
  end
112
111
 
113
112
  {
114
- regexp_states: simple_regexp,
115
- string_states: @string_states,
116
- stdparam_states: @stdparam_states,
117
- accepting: @accepting
113
+ regexp_states: simple_regexp.stringify_keys,
114
+ string_states: @string_states.stringify_keys,
115
+ stdparam_states: @stdparam_states.stringify_keys,
116
+ accepting: @accepting.stringify_keys
118
117
  }
119
118
  end
120
119
 
@@ -193,12 +192,15 @@ module ActionDispatch
193
192
  end
194
193
 
195
194
  def transitions
195
+ # double escaped because dot evaluates escapes
196
+ default_exp_anchored = "\\\\A#{DEFAULT_EXP.source}\\\\Z"
197
+
196
198
  @string_states.flat_map { |from, hash|
197
199
  hash.map { |s, to| [from, s, to] }
198
200
  } + @stdparam_states.map { |from, to|
199
- [from, DEFAULT_EXP_ANCHORED, to]
201
+ [from, default_exp_anchored, to]
200
202
  } + @regexp_states.flat_map { |from, hash|
201
- hash.map { |s, to| [from, s, to] }
203
+ hash.map { |r, to| [from, r.source.gsub("\\") { "\\\\" }, to] }
202
204
  }
203
205
  end
204
206
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # :markup: markdown
4
-
5
3
  module ActionDispatch
6
- class LogSubscriber < ActiveSupport::LogSubscriber
4
+ class LogSubscriber < ActiveSupport::LogSubscriber # :nodoc:
5
+ class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new
6
+
7
7
  def redirect(event)
8
8
  payload = event.payload
9
9
 
10
10
  info { "Redirected to #{payload[:location]}" }
11
11
 
12
+ if ActionDispatch.verbose_redirect_logs
13
+ info { "↳ #{payload[:source_location]}" }
14
+ end
15
+
12
16
  info do
13
17
  status = payload[:status]
14
18
 
@@ -44,6 +44,8 @@ module ActionDispatch
44
44
  "10.0.0.0/8", # private IPv4 range 10.x.x.x
45
45
  "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
46
46
  "192.168.0.0/16", # private IPv4 range 192.168.x.x
47
+ "169.254.0.0/16", # link-local IPv4 range 169.254.x.x
48
+ "fe80::/10", # link-local IPv6 range fe80::/10
47
49
  ].map { |proxy| IPAddr.new(proxy) }
48
50
 
49
51
  attr_reader :check_ip, :proxies
@@ -126,11 +128,11 @@ module ActionDispatch
126
128
  # left, which was presumably set by one of those proxies.
127
129
  def calculate_ip
128
130
  # Set by the Rack web server, this is a single value.
129
- remote_addr = ips_from(@req.remote_addr).last
131
+ remote_addr = sanitize_ips(ips_from(@req.remote_addr)).last
130
132
 
131
133
  # Could be a CSV list and/or repeated headers that were concatenated.
132
- client_ips = ips_from(@req.client_ip).reverse!
133
- forwarded_ips = ips_from(@req.x_forwarded_for).reverse!
134
+ client_ips = sanitize_ips(ips_from(@req.client_ip)).reverse!
135
+ forwarded_ips = sanitize_ips(@req.forwarded_for || []).reverse!
134
136
 
135
137
  # `Client-Ip` and `X-Forwarded-For` should not, generally, both be set. If they
136
138
  # are both set, it means that either:
@@ -176,7 +178,10 @@ module ActionDispatch
176
178
  def ips_from(header) # :doc:
177
179
  return [] unless header
178
180
  # Split the comma-separated list into an array of strings.
179
- ips = header.strip.split(/[,\s]+/)
181
+ header.strip.split(/[,\s]+/)
182
+ end
183
+
184
+ def sanitize_ips(ips) # :doc:
180
185
  ips.select! do |ip|
181
186
  # Only return IPs that are valid according to the IPAddr#new method.
182
187
  range = IPAddr.new(ip).to_range
@@ -11,6 +11,9 @@
11
11
  <main role="main" id="container">
12
12
  <h2>
13
13
  <%= h @exception.message %>
14
+ <% if defined?(ActionText) && @exception.message.match?(%r{#{ActionText::RichText.table_name}}) %>
15
+ <br />To resolve this issue run: bin/rails action_text:install
16
+ <% end %>
14
17
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
15
18
  <br />To resolve this issue run: bin/rails active_storage:install
16
19
  <% end %>
@@ -4,6 +4,9 @@
4
4
  <% end %>
5
5
 
6
6
  <%= @exception.message %>
7
+ <% if defined?(ActionText) && @exception.message.match?(%r{#{ActionText::RichText.table_name}}) %>
8
+ To resolve this issue run: bin/rails action_text:install
9
+ <% end %>
7
10
  <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
8
11
  To resolve this issue run: bin/rails active_storage:install
9
12
  <% end %>
@@ -4,6 +4,7 @@
4
4
 
5
5
  require "action_dispatch"
6
6
  require "action_dispatch/log_subscriber"
7
+ require "action_dispatch/structured_event_subscriber"
7
8
  require "active_support/messages/rotation_configuration"
8
9
 
9
10
  module ActionDispatch
@@ -33,6 +34,7 @@ module ActionDispatch
33
34
 
34
35
  config.action_dispatch.ignore_leading_brackets = nil
35
36
  config.action_dispatch.strict_query_string_separator = nil
37
+ config.action_dispatch.verbose_redirect_logs = false
36
38
 
37
39
  config.action_dispatch.default_headers = {
38
40
  "X-Frame-Options" => "SAMEORIGIN",
@@ -66,6 +68,8 @@ module ActionDispatch
66
68
  ActionDispatch::QueryParser.strict_query_string_separator = app.config.action_dispatch.strict_query_string_separator
67
69
  end
68
70
 
71
+ ActionDispatch.verbose_redirect_logs = app.config.action_dispatch.verbose_redirect_logs
72
+
69
73
  ActiveSupport.on_load(:action_dispatch_request) do
70
74
  self.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
71
75
  ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
@@ -64,6 +64,14 @@ module ActionDispatch
64
64
  def engine?
65
65
  app.engine?
66
66
  end
67
+
68
+ def to_h
69
+ { name: name,
70
+ verb: verb,
71
+ path: path,
72
+ reqs: reqs,
73
+ source_location: source_location }
74
+ end
67
75
  end
68
76
 
69
77
  ##
@@ -72,33 +80,51 @@ module ActionDispatch
72
80
  # not use this class.
73
81
  class RoutesInspector # :nodoc:
74
82
  def initialize(routes)
75
- @engines = {}
76
- @routes = routes
83
+ @routes = wrap_routes(routes)
84
+ @engines = load_engines_routes
77
85
  end
78
86
 
79
87
  def format(formatter, filter = {})
80
- routes_to_display = filter_routes(normalize_filter(filter))
81
- routes = collect_routes(routes_to_display)
82
- if routes.none?
83
- formatter.no_routes(collect_routes(@routes), filter)
84
- return formatter.result
85
- end
88
+ all_routes = { nil => @routes }.merge(@engines)
86
89
 
87
- formatter.header routes
88
- formatter.section routes
89
-
90
- @engines.each do |name, engine_routes|
91
- formatter.section_title "Routes for #{name}"
92
- if engine_routes.any?
93
- formatter.header engine_routes
94
- formatter.section engine_routes
95
- end
90
+ all_routes.each do |engine_name, routes|
91
+ format_routes(formatter, filter, engine_name, routes)
96
92
  end
97
93
 
98
94
  formatter.result
99
95
  end
100
96
 
101
97
  private
98
+ def format_routes(formatter, filter, engine_name, routes)
99
+ routes = filter_routes(routes, normalize_filter(filter)).map(&:to_h)
100
+
101
+ formatter.section_title "Routes for #{engine_name || "application"}" if @engines.any?
102
+ if routes.any?
103
+ formatter.header routes
104
+ formatter.section routes
105
+ else
106
+ formatter.no_routes engine_name, routes, filter
107
+ end
108
+ formatter.footer routes
109
+ end
110
+
111
+ def wrap_routes(routes)
112
+ routes.routes.map { |route| RouteWrapper.new(route) }.reject(&:internal?)
113
+ end
114
+
115
+ def load_engines_routes
116
+ engine_routes = @routes.select(&:engine?)
117
+
118
+ engines = engine_routes.to_h do |engine_route|
119
+ engine_app_routes = engine_route.rack_app.routes
120
+ engine_app_routes = engine_app_routes.routes if engine_app_routes.is_a?(ActionDispatch::Routing::RouteSet)
121
+
122
+ [engine_route.endpoint, wrap_routes(engine_app_routes)]
123
+ end
124
+
125
+ engines
126
+ end
127
+
102
128
  def normalize_filter(filter)
103
129
  if filter[:controller]
104
130
  { controller: /#{filter[:controller].underscore.sub(/_?controller\z/, "")}/ }
@@ -118,39 +144,13 @@ module ActionDispatch
118
144
  end
119
145
  end
120
146
 
121
- def filter_routes(filter)
147
+ def filter_routes(routes, filter)
122
148
  if filter
123
- @routes.select do |route|
124
- route_wrapper = RouteWrapper.new(route)
125
- filter.any? { |filter_type, value| route_wrapper.matches_filter?(filter_type, value) }
149
+ routes.select do |route|
150
+ filter.any? { |filter_type, value| route.matches_filter?(filter_type, value) }
126
151
  end
127
152
  else
128
- @routes
129
- end
130
- end
131
-
132
- def collect_routes(routes)
133
- routes.collect do |route|
134
- RouteWrapper.new(route)
135
- end.reject(&:internal?).collect do |route|
136
- collect_engine_routes(route)
137
-
138
- { name: route.name,
139
- verb: route.verb,
140
- path: route.path,
141
- reqs: route.reqs,
142
- source_location: route.source_location }
143
- end
144
- end
145
-
146
- def collect_engine_routes(route)
147
- name = route.endpoint
148
- return unless route.engine?
149
- return if @engines[name]
150
-
151
- routes = route.rack_app.routes
152
- if routes.is_a?(ActionDispatch::Routing::RouteSet)
153
- @engines[name] = collect_routes(routes.routes)
153
+ routes
154
154
  end
155
155
  end
156
156
  end
@@ -174,27 +174,36 @@ module ActionDispatch
174
174
  def header(routes)
175
175
  end
176
176
 
177
- def no_routes(routes, filter)
178
- @buffer <<
179
- if routes.none?
180
- <<~MESSAGE
181
- You don't have any routes defined!
177
+ def footer(routes)
178
+ end
182
179
 
183
- Please add some routes in config/routes.rb.
184
- MESSAGE
185
- elsif filter.key?(:controller)
180
+ def no_routes(engine, routes, filter)
181
+ @buffer <<
182
+ if filter.key?(:controller)
186
183
  "No routes were found for this controller."
187
184
  elsif filter.key?(:grep)
188
185
  "No routes were found for this grep pattern."
186
+ elsif routes.none?
187
+ if engine
188
+ "No routes defined."
189
+ else
190
+ <<~MESSAGE
191
+ You don't have any routes defined!
192
+
193
+ Please add some routes in config/routes.rb.
194
+ MESSAGE
195
+ end
189
196
  end
190
197
 
191
- @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
198
+ unless engine
199
+ @buffer << "For more information about routes, see the Rails guide: https://guides.rubyonrails.org/routing.html."
200
+ end
192
201
  end
193
202
  end
194
203
 
195
204
  class Sheet < Base
196
205
  def section_title(title)
197
- @buffer << "\n#{title}:"
206
+ @buffer << "#{title}:"
198
207
  end
199
208
 
200
209
  def section(routes)
@@ -205,6 +214,10 @@ module ActionDispatch
205
214
  @buffer << draw_header(routes)
206
215
  end
207
216
 
217
+ def footer(routes)
218
+ @buffer << ""
219
+ end
220
+
208
221
  private
209
222
  def draw_section(routes)
210
223
  header_lengths = ["Prefix", "Verb", "URI Pattern"].map(&:length)
@@ -235,13 +248,17 @@ module ActionDispatch
235
248
  end
236
249
 
237
250
  def section_title(title)
238
- @buffer << "\n#{"[ #{title} ]"}"
251
+ @buffer << "#{"[ #{title} ]"}"
239
252
  end
240
253
 
241
254
  def section(routes)
242
255
  @buffer << draw_expanded_section(routes)
243
256
  end
244
257
 
258
+ def footer(routes)
259
+ @buffer << ""
260
+ end
261
+
245
262
  private
246
263
  def draw_expanded_section(routes)
247
264
  routes.map.each_with_index do |r, i|
@@ -272,7 +289,7 @@ module ActionDispatch
272
289
  super
273
290
  end
274
291
 
275
- def no_routes(routes, filter)
292
+ def no_routes(engine, routes, filter)
276
293
  @buffer <<
277
294
  if filter.none?
278
295
  "No unused routes found."
@@ -303,6 +320,9 @@ module ActionDispatch
303
320
  def header(routes)
304
321
  end
305
322
 
323
+ def footer(routes)
324
+ end
325
+
306
326
  def no_routes(*)
307
327
  @buffer << <<~MESSAGE
308
328
  <p>You don't have any routes defined!</p>
@@ -12,9 +12,10 @@ module ActionDispatch
12
12
  class Redirect < Endpoint # :nodoc:
13
13
  attr_reader :status, :block
14
14
 
15
- def initialize(status, block)
15
+ def initialize(status, block, source_location)
16
16
  @status = status
17
17
  @block = block
18
+ @source_location = source_location
18
19
  end
19
20
 
20
21
  def redirect?; true; end
@@ -27,6 +28,7 @@ module ActionDispatch
27
28
  payload[:status] = @status
28
29
  payload[:location] = response.headers["Location"]
29
30
  payload[:request] = request
31
+ payload[:source_location] = @source_location if @source_location
30
32
 
31
33
  response.to_a
32
34
  end
@@ -202,16 +204,17 @@ module ActionDispatch
202
204
  # get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
203
205
  #
204
206
  def redirect(*args, &block)
205
- options = args.extract_options!
206
- status = options.delete(:status) || 301
207
- path = args.shift
207
+ options = args.extract_options!
208
+ status = options.delete(:status) || 301
209
+ path = args.shift
210
+ source_location = caller[0] if ActionDispatch.verbose_redirect_logs
208
211
 
209
- return OptionRedirect.new(status, options) if options.any?
210
- return PathRedirect.new(status, path) if String === path
212
+ return OptionRedirect.new(status, options, source_location) if options.any?
213
+ return PathRedirect.new(status, path, source_location) if String === path
211
214
 
212
215
  block = path if path.respond_to? :call
213
216
  raise ArgumentError, "redirection argument not supported" unless block
214
- Redirect.new status, block
217
+ Redirect.new status, block, source_location
215
218
  end
216
219
  end
217
220
  end
@@ -54,6 +54,7 @@ module ActionDispatch
54
54
  # dependent part.
55
55
  def merge_script_names(previous_script_name, new_script_name)
56
56
  return new_script_name unless previous_script_name
57
+ new_script_name = new_script_name.chomp("/")
57
58
 
58
59
  resolved_parts = new_script_name.count("/")
59
60
  previous_parts = previous_script_name.count("/")
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ class StructuredEventSubscriber < ActiveSupport::StructuredEventSubscriber # :nodoc:
5
+ def redirect(event)
6
+ payload = event.payload
7
+ status = payload[:status]
8
+
9
+ emit_event("action_dispatch.redirect", {
10
+ location: payload[:location],
11
+ status: status,
12
+ status_name: Rack::Utils::HTTP_STATUS_CODES[status],
13
+ duration_ms: event.duration.round(2),
14
+ source_location: payload[:source_location]
15
+ })
16
+ end
17
+ end
18
+ end
19
+
20
+ ActionDispatch::StructuredEventSubscriber.attach_to :action_dispatch
@@ -604,9 +604,8 @@ module ActionDispatch
604
604
  # end
605
605
  # end
606
606
  #
607
- # See the [request helpers documentation]
608
- # (rdoc-ref:ActionDispatch::Integration::RequestHelpers) for help
609
- # on how to use `get`, etc.
607
+ # See the [request helpers documentation](rdoc-ref:ActionDispatch::Integration::RequestHelpers)
608
+ # for help on how to use `get`, etc.
610
609
  #
611
610
  # ### Changing the request encoding
612
611
  #
@@ -622,7 +621,7 @@ module ActionDispatch
622
621
  # end
623
622
  #
624
623
  # assert_response :success
625
- # assert_equal({ id: Article.last.id, title: "Ahoy!" }, response.parsed_body)
624
+ # assert_equal({ "id" => Article.last.id, "title" => "Ahoy!" }, response.parsed_body)
626
625
  # end
627
626
  # end
628
627
  #
@@ -138,6 +138,14 @@ module ActionDispatch
138
138
 
139
139
  autoload :SystemTestCase, "action_dispatch/system_test_case"
140
140
 
141
+ ##
142
+ # :singleton-method:
143
+ #
144
+ # Specifies if the methods calling redirects in controllers and routes should
145
+ # be logged below their relevant log lines. Defaults to false.
146
+ singleton_class.attr_accessor :verbose_redirect_logs
147
+ self.verbose_redirect_logs = false
148
+
141
149
  def eager_load!
142
150
  super
143
151
  Routing.eager_load!
@@ -12,7 +12,7 @@ module ActionPack
12
12
  MAJOR = 8
13
13
  MINOR = 1
14
14
  TINY = 0
15
- PRE = "beta1"
15
+ PRE = "rc1"
16
16
 
17
17
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
18
18
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: actionpack
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0.beta1
4
+ version: 8.1.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Heinemeier Hansson
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 8.1.0.beta1
18
+ version: 8.1.0.rc1
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 8.1.0.beta1
25
+ version: 8.1.0.rc1
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: nokogiri
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -127,28 +127,28 @@ dependencies:
127
127
  requirements:
128
128
  - - '='
129
129
  - !ruby/object:Gem::Version
130
- version: 8.1.0.beta1
130
+ version: 8.1.0.rc1
131
131
  type: :runtime
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
135
  - - '='
136
136
  - !ruby/object:Gem::Version
137
- version: 8.1.0.beta1
137
+ version: 8.1.0.rc1
138
138
  - !ruby/object:Gem::Dependency
139
139
  name: activemodel
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
142
  - - '='
143
143
  - !ruby/object:Gem::Version
144
- version: 8.1.0.beta1
144
+ version: 8.1.0.rc1
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
149
  - - '='
150
150
  - !ruby/object:Gem::Version
151
- version: 8.1.0.beta1
151
+ version: 8.1.0.rc1
152
152
  description: Web apps on Rails. Simple, battle-tested conventions for building and
153
153
  testing MVC web applications. Works with any Rack-compatible server.
154
154
  email: david@loudthinking.com
@@ -218,6 +218,7 @@ files:
218
218
  - lib/action_controller/railtie.rb
219
219
  - lib/action_controller/railties/helpers.rb
220
220
  - lib/action_controller/renderer.rb
221
+ - lib/action_controller/structured_event_subscriber.rb
221
222
  - lib/action_controller/template_assertions.rb
222
223
  - lib/action_controller/test_case.rb
223
224
  - lib/action_dispatch.rb
@@ -326,6 +327,7 @@ files:
326
327
  - lib/action_dispatch/routing/route_set.rb
327
328
  - lib/action_dispatch/routing/routes_proxy.rb
328
329
  - lib/action_dispatch/routing/url_for.rb
330
+ - lib/action_dispatch/structured_event_subscriber.rb
329
331
  - lib/action_dispatch/system_test_case.rb
330
332
  - lib/action_dispatch/system_testing/browser.rb
331
333
  - lib/action_dispatch/system_testing/driver.rb
@@ -350,10 +352,10 @@ licenses:
350
352
  - MIT
351
353
  metadata:
352
354
  bug_tracker_uri: https://github.com/rails/rails/issues
353
- changelog_uri: https://github.com/rails/rails/blob/v8.1.0.beta1/actionpack/CHANGELOG.md
354
- documentation_uri: https://api.rubyonrails.org/v8.1.0.beta1/
355
+ changelog_uri: https://github.com/rails/rails/blob/v8.1.0.rc1/actionpack/CHANGELOG.md
356
+ documentation_uri: https://api.rubyonrails.org/v8.1.0.rc1/
355
357
  mailing_list_uri: https://discuss.rubyonrails.org/c/rubyonrails-talk
356
- source_code_uri: https://github.com/rails/rails/tree/v8.1.0.beta1/actionpack
358
+ source_code_uri: https://github.com/rails/rails/tree/v8.1.0.rc1/actionpack
357
359
  rubygems_mfa_required: 'true'
358
360
  rdoc_options: []
359
361
  require_paths: