actionpack 7.0.8.1 → 7.2.2.1

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -500
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/abstract_controller/asset_paths.rb +2 -0
  6. data/lib/abstract_controller/base.rb +119 -106
  7. data/lib/abstract_controller/caching/fragments.rb +51 -52
  8. data/lib/abstract_controller/caching.rb +2 -0
  9. data/lib/abstract_controller/callbacks.rb +94 -67
  10. data/lib/abstract_controller/collector.rb +6 -6
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +2 -0
  13. data/lib/abstract_controller/helpers.rb +121 -91
  14. data/lib/abstract_controller/logger.rb +2 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +3 -16
  16. data/lib/abstract_controller/rendering.rb +14 -13
  17. data/lib/abstract_controller/translation.rb +12 -30
  18. data/lib/abstract_controller/url_for.rb +9 -5
  19. data/lib/abstract_controller.rb +8 -0
  20. data/lib/action_controller/api/api_rendering.rb +2 -0
  21. data/lib/action_controller/api.rb +78 -73
  22. data/lib/action_controller/base.rb +199 -141
  23. data/lib/action_controller/caching.rb +16 -11
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +21 -16
  26. data/lib/action_controller/log_subscriber.rb +19 -5
  27. data/lib/action_controller/metal/allow_browser.rb +123 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +2 -0
  29. data/lib/action_controller/metal/conditional_get.rb +187 -174
  30. data/lib/action_controller/metal/content_security_policy.rb +26 -25
  31. data/lib/action_controller/metal/cookies.rb +4 -2
  32. data/lib/action_controller/metal/data_streaming.rb +65 -54
  33. data/lib/action_controller/metal/default_headers.rb +6 -2
  34. data/lib/action_controller/metal/etag_with_flash.rb +4 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +18 -14
  36. data/lib/action_controller/metal/exceptions.rb +19 -9
  37. data/lib/action_controller/metal/flash.rb +12 -10
  38. data/lib/action_controller/metal/head.rb +20 -16
  39. data/lib/action_controller/metal/helpers.rb +64 -67
  40. data/lib/action_controller/metal/http_authentication.rb +214 -200
  41. data/lib/action_controller/metal/implicit_render.rb +21 -17
  42. data/lib/action_controller/metal/instrumentation.rb +22 -12
  43. data/lib/action_controller/metal/live.rb +125 -92
  44. data/lib/action_controller/metal/logging.rb +6 -4
  45. data/lib/action_controller/metal/mime_responds.rb +151 -142
  46. data/lib/action_controller/metal/parameter_encoding.rb +34 -32
  47. data/lib/action_controller/metal/params_wrapper.rb +58 -58
  48. data/lib/action_controller/metal/permissions_policy.rb +14 -13
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +110 -84
  51. data/lib/action_controller/metal/renderers.rb +50 -49
  52. data/lib/action_controller/metal/rendering.rb +103 -82
  53. data/lib/action_controller/metal/request_forgery_protection.rb +279 -161
  54. data/lib/action_controller/metal/rescue.rb +12 -8
  55. data/lib/action_controller/metal/streaming.rb +174 -132
  56. data/lib/action_controller/metal/strong_parameters.rb +598 -473
  57. data/lib/action_controller/metal/testing.rb +2 -0
  58. data/lib/action_controller/metal/url_for.rb +23 -14
  59. data/lib/action_controller/metal.rb +145 -61
  60. data/lib/action_controller/railtie.rb +25 -9
  61. data/lib/action_controller/railties/helpers.rb +2 -0
  62. data/lib/action_controller/renderer.rb +105 -66
  63. data/lib/action_controller/template_assertions.rb +4 -2
  64. data/lib/action_controller/test_case.rb +157 -128
  65. data/lib/action_controller.rb +17 -3
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +28 -29
  69. data/lib/action_dispatch/http/content_disposition.rb +2 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +69 -49
  71. data/lib/action_dispatch/http/filter_parameters.rb +27 -12
  72. data/lib/action_dispatch/http/filter_redirect.rb +22 -1
  73. data/lib/action_dispatch/http/headers.rb +23 -21
  74. data/lib/action_dispatch/http/mime_negotiation.rb +37 -48
  75. data/lib/action_dispatch/http/mime_type.rb +60 -30
  76. data/lib/action_dispatch/http/mime_types.rb +5 -1
  77. data/lib/action_dispatch/http/parameters.rb +12 -10
  78. data/lib/action_dispatch/http/permissions_policy.rb +32 -34
  79. data/lib/action_dispatch/http/rack_cache.rb +4 -0
  80. data/lib/action_dispatch/http/request.rb +132 -79
  81. data/lib/action_dispatch/http/response.rb +136 -103
  82. data/lib/action_dispatch/http/upload.rb +19 -15
  83. data/lib/action_dispatch/http/url.rb +75 -73
  84. data/lib/action_dispatch/journey/formatter.rb +19 -6
  85. data/lib/action_dispatch/journey/gtg/builder.rb +4 -3
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +2 -0
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +10 -8
  88. data/lib/action_dispatch/journey/nfa/dot.rb +2 -0
  89. data/lib/action_dispatch/journey/nodes/node.rb +6 -5
  90. data/lib/action_dispatch/journey/parser.rb +4 -3
  91. data/lib/action_dispatch/journey/parser_extras.rb +2 -0
  92. data/lib/action_dispatch/journey/path/pattern.rb +18 -15
  93. data/lib/action_dispatch/journey/route.rb +12 -9
  94. data/lib/action_dispatch/journey/router/utils.rb +16 -15
  95. data/lib/action_dispatch/journey/router.rb +13 -10
  96. data/lib/action_dispatch/journey/routes.rb +6 -4
  97. data/lib/action_dispatch/journey/scanner.rb +4 -2
  98. data/lib/action_dispatch/journey/visitors.rb +2 -0
  99. data/lib/action_dispatch/journey.rb +2 -0
  100. data/lib/action_dispatch/log_subscriber.rb +25 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +7 -6
  102. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  103. data/lib/action_dispatch/middleware/callbacks.rb +4 -0
  104. data/lib/action_dispatch/middleware/cookies.rb +192 -194
  105. data/lib/action_dispatch/middleware/debug_exceptions.rb +36 -27
  106. data/lib/action_dispatch/middleware/debug_locks.rb +18 -13
  107. data/lib/action_dispatch/middleware/debug_view.rb +9 -2
  108. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -27
  109. data/lib/action_dispatch/middleware/executor.rb +9 -1
  110. data/lib/action_dispatch/middleware/flash.rb +65 -46
  111. data/lib/action_dispatch/middleware/host_authorization.rb +22 -17
  112. data/lib/action_dispatch/middleware/public_exceptions.rb +12 -8
  113. data/lib/action_dispatch/middleware/reloader.rb +9 -5
  114. data/lib/action_dispatch/middleware/remote_ip.rb +88 -83
  115. data/lib/action_dispatch/middleware/request_id.rb +15 -8
  116. data/lib/action_dispatch/middleware/server_timing.rb +8 -6
  117. data/lib/action_dispatch/middleware/session/abstract_store.rb +7 -0
  118. data/lib/action_dispatch/middleware/session/cache_store.rb +14 -7
  119. data/lib/action_dispatch/middleware/session/cookie_store.rb +32 -25
  120. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +9 -3
  121. data/lib/action_dispatch/middleware/show_exceptions.rb +42 -28
  122. data/lib/action_dispatch/middleware/ssl.rb +60 -45
  123. data/lib/action_dispatch/middleware/stack.rb +15 -9
  124. data/lib/action_dispatch/middleware/static.rb +40 -34
  125. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +2 -2
  126. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +4 -4
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +8 -1
  128. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +7 -7
  129. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +2 -2
  130. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +17 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +16 -12
  132. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +3 -3
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +4 -4
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +1 -1
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +3 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +47 -38
  139. data/lib/action_dispatch/railtie.rb +12 -4
  140. data/lib/action_dispatch/request/session.rb +39 -27
  141. data/lib/action_dispatch/request/utils.rb +10 -3
  142. data/lib/action_dispatch/routing/endpoint.rb +2 -0
  143. data/lib/action_dispatch/routing/inspector.rb +59 -9
  144. data/lib/action_dispatch/routing/mapper.rb +686 -639
  145. data/lib/action_dispatch/routing/polymorphic_routes.rb +70 -61
  146. data/lib/action_dispatch/routing/redirection.rb +52 -38
  147. data/lib/action_dispatch/routing/route_set.rb +106 -62
  148. data/lib/action_dispatch/routing/routes_proxy.rb +16 -19
  149. data/lib/action_dispatch/routing/url_for.rb +131 -122
  150. data/lib/action_dispatch/routing.rb +152 -150
  151. data/lib/action_dispatch/system_test_case.rb +91 -81
  152. data/lib/action_dispatch/system_testing/browser.rb +27 -19
  153. data/lib/action_dispatch/system_testing/driver.rb +16 -22
  154. data/lib/action_dispatch/system_testing/server.rb +2 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +53 -31
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +2 -0
  157. data/lib/action_dispatch/testing/assertion_response.rb +9 -7
  158. data/lib/action_dispatch/testing/assertions/response.rb +36 -26
  159. data/lib/action_dispatch/testing/assertions/routing.rb +203 -95
  160. data/lib/action_dispatch/testing/assertions.rb +5 -1
  161. data/lib/action_dispatch/testing/integration.rb +240 -229
  162. data/lib/action_dispatch/testing/request_encoder.rb +6 -1
  163. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  164. data/lib/action_dispatch/testing/test_process.rb +14 -9
  165. data/lib/action_dispatch/testing/test_request.rb +4 -2
  166. data/lib/action_dispatch/testing/test_response.rb +34 -19
  167. data/lib/action_dispatch.rb +52 -21
  168. data/lib/action_pack/gem_version.rb +5 -3
  169. data/lib/action_pack/version.rb +3 -1
  170. data/lib/action_pack.rb +18 -17
  171. metadata +91 -32
