Module: Wrangler

Defined in:
lib/wrangler/wrangler_exceptions.rb,
lib/wrangler.rb,
lib/wrangler/wrangler_helper.rb,
lib/wrangler/exception_handler.rb,
lib/wrangler/exception_notifier.rb

Overview

a bunch of handy exceptions. using the Http ones will require rails

Defined Under Namespace

Modules: ControllerMethods, LocalControllerMethods, PublicControllerMethods Classes: ExceptionHandler, ExceptionNotifier, HttpForbidden, HttpInternalServerError, HttpNotAcceptable, HttpNotFound, HttpNotImplemented, HttpServiceUnavailable, HttpStatusError, HttpUnauthorized

Constant Summary collapse

WRANGLER_ROOT =
"#{File.dirname(__FILE__)}/../.."

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.class_has_ancestor?(klass, other_klasses) ⇒ Boolean

utility to determine if a class has another class as its ancestor.

returns the ancestor class (if any) that is found to be the ancestor of klass (will return the nearest ancestor in other_klasses). returns nil or false otherwise.

arguments:

- klass: the class we're determining whether it has one of the other
         classes as an ancestor
- other_klasses: a Class, an Array (or any other container that responds
                 to include?() ) of Classes

Returns:

  • (Boolean)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/wrangler/wrangler_helper.rb', line 28

def class_has_ancestor?(klass, other_klasses)
  return nil if !klass.is_a?(Class)

  other_klasses = [other_klasses] unless other_klasses.is_a?(Array)
  if other_klasses.first.is_a?(Class)
    other_klasses.map! { |k| k.name }
  end

  current_klass = klass
  while current_klass
    return current_klass if other_klasses.include?(current_klass.name)
    current_klass = current_klass.superclass
  end

  return false
end

.codes_for_exception_classesObject

A utility method that should only be used internal to wrangler. don’t call this; it should only be called once by the Config class and you can get/set it there. returns a mapping from exception classes to http status codes




7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/wrangler/exception_handler.rb', line 7

def self.codes_for_exception_classes
  classes = {
    # These are standard errors in rails / ruby
    NameError.name =>      "503",
    TypeError.name =>      "503",
    RuntimeError.name =>   "500",
    ArgumentError.name =>  "500",
    # the default mapping for an unrecognized exception class
    :default => "500"
  }

  # from exception_notification gem:
  # Highly dependent on the verison of rails, so we're very protective about these'
  classes.merge!({ ActionView::TemplateError.name => "500"})             if defined?(ActionView)       && ActionView.const_defined?(:TemplateError)
  classes.merge!({ ActiveRecord::RecordNotFound.name => "400" })         if defined?(ActiveRecord)     && ActiveRecord.const_defined?(:RecordNotFound)
  classes.merge!({ ActiveResource::ResourceNotFound.name => "404" })     if defined?(ActiveResource)   && ActiveResource.const_defined?(:ResourceNotFound)

  # from exception_notification gem:
  if defined?(ActionController)
    classes.merge!({ ActionController::UnknownController.name => "404" })          if ActionController.const_defined?(:UnknownController)
    classes.merge!({ ActionController::MissingTemplate.name => "404" })            if ActionController.const_defined?(:MissingTemplate)
    classes.merge!({ ActionController::MethodNotAllowed.name => "405" })           if ActionController.const_defined?(:MethodNotAllowed)
    classes.merge!({ ActionController::UnknownAction.name => "501" })              if ActionController.const_defined?(:UnknownAction)
    classes.merge!({ ActionController::RoutingError.name => "404" })               if ActionController.const_defined?(:RoutingError)
    classes.merge!({ ActionController::InvalidAuthenticityToken.name => "405" })   if ActionController.const_defined?(:InvalidAuthenticityToken)
  end

  return classes
end

.configObject

shorthand access to the exception handling config




117
118
119
# File 'lib/wrangler/wrangler_helper.rb', line 117

def config
  Wrangler::ExceptionHandler.config
end

.find_file_matching_pattern(search_dirs, pattern) ⇒ Object

given an array of search directory strings (or a single directory string), searches for files matching pattern.

pattern expressed in cmd line wildcards…like “*.rb” or “foo.?”… and may contain subdirectories.




52
53
54
55
56
57
58
59
60
# File 'lib/wrangler/wrangler_helper.rb', line 52

def find_file_matching_pattern(search_dirs, pattern)
  search_dirs = [search_dirs] unless search_dirs.is_a?(Array)

  search_dirs.each do |d|
    matches = Dir.glob(File.join(d, pattern))
    return matches.first if matches.size > 0
  end
  return nil
end

