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 +4 -4
- data/CHANGELOG.md +102 -0
- data/lib/abstract_controller/base.rb +2 -1
- data/lib/action_controller/api.rb +1 -0
- data/lib/action_controller/base.rb +1 -0
- data/lib/action_controller/log_subscriber.rb +11 -3
- data/lib/action_controller/metal/live.rb +9 -18
- data/lib/action_controller/metal/rate_limiting.rb +9 -3
- data/lib/action_controller/metal/redirecting.rb +44 -5
- data/lib/action_controller/railtie.rb +25 -2
- data/lib/action_controller/structured_event_subscriber.rb +107 -0
- data/lib/action_dispatch/http/mime_negotiation.rb +55 -1
- data/lib/action_dispatch/http/url.rb +11 -11
- data/lib/action_dispatch/journey/gtg/transition_table.rb +9 -7
- data/lib/action_dispatch/log_subscriber.rb +7 -3
- data/lib/action_dispatch/middleware/remote_ip.rb +9 -4
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +3 -0
- data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +3 -0
- data/lib/action_dispatch/railtie.rb +4 -0
- data/lib/action_dispatch/routing/inspector.rb +79 -59
- data/lib/action_dispatch/routing/redirection.rb +10 -7
- data/lib/action_dispatch/routing/routes_proxy.rb +1 -0
- data/lib/action_dispatch/structured_event_subscriber.rb +20 -0
- data/lib/action_dispatch/testing/integration.rb +3 -4
- data/lib/action_dispatch.rb +8 -0
- data/lib/action_pack/gem_version.rb +1 -1
- metadata +12 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c8381d10158212e4c059184fe5d92666b06c59d245fb71694323238d90eba282
|
4
|
+
data.tar.gz: 0dc561b01b220cee074c0278ce18630f60772998a20804314efff2531ae0c4fb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
100
|
+
methods.reject! { |m| m.start_with?("_") }
|
101
|
+
methods.map!(&:name)
|
101
102
|
methods.to_set
|
102
103
|
end
|
103
104
|
end
|
@@ -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
|
-
|
139
|
+
event << "#{option_name}: #{option_value}\n"
|
140
140
|
end
|
141
141
|
end
|
142
142
|
|
143
143
|
message = json.gsub("\n", "\ndata: ")
|
144
|
-
|
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
|
-
|
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:
|
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
|
-
#
|
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)
|
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
|
-
|
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
|
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
|
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.
|
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
|
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
|
-
|
206
|
-
host
|
207
|
-
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
|
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
|
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
|
-
|
221
|
-
|
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
|
268
|
+
when "//" then port
|
269
269
|
when "https://"
|
270
|
-
|
270
|
+
port unless port.to_i == 443
|
271
271
|
else
|
272
|
-
|
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,
|
201
|
+
[from, default_exp_anchored, to]
|
200
202
|
} + @regexp_states.flat_map { |from, hash|
|
201
|
-
hash.map { |
|
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 =
|
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
|
-
|
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
|
-
@
|
76
|
-
@
|
83
|
+
@routes = wrap_routes(routes)
|
84
|
+
@engines = load_engines_routes
|
77
85
|
end
|
78
86
|
|
79
87
|
def format(formatter, filter = {})
|
80
|
-
|
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
|
-
|
88
|
-
|
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
|
-
|
124
|
-
|
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
|
-
|
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
|
178
|
-
|
179
|
-
if routes.none?
|
180
|
-
<<~MESSAGE
|
181
|
-
You don't have any routes defined!
|
177
|
+
def footer(routes)
|
178
|
+
end
|
182
179
|
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
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 << "
|
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 << "
|
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
|
206
|
-
status
|
207
|
-
path
|
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
|
-
#
|
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
|
624
|
+
# assert_equal({ "id" => Article.last.id, "title" => "Ahoy!" }, response.parsed_body)
|
626
625
|
# end
|
627
626
|
# end
|
628
627
|
#
|
data/lib/action_dispatch.rb
CHANGED
@@ -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!
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
354
|
-
documentation_uri: https://api.rubyonrails.org/v8.1.0.
|
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.
|
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:
|