@@ -1,13 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "action_dispatch/middleware/exception_wrapper"
4
6
  require "action_dispatch/routing/inspector"
5
7
 
6
8
  require "action_view"
7
9
 
8
10
  module ActionDispatch
9
- # This middleware is responsible for logging exceptions and
10
- # showing a debugging page in case the request is local.
11
+ # # Action Dispatch DebugExceptions
12
+ #
13
+ # This middleware is responsible for logging exceptions and showing a debugging
14
+ # page in case the request is local.
11
15
  class DebugExceptions
12
16
  cattr_reader :interceptors, instance_accessor: false, default: []
13
17
 
@@ -24,26 +28,26 @@ module ActionDispatch
24
28
  end
25
29
 
26
30
  def call(env)
27
- request = ActionDispatch::Request.new env
28
31
  _, headers, body = response = @app.call(env)
29
32
 
30
- if headers["X-Cascade"] == "pass"
33
+ if headers[Constants::X_CASCADE] == "pass"
31
34
  body.close if body.respond_to?(:close)
32
35
  raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
33
36
  end
34
37
 
35
38
  response
36
39
  rescue Exception => exception
37
- invoke_interceptors(request, exception)
38
- raise exception unless request.show_exceptions?
39
- render_exception(request, exception)
40
+ request = ActionDispatch::Request.new env
41
+ backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
42
+ wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
43
+
44
+ invoke_interceptors(request, exception, wrapper)
45
+ raise exception unless wrapper.show?(request)
46
+ render_exception(request, exception, wrapper)
40
47
  end
