Module: Otto::Core::ErrorHandler

Included in:
Otto
Defined in:
lib/otto/core/error_handler.rb

Overview

Error handling module providing secure error reporting and logging functionality

Instance Method Summary collapse

Instance Method Details

#handle_error(error, env) ⇒ Object



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
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
# File 'lib/otto/core/error_handler.rb', line 13

def handle_error(error, env)
  # Check if this is a registered expected error
  if handler_config = @error_handlers[error.class.name]
    return handle_expected_error(error, env, handler_config)
  end

  # Log error details internally but don't expose them
  error_id = SecureRandom.hex(8)

  # Base context pattern: create once, reuse for correlation
  base_context = Otto::LoggingHelpers.request_context(env)

  # Include handler context if available (set by route handlers)
  log_context = base_context.merge(
    error: error.message,
    error_class: error.class.name,
    error_id: error_id,
  )
  log_context[:handler] = env['otto.handler'] if env['otto.handler']
  log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']

  Otto.structured_log(:error, 'Unhandled error in request', log_context)

  Otto::LoggingHelpers.log_backtrace(error,
    base_context.merge(error_id: error_id))

  # Parse request for content negotiation
  begin
    Otto::Request.new(env)
  rescue StandardError
    nil
  end
  literal_routes = @routes_literal[:GET] || {}

  # Try custom 500 route first
  if found_route = literal_routes['/500']
    begin
      env['otto.error_id'] = error_id
      return found_route.call(env)
    rescue StandardError => e
      # When the custom error handler itself fails, generate a new error ID
      # to distinguish it from the original error, but link them.
      custom_handler_error_id = SecureRandom.hex(8)
      base_context = Otto::LoggingHelpers.request_context(env)

      Otto.structured_log(:error, 'Error in custom error handler',
        base_context.merge(
          error: e.message,
          error_class: e.class.name,
          error_id: custom_handler_error_id,
          original_error_id: error_id # Link to original error
        ))

      Otto::LoggingHelpers.log_backtrace(e,
        base_context.merge(error_id: custom_handler_error_id, original_error_id: error_id))
    end
  end

  # Content negotiation for built-in error response
  return json_error_response(error_id) if wants_json_response?(env)

  # Fallback to built-in error response
  @server_error || secure_error_response(error_id)
end

#register_error_handler(error_class, status: 500, log_level: :info, &handler) ⇒ Object

Register an error handler for expected business logic errors

This allows you to handle known error conditions (like missing resources, expired data, rate limits) without logging them as unhandled 500 errors.

Examples:

Basic usage with status code

otto.register_error_handler(Onetime::MissingSecret, status: 404, log_level: :info)
otto.register_error_handler(Onetime::SecretExpired, status: 410, log_level: :info)

With custom response handler

otto.register_error_handler(Onetime::RateLimited, status: 429, log_level: :warn) do |error, req|
  {
    error: 'Rate limit exceeded',
    retry_after: error.retry_after,
    message: error.message
  }
end

Using string class names (for lazy loading)

otto.register_error_handler('Onetime::MissingSecret', status: 404, log_level: :info)

Parameters:

  • error_class (Class, String)

    The exception class or class name to handle

  • status (Integer) (defaults to: 500)

    HTTP status code to return (default: 500)

  • log_level (Symbol) (defaults to: :info)

    Log level for expected errors (:info, :warn, :error)

  • handler (Proc)

    Optional block to customize error response



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/otto/core/error_handler.rb', line 104

def register_error_handler(error_class, status: 500, log_level: :info, &handler)
  ensure_not_frozen!

  # Normalize error class to string for consistent lookup
  error_class_name = error_class.is_a?(String) ? error_class : error_class.name

  @error_handlers[error_class_name] = {
    status: status,
    log_level: log_level,
    handler: handler
  }
end