Module: Controllers::ErrorRendering
- Extended by:
- ActiveSupport::Concern
- Included in:
- ApplicationController
- Defined in:
- app/concerns/controllers/error_rendering.rb
Overview
Unified error rendering for HTTP error responses
Provides consistent error pages across CRM and WWW with proper:
- Error reporting to AppSignal with appropriate severity
- Format-aware responses (HTML, JSON, Turbo Stream, etc.)
- Cache busting for error pages
Error Severity Mapping:
- 500 errors → web namespace (critical)
- CSRF/IP Spoof → web_warning namespace (warning)
- 404 errors → web_info namespace (informational)
Constant Summary collapse
- NON_CONTENT_PATH_PREFIXES =
Convert a 404 URL path to a Swiftype search query, but only for content-like paths.
Application routes (accounts, cart, sessions, etc.) are never real content pages —
auto-searching Swiftype for them produces noise and pollutes search analytics. %w[ accounts customer_sessions my_cart my_account my_quotes my_projects my_rooms my_orders my_contacts my_addresses preset_jobs globals api ahoy ].freeze
Instance Method Summary collapse
-
#excp_string(exception, options) ⇒ Array<String>
Build exception string for error reporting emails.
-
#mail_to_for_error_reporting(exception, options = {}) ⇒ String
Generate mailto link for error reporting.
-
#render_400(_exception = nil) ⇒ void
Render 400 Bad Request.
-
#render_404(exception = nil) ⇒ void
Render 404 Not Found.
-
#render_406(_exception = nil) ⇒ void
Render 406 Not Acceptable (for unsupported request formats) Not reported to AppSignal - this is expected behavior when bots/clients request unsupported formats.
-
#render_410(_exception = nil) ⇒ void
Render 410 Gone — tells search engines the resource is permanently removed.
-
#render_500(exception = nil) ⇒ void
Render 500 Internal Server Error.
-
#render_invalid_authenticity_token(exception = nil) ⇒ void
Render CSRF token invalid error (warning severity).
-
#render_ip_spoof_error(exception = nil) ⇒ void
Render IP spoofing detection error (warning severity).
-
#render_unpermitted_parameters(exception = nil) ⇒ void
Surface unpermitted strong-parameter input as a user-visible 4xx instead of a 500.
-
#safe_referer_or_fallback ⇒ String
Return the request referer if it is a same-host http(s) URL, falling back to the root path.
Instance Method Details
#excp_string(exception, options) ⇒ Array<String>
Build exception string for error reporting emails
366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'app/concerns/controllers/error_rendering.rb', line 366 def excp_string(exception, ) str = [] str << "Bug report url: #{[:bug_url]}" if [:bug_url] str << "Request: #{request&.request_method} #{request&.original_url}" str << "Request UUID: #{request&.uuid}" str << "Exception: #{exception&.to_s&.truncate(1000)}" if request&.request_parameters.present? str << 'Params:' request&.request_parameters&.each { |k, v| str << "#{k}: #{v}" } end str << '------ Put your comments below ------' str end |
#mail_to_for_error_reporting(exception, options = {}) ⇒ String
Generate mailto link for error reporting
391 392 393 394 395 396 397 398 399 400 401 |
# File 'app/concerns/controllers/error_rendering.rb', line 391 def mail_to_for_error_reporting(exception, = {}) str = excp_string(exception, ) link_name = [:link_name] || 'Send details to Heatwave team' css_classes = [:class] || 'btn btn-lg btn-primary' debug_info = str.join("\n") view_context.mail_to 'heatwaveteam@warmlyyours.com', link_name, class: css_classes, subject: "[HW] #{exception&.to_s&.truncate(100)}", body: debug_info end |
#render_400(_exception = nil) ⇒ void
This method returns an undefined value.
Render 400 Bad Request
108 109 110 111 |
# File 'app/concerns/controllers/error_rendering.rb', line 108 def render_400(_exception = nil) reset_cloudflare_cache head :bad_request end |
#render_404(exception = nil) ⇒ void
This method returns an undefined value.
Render 404 Not Found
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'app/concerns/controllers/error_rendering.rb', line 154 def render_404(exception = nil) @skip_analytics = true @exception = exception reset_cloudflare_cache # Self-healing rename: if this www URL is a prior path of a page that moved, # 301 to its current path instead of 404ing. Reported (not raised) so the # hot ones can later be promoted to Cloudflare bulk redirects. if !is_crm_request? && request.format.html? && (target = sitemap_history_redirect_target) ErrorReporting.from_controller(self).informational( "SiteMap history redirect: #{request.path} -> #{target}", reason: 'sitemap_history_redirect', from: request.path, to: target ) return redirect_to(cms_link(target), status: :moved_permanently, allow_other_host: false) end ErrorReporting.from_controller(self).informational(exception, reason: 'not_found') if exception.is_a?(ActiveRecord::RecordNotFound) || exception.is_a?(ActionController::RoutingError) if is_crm_request? @bug_url = nil layout_to_use = 'crm/crm' error_page = 'error_pages/crm/404' else layout_to_use = 'www/cms_page' error_page = 'error_pages/www/404' elements = request.path.split('/') - %w[en-US en-CA fr-CA] @q ||= search_query_from_path(elements) end respond_to do |format| format.html do flash[:error] = 'The requested page could not be found.' render error_page, status: :not_found, layout: layout_to_use end format.turbo_stream do flash[:error] = 'The requested resource could not be found.' render turbo_stream: [], status: :not_found end format.xml { head :not_found } format.json { render json: { not_found: '1', exception: exception.to_s } } format.any { head :not_found } end end |
#render_406(_exception = nil) ⇒ void
This method returns an undefined value.
Render 406 Not Acceptable (for unsupported request formats)
Not reported to AppSignal - this is expected behavior when bots/clients request unsupported formats
118 119 120 121 |
# File 'app/concerns/controllers/error_rendering.rb', line 118 def render_406(_exception = nil) reset_cloudflare_cache head :not_acceptable end |
#render_410(_exception = nil) ⇒ void
This method returns an undefined value.
Render 410 Gone — tells search engines the resource is permanently removed
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'app/concerns/controllers/error_rendering.rb', line 127 def render_410(_exception = nil) @skip_analytics = true reset_cloudflare_cache respond_to do |format| format.html do if is_crm_request? render 'error_pages/crm/404', status: :gone, layout: 'crm/crm' else elements = request.path.split('/') - %w[en-US en-CA fr-CA] @q ||= search_query_from_path(elements) render 'error_pages/www/404', status: :gone, layout: 'www/cms_page' end end format.turbo_stream do flash[:error] = 'This page has been permanently removed.' render turbo_stream: [], status: :gone end format.json { render json: { gone: true }, status: :gone } format.any { head :gone } end end |
#render_500(exception = nil) ⇒ void
This method returns an undefined value.
Render 500 Internal Server Error
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'app/concerns/controllers/error_rendering.rb', line 40 def render_500(exception = nil) ErrorReporting.from_controller(self).error(exception) if exception @bug_url = nil return if performed? if (@exception = exception) @backtrace = @exception.backtrace.select { |line| line.starts_with?(Rails.root.join.to_s) } @backtrace = @backtrace.map { |line| line.sub(Rails.root.join.to_s, '') } end if is_crm_request? layout_to_use = current_user ? 'crm/crm' : false error_page = 'error_pages/crm/500' js_payload = { error: [exception.to_s, (view_context.link_to('Report to Heatwaveteam', mail_to_for_error_reporting(@exception, bug_url: @bug_url)) if @bug_url)].compact.join(' ').to_s } else layout_to_use = false error_page = 'error_pages/www/500' js_payload = { error: exception.to_s } end reset_cloudflare_cache respond_to do |format| format.turbo_stream do if js_error_redirect? referrer = request.env['HTTP_REFERER'].presence @redirect_url = referrer || cms_link('/') render 'shared/redirect', status: :internal_server_error else flash[:error] = "We're sorry, but something went wrong." render turbo_stream: [], status: :internal_server_error end end format.html do if turbo_frame_request? if is_crm_request? && request.headers['Turbo-Frame'].present? render partial: 'shared/turbo_frame_error', locals: { frame_id: request.headers['Turbo-Frame'], exception: exception, show_details: Rails.env.in?(%w[development staging]) || current_user&.admin? }, status: :internal_server_error else render 'error_pages/turbo_frame_500', status: :internal_server_error, layout: false end else render error_page, status: :internal_server_error, layout: layout_to_use end end format.json { render json: js_payload } format.xml { head '500' } format.any { head '500' } end rescue ActionController::RespondToMismatchError # The original action already matched a format via its own `respond_to` # and then raised before rendering completed (so `@rendered_format` is # set). Rails 7 raises RespondToMismatchError if our `respond_to` above # negotiates a different format, which masks the underlying error and # produces a noisy secondary incident (AppSignal #2117). Fall back to # a bare 500 so the original error is what gets reported, not this one. head :internal_server_error end |
#render_invalid_authenticity_token(exception = nil) ⇒ void
We intentionally do NOT call reset_session here. Edge-cached pages
This method returns an undefined value.
Render CSRF token invalid error (warning severity)
lack a CSRF meta tag; after Turbo navigation there is a brief window
before globals.json injects a fresh token. A form POST during that window
triggers this handler. Calling reset_session turns a harmless stale-token
race into a real logout — which is the reported "logged out after search"
bug. The redirect already blocks the stale request; globals.json will
supply a valid token on the next page load.
209 210 211 212 213 214 215 216 217 |
# File 'app/concerns/controllers/error_rendering.rb', line 209 def render_invalid_authenticity_token(exception = nil) ErrorReporting.from_controller(self).warning(exception, reason: 'csrf_token_invalid') reset_cloudflare_cache respond_to do |format| format.html { redirect_to safe_referer_or_fallback, alert: 'Please try again.' } format.json { render json: { error: 'Invalid authenticity token' }, status: :unprocessable_entity } format.any { head :unprocessable_entity } end end |
#render_ip_spoof_error(exception = nil) ⇒ void
This method returns an undefined value.
Render IP spoofing detection error (warning severity)
260 261 262 263 264 |
# File 'app/concerns/controllers/error_rendering.rb', line 260 def render_ip_spoof_error(exception = nil) ErrorReporting.from_controller(self).warning(exception, reason: 'ip_spoof_detected') reset_cloudflare_cache head :bad_request end |
#render_unpermitted_parameters(exception = nil) ⇒ void
This method returns an undefined value.
Surface unpermitted strong-parameter input as a user-visible 4xx instead
of a 500.
config.action_controller.action_on_unpermitted_parameters = :raise
(config/application.rb) turns any shape mismatch — most commonly a
form rendering a single-select picker for a key the controller permits
as an Array (foo: []) — into a 500. AppSignal #5083 / #4972 are both
examples of that pattern. Until every report's permits and form widgets
are aligned, treat this as a 4xx-class user-visible error: flash through
TurboStreamFlashable, redirect HTML responses back to where they came
from, and report at warning severity so we still see incidents in
AppSignal but they don't page anyone.
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'app/concerns/controllers/error_rendering.rb', line 236 def render_unpermitted_parameters(exception = nil) ErrorReporting.from_controller(self).warning(exception, reason: 'unpermitted_parameters') if exception reset_cloudflare_cache = 'Some inputs were not recognized. Please check the form and try again.' respond_to do |format| format.turbo_stream do flash.now[:error] = render turbo_stream: [], status: :unprocessable_content end format.html do flash[:error] = redirect_to safe_referer_or_fallback end format.json { render json: { error: , params: exception&.params }, status: :unprocessable_content } format.any { head :unprocessable_content } end end |
#safe_referer_or_fallback ⇒ String
Return the request referer if it is a same-host http(s) URL, falling back
to the root path. Guards open-redirect on the CSRF-failure redirect.
346 347 348 349 350 351 352 353 354 355 356 357 |
# File 'app/concerns/controllers/error_rendering.rb', line 346 def safe_referer_or_fallback ref = request.referer return root_path if ref.blank? uri = URI.parse(ref) return root_path unless uri.scheme.nil? || %w[http https].include?(uri.scheme) return root_path unless uri.host.nil? || uri.host == request.host ref rescue URI::InvalidURIError root_path end |