41
48
 
42
49
  private
43
- def invoke_interceptors(request, exception)
44
- backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
45
- wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
46
-
50
+ def invoke_interceptors(request, exception, wrapper)
47
51
  @interceptors.each do |interceptor|
48
52
  interceptor.call(request, exception)
49
53
  rescue Exception
@@ -51,9 +55,7 @@ module ActionDispatch
51
55
  end
52
56
  end
53
57
 
54
- def render_exception(request, exception)
55
- backtrace_cleaner = request.get_header("action_dispatch.backtrace_cleaner")
56
- wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
58
+ def render_exception(request, exception, wrapper)
57
59
  log_error(request, wrapper)
58
60
 
59
61
  if request.get_header("action_dispatch.show_detailed_exceptions")
@@ -94,7 +96,7 @@ module ActionDispatch
94
96
  wrapper.status_code,
95
97
  Rack::Utils::HTTP_STATUS_CODES[500]
96
98
  ),
97
- exception: wrapper.exception.inspect,
99
+ exception: wrapper.exception_inspect,
98
100
  traces: wrapper.traces
99
101
  }
100
102
 
@@ -115,19 +117,19 @@ module ActionDispatch
115
117
  DebugView.new(
116
118
  request: request,
117
119
  exception_wrapper: wrapper,
120
+ # Everything should use the wrapper, but we need to pass `exception` for legacy
121
+ # code.
118
122
  exception: wrapper.exception,
119
123
  traces: wrapper.traces,
120
124
  show_source_idx: wrapper.source_to_show_id,
121
125
  trace_to_show: wrapper.trace_to_show,
122
- routes_inspector: routes_inspector(wrapper.exception),
126
+ routes_inspector: routes_inspector(wrapper),
123
127
  source_extracts: wrapper.source_extracts,
124
- line_number: wrapper.line_number,
125
- file: wrapper.file
126
128
  )
