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
@@ -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>
@@ -1948,8 +1948,10 @@ module ActionDispatch
1948
1948
  end
1949
1949
 
1950
1950
  scope_options = options.slice!(*RESOURCE_OPTIONS)
1951
- if !scope_options.empty? || !shallow.nil?
1952
- scope(**scope_options, shallow:) do
1951
+ scope_options[:shallow] = shallow unless shallow.nil?
1952
+
1953
+ unless scope_options.empty?
1954
+ scope(**scope_options) do
1953
1955
  public_send(method, resources.pop, **options, &block)
1954
1956
  end
1955
1957
  return true
@@ -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
  #
@@ -3,6 +3,7 @@
3
3
  # :markup: markdown
4
4
 
5
5
  require "nokogiri"
6
+ require "action_dispatch/http/mime_type"
6
7
 
7
8
  module ActionDispatch
8
9
  class RequestEncoder # :nodoc:
@@ -15,9 +16,9 @@ module ActionDispatch
15
16
 
16
17
  @encoders = { identity: IdentityEncoder.new }
17
18
 
18
- attr_reader :response_parser
19
+ attr_reader :response_parser, :content_type
19
20
 
20
- def initialize(mime_name, param_encoder, response_parser)
21
+ def initialize(mime_name, param_encoder, response_parser, content_type)
21
22
  @mime = Mime[mime_name]
22
23
 
23
24
  unless @mime
@@ -27,10 +28,7 @@ module ActionDispatch
27
28
 
28
29
  @response_parser = response_parser || -> body { body }
29
30
  @param_encoder = param_encoder || :"to_#{@mime.symbol}".to_proc
30
- end
31
-
32
- def content_type
33
- @mime.to_s
31
+ @content_type = content_type || @mime.to_s
34
32
  end
35
33
 
36
34
  def accept_header
@@ -50,11 +48,13 @@ module ActionDispatch
50
48
  @encoders[name] || @encoders[:identity]
51
49
  end
52
50
 
53
- def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil)
54
- @encoders[mime_name] = new(mime_name, param_encoder, response_parser)
51
+ def self.register_encoder(mime_name, param_encoder: nil, response_parser: nil, content_type: nil)
52
+ @encoders[mime_name] = new(mime_name, param_encoder, response_parser, content_type)
55
53
  end
56
54
 
57
- register_encoder :html, response_parser: -> body { Rails::Dom::Testing.html_document.parse(body) }
55
+ register_encoder :html, response_parser: -> body { Rails::Dom::Testing.html_document.parse(body) },
56
+ param_encoder: -> param { param },
57
+ content_type: Mime[:url_encoded_form].to_s
58
58
  register_encoder :json, response_parser: -> body { JSON.parse(body, object_class: ActiveSupport::HashWithIndifferentAccess) }
59
59
  end
60
60
  end
@@ -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 = nil
16
16
 
17
17
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
18
18
  end