.handle_error(error_messages, options = {}) ⇒ Object

publicly available method for explicitly telling wrangler to handle a specific error condition without an actual exception. it’s useful if you want to send a notification after detecting an error condition, but don’t want to interrupt the stack by raising an exception. if you did catch an exception and want to do somethign similar, just call handle_exception diretly.

the error condition will get logged and may result in notification, according to configuration see notify_on_exception?

arguments

error_messages

a message or array of messages (each gets logged on separate log call) capturing the error condition that occurred. this will get logged AND sent in any notifications sent

options

also, any of the options accepted by handle_exception




250
251
252
253
# File 'lib/wrangler/exception_handler.rb', line 250

def handle_error(error_messages, options = {})
  options.merge! :error_messages => error_messages
  handle_exception(nil, options)
end

.handle_exception(exception, options = {}) ⇒ Object

the main exception-handling method. decides whether to notify or not, whether to render an error page or not, and to make it happen.

arguments

exception

the exception that was caught. can be nil, but should only be nil if notifications should always be sent, as notification rules are bypassed this case

options

error_messages

any additional message to log and send in notification. can also be an array of messages (each gets logged separately)

request

the request object (if any) that resulted in the exception

render_errors

boolean indicating if an error page should be rendered or not (Rails only). default => false

proc_name

a string representation of the process/app that was running when the exception was raised. default value is Wrangler::ExceptionHandler.config.




275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/wrangler/exception_handler.rb', line 275

def handle_exception(exception, options = {})
  request = options[:request]
  render_errors = options[:render_errors] || false
  proc_name = options[:proc_name] || config[:app_name]
  error_messages = options[:error_messages] || ['']

  error_messages = [error_messages] unless error_messages.is_a?(Array)

  if exception.respond_to?(:backtrace)
    backtrace = exception.backtrace
  else
    backtrace = caller
  end

  # extract the relevant request data and also filter out any params
  # that should NOT be logged/emailed (passwords etc.)
  request_data = request_data_from_request(request) unless request.nil?

  supplementary_info = nil

  unless config[:call_for_supplementary_info].nil?
    supplementary_info = config[:call_for_supplementary_info].call(request)
    supplementary_info = [supplementary_info] unless supplementary_info.is_a?(Array)
  end

  unless supplementary_info.blank?
    error_messages << "Supplementary info:"
    error_messages += supplementary_info
  end

  if exception.nil?
    exception_classname = nil
    status_code = nil
    log_error error_messages.inspect
    log_error backtrace
    log_error "Request params were:"
    log_error request_data.to_yaml
    error_string = error_messages.shift
  else
    status_code =
      Wrangler::ExceptionHandler.status_code_for_exception(exception)

    log_exception(exception, request_data, status_code, error_messages)

    if exception.is_a?(Class)
      exception_classname = exception.name
    else
      exception_classname = exception.class.name
    end

    if exception.respond_to?(:message)
      error_string = exception.message
    else
      error_string = exception.to_s
    end
  end

  if send_notification?(exception, request, status_code)
    if notify_with_delayed_job?
      # don't pass in request as it contains not-easily-serializable stuff
      log_error "Wrangler sending email notification asynchronously"
      Wrangler::ExceptionNotifier.send_later(:deliver_exception_notification,
                                            exception_classname,
                                            error_string,
                                            error_messages,
                                            proc_name,
                                            backtrace,
                                            supplementary_info,
                                            status_code,
                                            request_data)
    else
      log_error "Wrangler sending email notification synchronously"
      Wrangler::ExceptionNotifier.deliver_exception_notification(exception_classname,
                                       error_string,
                                       error_messages,
                                       proc_name,
                                       backtrace,
                                       supplementary_info,
                                       status_code,
                                       request_data,
                                       request)
    end
  end

  if render_errors
    render_error_template(exception, status_code)
  end

rescue Exception => unhandled_exception
  # if it looks like a temporary error interacting with SMTP, then enqueue
  # the error using delayed job if possible
  # (testing by name this way in case the exception isn't loaded into
  # environment, which would cause a NameError and be counterproductive...)
  if unhandled_exception.class.name == 'Net::SMTPAuthenticationError' &&
     Wrangler::ExceptionNotifier.respond_to?(:send_later)

    log_error "Wrangler failed to send error notification: #{unhandled_exception.class.name}:"
    log_error "  #{unhandled_exception.to_s}"

    # note: this is specific to an old-ish version of delayed job...should
    # make wrangler compatible with the old and the new...
    log_error "Wrangler attempting to send via delayed job"
      Wrangler::ExceptionNotifier.send_later(:deliver_exception_notification,
                                            exception_classname,
                                            error_string,
                                            error_messages,
                                            proc_name,
                                            backtrace,
                                            supplementary_info,
                                            status_code,
                                            request_data)
  else
    log_error "/!\\ FAILSAFE /!\\ Wrangler encountered an unhandled exception " +
              "while trying to handle an error. The arguments it received " +
              "were:"
    log_error "  exception: #{exception.inspect}"
    log_error "  options: #{options.inspect}"
    log_error "The unhandled error encountered was #{unhandled_exception.class.name}:"
    log_error "  #{unhandled_exception.to_s}"
  end