127
129
  end
128
130
 
129
131
  def render(status, body, format)
130
- [status, { "Content-Type" => "#{format}; charset=#{Response.default_charset}", "Content-Length" => body.bytesize.to_s }, [body]]
132
+ [status, { Rack::CONTENT_TYPE => "#{format}; charset=#{Response.default_charset}", Rack::CONTENT_LENGTH => body.bytesize.to_s }, [body]]
131
133
  end
132
134
 
133
135
  def log_error(request, wrapper)
@@ -136,26 +138,33 @@ module ActionDispatch
136
138
  return unless logger
137
139
  return if !log_rescued_responses?(request) && wrapper.rescue_response?
138
140
 
139
- exception = wrapper.exception
140
141
  trace = wrapper.exception_trace
141
142
 
142
143
  message = []
143
144
  message << " "
144
- message << "#{exception.class} (#{exception.message}):"
145
- message.concat(exception.annotated_source_code) if exception.respond_to?(:annotated_source_code)
145
+ message << "#{wrapper.exception_class_name} (#{wrapper.message}):"
146
+ if wrapper.has_cause?
147
+ message << "\nCauses:"
148
+ wrapper.wrapped_causes.each do |wrapped_cause|
149
+ message << "#{wrapped_cause.exception_class_name} (#{wrapped_cause.message})"
150
+ end
151
+ end
152
+ message.concat(wrapper.annotated_source_code)
146
153
  message << " "
147
154
  message.concat(trace)
148
155
 
149
- log_array(logger, message)
156
+ log_array(logger, message, request)
150
157
  end
151
158
 
152
- def log_array(logger, lines)
159
+ def log_array(logger, lines, request)
153
160
  return if lines.empty?
154
161
 
162
+ level = request.get_header("action_dispatch.debug_exception_log_level")
163
+
155
164
  if logger.formatter && logger.formatter.respond_to?(:tags_text)
156
- logger.fatal lines.join("\n#{logger.formatter.tags_text}")
165
+ logger.add(level, lines.join("\n#{logger.formatter.tags_text}"))
157
166
  else
158
- logger.fatal lines.join("\n")
167
+ logger.add(level, lines.join("\n"))
159
168
  end
160
169
  end
161
170
 
@@ -168,7 +177,7 @@ module ActionDispatch
168
177
  end
169
178
 
170
179
  def routes_inspector(exception)
171
- if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
180
+ if @routes_app.respond_to?(:routes) && (exception.routing_error? || exception.template_error?)
172
181
  ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
173
182
  end
174
183
  end
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  module ActionDispatch
6
+ # # Action Dispatch DebugLocks
7
+ #
4
8
  # This middleware can be used to diagnose deadlocks in the autoload interlock.
5
9
  #
6
10
  # To use it, insert it near the top of the middleware stack, using
7
- # <tt>config/application.rb</tt>:
11
+ # `config/application.rb`:
8
12
  #
9
13
  # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
10
14
  #
