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) ⇒ Object
Build exception string for error reporting emails.
-
#mail_to_for_error_reporting(exception, options = {}) ⇒ Object
Generate mailto link for error reporting.
-
#render_400(_exception = nil) ⇒ Object
Render 400 Bad Request.
-
#render_404(exception = nil) ⇒ Object
Render 404 Not Found.
-
#render_406(_exception = nil) ⇒ Object
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) ⇒ Object
Render 410 Gone — tells search engines the resource is permanently removed.
-
#render_500(exception = nil) ⇒ Object
Render 500 Internal Server Error.
-
#render_invalid_authenticity_token(exception = nil) ⇒ Object
Render CSRF token invalid error (warning severity).
-
#render_ip_spoof_error(exception = nil) ⇒ Object
Render IP spoofing detection error (warning severity).
- #safe_referer_or_fallback ⇒ Object
Instance Method Details
#excp_string(exception, options) ⇒ Object
Build exception string for error reporting emails
255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'app/concerns/controllers/error_rendering.rb', line 255 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 = {}) ⇒ Object
Generate mailto link for error reporting
270 271 272 273 274 275 276 277 278 279 280 |
# File 'app/concerns/controllers/error_rendering.rb', line 270 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) ⇒ Object
Render 400 Bad Request
93 94 95 96 |
# File 'app/concerns/controllers/error_rendering.rb', line 93 def render_400(_exception = nil) reset_cloudflare_cache head :bad_request end |
#render_404(exception = nil) ⇒ Object
Render 404 Not Found
130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'app/concerns/controllers/error_rendering.rb', line 130 def render_404(exception = nil) @skip_analytics = true @exception = exception reset_cloudflare_cache 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) ⇒ Object
Render 406 Not Acceptable (for unsupported request formats)
Not reported to AppSignal - this is expected behavior when bots/clients request unsupported formats
100 101 102 103 |
# File 'app/concerns/controllers/error_rendering.rb', line 100 def render_406(_exception = nil) reset_cloudflare_cache head :not_acceptable end |
#render_410(_exception = nil) ⇒ Object
Render 410 Gone — tells search engines the resource is permanently removed
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'app/concerns/controllers/error_rendering.rb', line 106 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) ⇒ Object
Render 500 Internal Server Error
36 37 38 39 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 |
# File 'app/concerns/controllers/error_rendering.rb', line 36 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('')}") } @backtrace = @backtrace.map { |line| line.sub("#{Rails.root.join('')}", '') } 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(' ')}" } 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 end |
#render_invalid_authenticity_token(exception = nil) ⇒ Object
Render CSRF token invalid error (warning severity)
NOTE: We intentionally do NOT call reset_session here. Edge-cached pages
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.
172 173 174 175 176 177 178 179 180 |
# File 'app/concerns/controllers/error_rendering.rb', line 172 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) ⇒ Object
Render IP spoofing detection error (warning severity)
183 184 185 186 187 |
# File 'app/concerns/controllers/error_rendering.rb', line 183 def render_ip_spoof_error(exception = nil) ErrorReporting.from_controller(self).warning(exception, reason: 'ip_spoof_detected') reset_cloudflare_cache head :bad_request end |
#safe_referer_or_fallback ⇒ Object
241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'app/concerns/controllers/error_rendering.rb', line 241 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 |