end

.included(base) ⇒ Object



12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/wrangler.rb', line 12

def self.included(base)
  # only add in the controller-specific methods if the including class is one
  if defined?(Rails) && class_has_ancestor?(base, ActionController::Base)
    base.send(:include, ControllerMethods)

    # conditionally including these methods (each wrapped in a separate
    # module) based on the configuration of whether to handle exceptions in
    # the given environment or not. this allows the default implementation
    # of the two rescue methods to run when Wrangler-based exception handling
    # is disabled.

    if Wrangler::ExceptionHandler.config[:handle_public_errors]
      Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_in_public"
      base.send(:include, PublicControllerMethods)
    else
      Rails.logger.info "NOT Configuring #{base.name} with Wrangler's rescue_action_in_public"
    end

    if Wrangler::ExceptionHandler.config[:handle_local_errors]
      Rails.logger.info "Configuring #{base.name} with Wrangler's rescue_action_locally"
      base.send(:include, LocalControllerMethods)
    else
      Rails.logger.info "NOT configuring #{base.name} with Wrangler's rescue_action_locally"
    end
  end
end

.log_error(msgs) ⇒ Object

handles logging error messages, using logger if available and puts otherwise




100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/wrangler/wrangler_helper.rb', line 100

def log_error(msgs)
  unless msgs.is_a?(Array)
    msgs = [msgs]
  end

  msgs.each do |m|
    if respond_to?(:logger) && !logger.blank?
      logger.error m
    else
      puts m
    end
  end
end

.log_exception(exception, request_data = nil, status_code = nil, error_messages = nil) ⇒ Object

log the exception using logger if available. if object does not have a logger, will just puts()




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
# File 'lib/wrangler/wrangler_helper.rb', line 66

def log_exception(exception, request_data = nil,
                  status_code = nil, error_messages = nil)
  msgs = []
  msgs << "An exception was caught (#{exception.class.name}):"

  if exception.respond_to?(:message)
    msgs << exception.message
  else
    msgs << exception.to_s
  end

  if error_messages.is_a?(Array)
    msgs.concat error_messages
  elsif !error_messages.nil? && !error_messages.empty?
    msgs << error_messages
  end

  unless request_data.blank?
    msgs <<  "Request params were:"
    msgs <<  request_data.inspect
  end
  unless status_code.blank?
    msgs <<  "Handling with status code: #{status_code}"
  end
  if exception.respond_to?(:backtrace) && !exception.backtrace.blank?
    msgs <<  exception.backtrace.join("\n  ")
  end

  log_error msgs
end

.notify_in_context?Boolean

determine if the current context (local?, background) indicates that a notification should be sent. this applies all of the rules around notifications EXCEPT for what the current exception or status code is (see notify_on_exception? for that)


Returns:

  • (Boolean)


443
444
445
446
447
448
449
450
451
452
453
454
455
456
# File 'lib/wrangler/exception_handler.rb', line 443

def notify_in_context?
  if self.respond_to?(:local_request?)
    if (local_request? && config[:notify_on_local_error]) ||
        (!local_request? && config[:notify_on_public_error])
      notify = true
    else
      notify = false
    end
  else
    notify = config[:notify_on_background_error]
  end

  return notify
end

.notify_on_error(proc_name = nil, message = nil, &block) ⇒ Object

execute the code block passed as an argument, and follow notification rules if an exception bubbles out of the block.

arguments

proc_name

a name for the chunk of code you’re running, included in logs and in the email notifications’ subject line. optional, default is nil (nothing will be displayed).

message

a message to include in any logs regarding exceptions thrown. useful to explain what the context of the code was to help diagnose. optional, default is nil (no message will be displayed other than the exception’s own message).

return value

  • if an exception bubbles out of the block, the exception is re-raised to calling code.

  • otherwise, returns nil




217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/wrangler/exception_handler.rb', line 217

def notify_on_error(proc_name = nil, message = nil, &block)
  begin
    yield
  rescue => exception
    options = {}
    options.merge! :proc_name => proc_name unless proc_name.nil?
    options.merge! :error_messages => message unless message.nil?
    handle_exception(exception, options)
  end

  return nil