11
- # After restarting the application and re-triggering the deadlock condition,
12
- # the route <tt>/rails/locks</tt> will show a summary of all threads currently
13
- # known to the interlock, which lock level they are holding or awaiting, and
14
- # their current backtrace.
15
+ # After restarting the application and re-triggering the deadlock condition, the
16
+ # route `/rails/locks` will show a summary of all threads currently known to the
17
+ # interlock, which lock level they are holding or awaiting, and their current
18
+ # backtrace.
15
19
  #
16
20
  # Generally a deadlock will be caused by the interlock conflicting with some
17
21
  # other external lock or blocking I/O call. These cannot be automatically
@@ -44,14 +48,14 @@ module ActionDispatch
44
48
  private
45
49
  def render_details(req)
46
50
  threads = ActiveSupport::Dependencies.interlock.raw_state do |raw_threads|
47
- # The Interlock itself comes to a complete halt as long as this block
48
- # is executing. That gives us a more consistent picture of everything,
49
- # but creates a pretty strong Observer Effect.
51
+ # The Interlock itself comes to a complete halt as long as this block is
52
+ # executing. That gives us a more consistent picture of everything, but creates
53
+ # a pretty strong Observer Effect.
50
54
  #
51
- # Most directly, that means we need to do as little as possible in
52
- # this block. More widely, it means this middleware should remain a
53
- # strictly diagnostic tool (to be used when something has gone wrong),
54
- # and not for any sort of general monitoring.
55
+ # Most directly, that means we need to do as little as possible in this block.
56
+ # More widely, it means this middleware should remain a strictly diagnostic tool
57
+ # (to be used when something has gone wrong), and not for any sort of general
58
+ # monitoring.
55
59
 
56
60
  raw_threads.each.with_index do |(thread, info), idx|
57
61
  info[:index] = idx
@@ -97,7 +101,8 @@ module ActionDispatch
97
101
  msg << "\n#{info[:backtrace].join("\n")}\n" if info[:backtrace]
98
102
  end.join("\n\n---\n\n\n")
99
103
 
100
- [200, { "Content-Type" => "text/plain", "Content-Length" => str.size }, [str]]
104
+ [200, { Rack::CONTENT_TYPE => "text/plain; charset=#{ActionDispatch::Response.default_charset}",
105
+ Rack::CONTENT_LENGTH => str.size.to_s }, [str]]
101
106
  end
102
107
 
103
108
  def blocked_by?(victim, blocker, all_threads)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "pp"
4
6
 
5
7
  require "action_view"
@@ -7,18 +9,23 @@ require "action_view/base"
7
9
 
8
10
  module ActionDispatch
9
11
  class DebugView < ActionView::Base # :nodoc:
10
- RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
12
+ RESCUES_TEMPLATE_PATHS = [File.expand_path("templates", __dir__)]
11
13
 
12
14
  def initialize(assigns)
13
- paths = [RESCUES_TEMPLATE_PATH]
15
+ paths = RESCUES_TEMPLATE_PATHS.dup
14
16
  lookup_context = ActionView::LookupContext.new(paths)
15
17
  super(lookup_context, assigns, nil)
18
+ @exception_wrapper = assigns[:exception_wrapper]
16
19
  end
17
20
 
18
21
  def compiled_method_container
19
22
  self.class
20
23
  end
21
24
 
25
+ def error_highlight_available?
26
+ @exception_wrapper.error_highlight_available?
27
+ end
28
+
22
29
  def debug_params(params)
23
30
  clean_params = params.clone
24
31
  clean_params.delete("action")
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "active_support/core_ext/module/attribute_accessors"
6
+ require "active_support/syntax_error_proxy"
7
+ require "active_support/core_ext/thread/backtrace/location"
4
8
  require "rack/utils"
5
9
 
6
10
  module ActionDispatch
@@ -41,22 +45,76 @@ module ActionDispatch
41
45
  "ActionDispatch::Http::MimeNegotiation::InvalidType"
42
46
  ]
43
47
 
44
- attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
48
+ attr_reader :backtrace_cleaner, :wrapped_causes, :exception_class_name, :exception
45
49
 
46
50
  def initialize(backtrace_cleaner, exception)
47
51
  @backtrace_cleaner = backtrace_cleaner
48
- @exception = exception
49
- @exception_class_name = @exception.class.name
52
+ @exception_class_name = exception.class.name
50
53
  @wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
54
+ @exception = exception
55
+ if exception.is_a?(SyntaxError)
56
+ @exception = ActiveSupport::SyntaxErrorProxy.new(exception)
57
+ end
58
+ @backtrace = build_backtrace
59
+ end
60
+
61
+ def routing_error?
62
+ @exception.is_a?(ActionController::RoutingError)
63
+ end
64
+
65
+ def template_error?
66
+ @exception.is_a?(ActionView::Template::Error)
67
+ end
68
+
69
+ def sub_template_message
70
+ @exception.sub_template_message
71
+ end
72
+
73
+ def has_cause?
74
+ @exception.cause
75
+ end
76
+
77
+ def failures
78
+ @exception.failures
79
+ end
80
+
81
+ def has_corrections?
82
+ @exception.respond_to?(:original_message) && @exception.respond_to?(:corrections)
83
+ end
84
+
85
+ def original_message
86
+ @exception.original_message
87
+ end
88
+
89
+ def corrections
90
+ @exception.corrections
91
+ end
51
92
 
52
- expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
93
+ def file_name
94
+ @exception.file_name
95
+ end
96
+
97
+ def line_number
98
+ @exception.line_number
99
+ end
100
+
101
+ def actions
102
+ ActiveSupport::ActionableError.actions(@exception)
53
103
  end
54
104
 
55
105
  def unwrapped_exception
56
106
  if wrapper_exceptions.include?(@exception_class_name)
57
- exception.cause
107
+ @exception.cause
58
108
  else
59
- exception
109
+ @exception
110
+ end
111
+ end
112
+
113
+ def annotated_source_code
114
+ if exception.respond_to?(:annotated_source_code)
115
+ exception.annotated_source_code
116
+ else
117
+ []
60
118
  end
61
119
  end
62
120
 
@@ -118,21 +176,37 @@ module ActionDispatch
118
176
  Rack::Utils.status_code(@@rescue_responses[class_name])
119
177
  end
120
178
 
179
+ def show?(request)
180
+ # We're treating `nil` as "unset", and we want the default setting to be `:all`.
181
+ # This logic should be extracted to `env_config` and calculated once.
182
+ config = request.get_header("action_dispatch.show_exceptions")
183
+
184
+ case config
185
+ when :none
186
+ false
187
+ when :rescuable
188
+ rescue_response?
189
+ else
190
+ true
191
+ end
192
+ end
193
+
121
194
  def rescue_response?
122
195
  @@rescue_responses.key?(exception.class.name)
123
196
  end
124
197
 
125
198
  def source_extracts
126
199
  backtrace.map do |trace|
127
- file, line_number = extract_file_and_line_number(trace)
128
-
129
- {
130
- code: source_fragment(file, line_number),
131
- line_number: line_number
132
- }
200
+ extract_source(trace)
133
201
  end
134
202
  end
135
203
 
204
+ def error_highlight_available?
205
+ # ErrorHighlight.spot with backtrace_location keyword is available since
206
+ # error_highlight 0.4.0
207
+ defined?(ErrorHighlight) && Gem::Version.new(ErrorHighlight::VERSION) >= Gem::Version.new("0.4.0")
208
+ end
209
+
136
210
  def trace_to_show
137
211
  if traces["Application Trace"].empty? && rescue_template != "routing_error"
138
212
  "Full Trace"
@@ -145,9 +219,65 @@ module ActionDispatch
145
219
  (traces[trace_to_show].first || {})[:id]
146
220
  end
147
221
 
222
+ def exception_name
223
+ exception.cause.class.to_s
224
+ end
225
+
226
+ def message
227
+ exception.message
228
+ end
229
+
230
+ def exception_inspect
231
+ exception.inspect
232
+ end
233
+
234
+ def exception_id
235
+ exception.object_id
236
+ end
237
+
148
238
  private