end

.notify_on_exception?(exception, status_code = nil) ⇒ Boolean

determine if the app is configured to notify for the given exception or status code


Returns:

  • (Boolean)


462
463
464
465
466
467
468
469
470
471
472
# File 'lib/wrangler/exception_handler.rb', line 462

def notify_on_exception?(exception, status_code = nil)
  # first determine if we're configured to notify given the context of the
  # exception
  notify = notify_in_context?

  # now if config says notify in this case, check if we're configured to
  # notify for this exception or this status code
  return notify &&
    (config[:notify_exception_classes].include?(exception.class) ||
     config[:notify_status_codes].include?(status_code))
end

.notify_with_delayed_job?Boolean

determine if email should be sent with delayed job or not (delayed job must be installed and config set to use delayed job


Returns:

  • (Boolean)


477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# File 'lib/wrangler/exception_handler.rb', line 477

def notify_with_delayed_job?
  use_dj = false

  if self.is_a?(ActionController::Base)
    if config[:delayed_job_for_controller_errors] &&
        ExceptionNotifier.respond_to?(:send_later)
      use_dj = true
    else
      use_dj = false
    end
  else
    if config[:delayed_job_for_non_controller_errors] &&
        ExceptionNotifier.respond_to?(:send_later)
      use_dj = true
    else
      use_dj = false
    end
  end

  return use_dj
end

.send_notification?(exception = nil, request = nil, status_code = nil) ⇒ Boolean

determine if a notificaiton should be sent


Returns:

  • (Boolean)


400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/wrangler/exception_handler.rb', line 400

def send_notification?(exception = nil, request = nil, status_code = nil)
  send_notification = notify_in_context?

  if !send_notification
    log_error("Will not notify because notify_in_context? returned false")
  end

  if send_notification && !exception.nil?
    send_notification &&= notify_on_exception?(exception, status_code)
    if !send_notification
      log_error("Will not notify because notify_on_exception? returned false")
    end
  end

  if send_notification && !request.nil?
    config[:block_notify_on_request_headers].each do |headers_and_patterns|
      headers_and_patterns.each_pair do |header, regexp|
        # only send notification if the header does NOT match the regexp
        if request.env.include?(header)
          send_notification &&= (regexp !~ request.env[header])
        end
        if request.env['action_controller.request.query_parameters'].include?(header)
          send_notification &&= (regexp !~ request.env['action_controller.request.query_parameters'][header])
        end
        if request.env['action_controller.request.request_parameters'].include?(header)
          send_notification &&= (regexp !~ request.env['action_controller.request.request_parameters'][header])
        end
      end
    end
    if !send_notification
      log_error "Will not notify because :block_notify_on_request_headers was configured to block this request"
    end
  end

  return send_notification
end

Instance Method Details

#request_data_from_request(request) ⇒ Object

extract a hash of relevant (and serializable) parameters from a request NOTE: will obey filter_paramters on any class including the module, avoid logging any data in the request that the app wouldn’t log itself. filter_paramters must follow the rails convention of returning the association but with the value obscured in some way (e.g. “[FILTERED]”). see filter_paramter_logging .




247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/wrangler.rb', line 247

def request_data_from_request(request)
  return nil if request.nil?

  request_data = {}
  request.env.each_pair do |k,v|
    next if skip_request_env?(k)

    if self.respond_to?(:filter_parameters)
      request_data.merge! self.send(:filter_parameters, k => v)
    else
      request_data.merge! k => v
    end
  end

  request_params = {}
  if self.respond_to?(:filter_parameters)
    request_params.merge!(
                  filter_parameters(request.env['action_controller.request.query_parameters'])
                  )
    request_params.merge!(
                  filter_parameters(request.env['action_controller.request.request_parameters'])
                  )
  else
    request_params.merge! request.env['action_controller.request.query_parameters']
    request_params.merge! request.env['action_controller.request.request_parameters']
  end

  request_data.merge! :params => request_params unless request_params.blank?

  return request_data
end

#skip_request_env?(request_param) ⇒ Boolean

determine if the request env param should be ommitted from the request data object, as specified in config (either for aesthetic reasons or because the param won’t serialize well).


Returns:

  • (Boolean)


284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/wrangler.rb', line 284

def skip_request_env?(request_param)
  skip_env = false
  Wrangler::ExceptionHandler.config[:request_env_to_skip].each do |pattern|
    if (pattern.is_a?(String) && pattern == request_param) ||
       (pattern.is_a?(Regexp) && pattern =~ request_param)
      skip_env = true
      break
    end
  end

  return skip_env
end