149
- def backtrace
150
- Array(@exception.backtrace)
239
+ class SourceMapLocation < DelegateClass(Thread::Backtrace::Location) # :nodoc:
240
+ def initialize(location, template)
241
+ super(location)
242
+ @template = template
243
+ end
244
+
245
+ def spot(exc)
246
+ if RubyVM::AbstractSyntaxTree.respond_to?(:node_id_for_backtrace_location) && __getobj__.is_a?(Thread::Backtrace::Location)
247
+ location = @template.spot(__getobj__)
248
+ else
249
+ location = super
250
+ end
251
+
252
+ if location
253
+ @template.translate_location(__getobj__, location)
254
+ end
255
+ end
256
+ end
257
+
258
+ attr_reader :backtrace
259
+
260
+ def build_backtrace
261
+ built_methods = {}
262
+
263
+ ActionView::PathRegistry.all_resolvers.each do |resolver|
264
+ resolver.built_templates.each do |template|
265
+ built_methods[template.method_name] = template
266
+ end
267
+ end
268
+
269
+ (@exception.backtrace_locations || []).map do |loc|
270
+ if built_methods.key?(loc.label.to_s)
271
+ thread_backtrace_location = if loc.respond_to?(:__getobj__)
272
+ loc.__getobj__
273
+ else
274
+ loc
275
+ end
276
+ SourceMapLocation.new(thread_backtrace_location, built_methods[loc.label.to_s])
277
+ else
278
+ loc
279
+ end
280
+ end
151
281
  end
152
282
 
153
283
  def causes_for(exception)
@@ -168,29 +298,53 @@ module ActionDispatch
168
298
  end
169
299
  end
170
300
 
301
+ def extract_source(trace)
302
+ spot = trace.spot(@exception)
303
+
304
+ if spot
305
+ line = spot[:first_lineno]
306
+ code = extract_source_fragment_lines(spot[:script_lines], line)
307
+
308
+ if line == spot[:last_lineno]
309
+ code[line] = [
310
+ code[line][0, spot[:first_column]],
311
+ code[line][spot[:first_column]...spot[:last_column]],
312
+ code[line][spot[:last_column]..-1],
313
+ ]
314
+ end
315
+
316
+ return {
317
+ code: code,
318
+ line_number: line
319
+ }
320
+ end
321
+
322
+ file, line_number = extract_file_and_line_number(trace)
323
+
324
+ {
325
+ code: source_fragment(file, line_number),
326
+ line_number: line_number
327
+ }
328
+ end
329
+
330
+ def extract_source_fragment_lines(source_lines, line)
331
+ start = [line - 3, 0].max
332
+ lines = source_lines.drop(start).take(6)
333
+ Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
334
+ end
335
+
171
336
  def source_fragment(path, line)
172
337
  return unless Rails.respond_to?(:root) && Rails.root
173
338
  full_path = Rails.root.join(path)
174
339
  if File.exist?(full_path)
175
340
  File.open(full_path, "r") do |file|
176
- start = [line - 3, 0].max
177
- lines = file.each_line.drop(start).take(6)
178
- Hash[*(start + 1..(lines.count + start)).zip(lines).flatten]
341
+ extract_source_fragment_lines(file.each_line, line)
179
342
  end
180
343
  end
181
344
  end
182
345
 
183
346
  def extract_file_and_line_number(trace)
184
- # Split by the first colon followed by some digits, which works for both
185
- # Windows and Unix path styles.
186
- file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
187
- [file, line.to_i]
188
- end
189
-
190
- def expand_backtrace
191
- @exception.backtrace.unshift(
192
- @exception.to_s.split("\n")
193
- ).flatten!
347
+ [trace.path, trace.lineno]
194
348
  end
195
349
  end
196
350
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # :markup: markdown
4
+
3
5
  require "rack/body_proxy"
4
6
 
5
7
  module ActionDispatch
@@ -12,9 +14,15 @@ module ActionDispatch
12
14
  state = @executor.run!(reset: true)
13
15
  begin
14
16
  response = @app.call(env)
17
+
18
+ if env["action_dispatch.report_exception"]
19
+ error = env["action_dispatch.exception"]
20
+ @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
21
+ end
22
+
15
23
  returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
16
24
  rescue => error
17
- @executor.error_reporter.report(error, handled: false)
25
+ @executor.error_reporter.report(error, handled: false, source: "application.action_dispatch")
18
26
  raise
19
27
  ensure
20
28
  state.complete! unless returned