otto 2.0.0.pre8 → 2.0.0.pre10

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/claude.yml +1 -1
  5. data/.github/workflows/code-smells.yml +2 -2
  6. data/CHANGELOG.rst +73 -35
  7. data/CLAUDE.md +44 -0
  8. data/Gemfile.lock +7 -7
  9. data/README.md +20 -0
  10. data/docs/.gitignore +2 -0
  11. data/docs/modern-authentication-authorization-landscape.md +558 -0
  12. data/docs/multi-strategy-authentication-design.md +1401 -0
  13. data/lib/otto/core/error_handler.rb +104 -10
  14. data/lib/otto/core/freezable.rb +0 -2
  15. data/lib/otto/core/helper_registry.rb +135 -0
  16. data/lib/otto/core/lifecycle_hooks.rb +63 -0
  17. data/lib/otto/core/middleware_management.rb +70 -0
  18. data/lib/otto/core/middleware_stack.rb +12 -8
  19. data/lib/otto/core/router.rb +25 -31
  20. data/lib/otto/core.rb +3 -0
  21. data/lib/otto/errors.rb +92 -0
  22. data/lib/otto/helpers.rb +2 -2
  23. data/lib/otto/locale/middleware.rb +1 -1
  24. data/lib/otto/mcp/core.rb +33 -0
  25. data/lib/otto/mcp/protocol.rb +1 -1
  26. data/lib/otto/mcp/rate_limiting.rb +6 -2
  27. data/lib/otto/mcp/schema_validation.rb +2 -2
  28. data/lib/otto/mcp.rb +1 -0
  29. data/lib/otto/privacy/core.rb +82 -0
  30. data/lib/otto/privacy.rb +1 -0
  31. data/lib/otto/{helpers/request.rb → request.rb} +17 -6
  32. data/lib/otto/{helpers/response.rb → response.rb} +20 -7
  33. data/lib/otto/response_handlers/json.rb +1 -3
  34. data/lib/otto/response_handlers/view.rb +1 -1
  35. data/lib/otto/route.rb +2 -4
  36. data/lib/otto/route_handlers/base.rb +88 -5
  37. data/lib/otto/route_handlers/class_method.rb +9 -67
  38. data/lib/otto/route_handlers/instance_method.rb +10 -57
  39. data/lib/otto/route_handlers/lambda.rb +2 -2
  40. data/lib/otto/route_handlers/logic_class.rb +85 -90
  41. data/lib/otto/security/authentication/auth_strategy.rb +2 -2
  42. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +1 -1
  43. data/lib/otto/security/authentication/strategy_result.rb +9 -9
  44. data/lib/otto/security/authorization_error.rb +1 -1
  45. data/lib/otto/security/config.rb +3 -3
  46. data/lib/otto/security/core.rb +167 -0
  47. data/lib/otto/security/middleware/csrf_middleware.rb +1 -1
  48. data/lib/otto/security/middleware/validation_middleware.rb +1 -1
  49. data/lib/otto/security/rate_limiter.rb +7 -3
  50. data/lib/otto/security.rb +1 -0
  51. data/lib/otto/version.rb +1 -1
  52. data/lib/otto.rb +29 -404
  53. metadata +12 -3
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Base error classes for Otto framework
4
+ #
5
+ # These classes provide a foundation for HTTP error handling and can be
6
+ # subclassed by implementing projects for consistent error handling.
7
+ #
8
+ # @example Subclassing in an application
9
+ # class MyApp::ResourceNotFound < Otto::NotFoundError; end
10
+ #
11
+ # otto.register_error_handler(MyApp::ResourceNotFound, status: 404, log_level: :info)
12
+ #
13
+ class Otto
14
+ # Base class for all Otto HTTP errors
15
+ #
16
+ # Provides default_status and default_log_level class methods that
17
+ # define the HTTP status code and logging level for the error.
18
+ class HTTPError < StandardError
19
+ def self.default_status
20
+ 500
21
+ end
22
+
23
+ def self.default_log_level
24
+ :error
25
+ end
26
+ end
27
+
28
+ # Bad Request (400) error
29
+ #
30
+ # Use for malformed requests, invalid parameters, or failed validation
31
+ class BadRequestError < HTTPError
32
+ def self.default_status
33
+ 400
34
+ end
35
+
36
+ def self.default_log_level
37
+ :info
38
+ end
39
+ end
40
+
41
+ # Unauthorized (401) error
42
+ #
43
+ # Use when authentication is required but missing or invalid
44
+ class UnauthorizedError < HTTPError
45
+ def self.default_status
46
+ 401
47
+ end
48
+
49
+ def self.default_log_level
50
+ :info
51
+ end
52
+ end
53
+
54
+ # Forbidden (403) error
55
+ #
56
+ # Use when the user is authenticated but lacks permission
57
+ class ForbiddenError < HTTPError
58
+ def self.default_status
59
+ 403
60
+ end
61
+
62
+ def self.default_log_level
63
+ :warn
64
+ end
65
+ end
66
+
67
+ # Not Found (404) error
68
+ #
69
+ # Use when the requested resource does not exist
70
+ class NotFoundError < HTTPError
71
+ def self.default_status
72
+ 404
73
+ end
74
+
75
+ def self.default_log_level
76
+ :info
77
+ end
78
+ end
79
+
80
+ # Payload Too Large (413) error
81
+ #
82
+ # Use when request body exceeds configured size limits
83
+ class PayloadTooLargeError < HTTPError
84
+ def self.default_status
85
+ 413
86
+ end
87
+
88
+ def self.default_log_level
89
+ :warn
90
+ end
91
+ end
92
+ end
data/lib/otto/helpers.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative 'helpers/request'
6
- require_relative 'helpers/response'
5
+ # Request and response helpers are now defined directly in Otto::Request and Otto::Response
6
+ # This file is kept for backward compatibility with require statements
@@ -68,7 +68,7 @@ class Otto
68
68
  # @return [String] Resolved locale code
69
69
  def detect_locale(env)
70
70
  # 1. Check URL parameter
71
- req = Rack::Request.new(env)
71
+ req = Otto::Request.new(env)
72
72
  locale = req.params['locale']
73
73
  return locale if valid_locale?(locale)
74
74
 
@@ -0,0 +1,33 @@
1
+ # lib/otto/mcp/core.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module MCP
7
+ # Core MCP (Model Context Protocol) methods included in the Otto class.
8
+ # Provides the public API for enabling and querying MCP server support.
9
+ module Core
10
+ # Enable MCP (Model Context Protocol) server support
11
+ #
12
+ # @param options [Hash] MCP configuration options
13
+ # @option options [Boolean] :http Enable HTTP endpoint (default: true)
14
+ # @option options [Boolean] :stdio Enable STDIO communication (default: false)
15
+ # @option options [String] :endpoint HTTP endpoint path (default: '/_mcp')
16
+ # @example
17
+ # otto.enable_mcp!(http: true, endpoint: '/api/mcp')
18
+ def enable_mcp!(options = {})
19
+ ensure_not_frozen!
20
+ @mcp_server ||= Otto::MCP::Server.new(self)
21
+
22
+ @mcp_server.enable!(options)
23
+ Otto.logger.info '[MCP] Enabled MCP server' if Otto.debug
24
+ end
25
+
26
+ # Check if MCP is enabled
27
+ # @return [Boolean]
28
+ def mcp_enabled?
29
+ @mcp_server&.enabled?
30
+ end
31
+ end
32
+ end
33
+ end
@@ -17,7 +17,7 @@ class Otto
17
17
  end
18
18
 
19
19
  def handle_request(env)
20
- request = Rack::Request.new(env)
20
+ request = @otto.request_class.new(env)
21
21
 
22
22
  unless request.post? && request.content_type&.include?('application/json')
23
23
  return error_response(nil, -32_600, 'Invalid Request', 'Only JSON-RPC POST requests supported')
@@ -87,8 +87,12 @@ class Otto
87
87
  [429, headers, [JSON.generate(error_response)]]
88
88
  else
89
89
  # Use the general rate limiting response for non-MCP requests
90
- accept_header = request.env['HTTP_ACCEPT'].to_s
91
- if accept_header.include?('application/json')
90
+ # Route's response_type takes precedence over Accept header
91
+ route_def = request.env['otto.route_definition']
92
+ wants_json = (route_def&.response_type == 'json') ||
93
+ request.env['HTTP_ACCEPT'].to_s.include?('application/json')
94
+
95
+ if wants_json
92
96
  error_response = {
93
97
  error: 'Rate limit exceeded',
94
98
  message: 'Too many requests',
@@ -12,7 +12,7 @@ end
12
12
 
13
13
  class Otto
14
14
  module MCP
15
- class ValidationError < StandardError; end
15
+ class ValidationError < Otto::BadRequestError; end
16
16
 
17
17
  # JSON Schema validator for MCP protocol requests
18
18
  class Validator
@@ -78,7 +78,7 @@ class Otto
78
78
  # Only validate MCP endpoints
79
79
  return @app.call(env) unless mcp_endpoint?(env)
80
80
 
81
- request = Rack::Request.new(env)
81
+ request = Otto::Request.new(env)
82
82
 
83
83
  if request.post? && request.content_type&.include?('application/json')
84
84
  begin
data/lib/otto/mcp.rb CHANGED
@@ -2,4 +2,5 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'mcp/core'
5
6
  require_relative 'mcp/server'
@@ -0,0 +1,82 @@
1
+ # lib/otto/privacy/core.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Privacy
7
+ # Core privacy configuration methods included in the Otto class.
8
+ # Provides the public API for configuring IP privacy features.
9
+ module Core
10
+ # Disable IP privacy to access original IP addresses
11
+ #
12
+ # IMPORTANT: By default, Otto masks public IP addresses for privacy.
13
+ # Private/localhost IPs (127.0.0.0/8, 10.0.0.0/8, etc.) are never masked.
14
+ # Only disable this if you need access to original public IPs.
15
+ #
16
+ # When disabled:
17
+ # - env['REMOTE_ADDR'] contains the real IP address
18
+ # - env['otto.original_ip'] also contains the real IP
19
+ # - No PrivateFingerprint is created
20
+ #
21
+ # @example
22
+ # otto.disable_ip_privacy!
23
+ def disable_ip_privacy!
24
+ ensure_not_frozen!
25
+ @security_config.ip_privacy_config.disable!
26
+ end
27
+
28
+ # Enable full IP privacy (mask ALL IPs including private/localhost)
29
+ #
30
+ # By default, Otto exempts private and localhost IPs from masking for
31
+ # better development experience. Call this method to mask ALL IPs
32
+ # regardless of type.
33
+ #
34
+ # @example Enable full privacy (mask all IPs)
35
+ # otto = Otto.new(routes_file)
36
+ # otto.enable_full_ip_privacy!
37
+ # # Now 127.0.0.1 → 127.0.0.0, 192.168.1.100 → 192.168.1.0
38
+ #
39
+ # @return [void]
40
+ # @raise [FrozenError] if called after configuration is frozen
41
+ def enable_full_ip_privacy!
42
+ ensure_not_frozen!
43
+ @security_config.ip_privacy_config.mask_private_ips = true
44
+ end
45
+
46
+ # Configure IP privacy settings
47
+ #
48
+ # Privacy is enabled by default. Use this method to customize privacy
49
+ # behavior without disabling it entirely.
50
+ #
51
+ # @param octet_precision [Integer] Number of octets to mask (1 or 2, default: 1)
52
+ # @param hash_rotation [Integer] Seconds between key rotation (default: 86400)
53
+ # @param geo [Boolean] Enable geo-location resolution (default: true)
54
+ # @param redis [Redis] Redis connection for multi-server atomic key generation
55
+ #
56
+ # @example Mask 2 octets instead of 1
57
+ # otto.configure_ip_privacy(octet_precision: 2)
58
+ #
59
+ # @example Disable geo-location
60
+ # otto.configure_ip_privacy(geo: false)
61
+ #
62
+ # @example Custom hash rotation
63
+ # otto.configure_ip_privacy(hash_rotation: 24.hours)
64
+ #
65
+ # @example Multi-server with Redis
66
+ # redis = Redis.new(url: ENV['REDIS_URL'])
67
+ # otto.configure_ip_privacy(redis: redis)
68
+ def configure_ip_privacy(octet_precision: nil, hash_rotation: nil, geo: nil, redis: nil)
69
+ ensure_not_frozen!
70
+ config = @security_config.ip_privacy_config
71
+
72
+ config.octet_precision = octet_precision if octet_precision
73
+ config.hash_rotation_period = hash_rotation if hash_rotation
74
+ config.geo_enabled = geo unless geo.nil?
75
+ config.instance_variable_set(:@redis, redis) if redis
76
+
77
+ # Validate configuration
78
+ config.validate!
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/otto/privacy.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
+ require_relative 'privacy/core'
5
6
  require_relative 'privacy/config'
6
7
  require_relative 'privacy/ip_privacy'
7
8
  require_relative 'privacy/geo_resolver'
@@ -1,14 +1,25 @@
1
- # lib/otto/helpers/request.rb
1
+ # lib/otto/request.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative 'base'
5
+ require 'rack/request'
6
6
 
7
7
  class Otto
8
- # Request helper methods providing HTTP request handling utilities
9
- module RequestHelpers
10
- include Otto::BaseHelpers
11
-
8
+ # Otto's enhanced Rack::Request class with built-in helpers
9
+ #
10
+ # This class extends Rack::Request with Otto's framework helpers for
11
+ # HTTP request handling, privacy, security, and locale management.
12
+ # Projects can register additional helpers via Otto#register_request_helpers.
13
+ #
14
+ # @example Using Otto's request in route handlers
15
+ # def show(req, res)
16
+ # req.masked_ip # Privacy-safe masked IP
17
+ # req.geo_country # ISO country code
18
+ # req.check_locale! # Set locale for request
19
+ # end
20
+ #
21
+ # @see Otto#register_request_helpers
22
+ class Request < Rack::Request
12
23
  def user_agent
13
24
  env['HTTP_USER_AGENT']
14
25
  end
@@ -1,14 +1,27 @@
1
- # lib/otto/helpers/response.rb
1
+ # lib/otto/response.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require_relative 'base'
5
+ require 'rack/response'
6
6
 
7
7
  class Otto
8
- # Response helper methods providing HTTP response handling utilities
9
- module ResponseHelpers
10
- include Otto::BaseHelpers
11
-
8
+ # Otto's enhanced Rack::Response class with built-in helpers
9
+ #
10
+ # This class extends Rack::Response with Otto's framework helpers for
11
+ # HTTP response handling, cookie management, CSP headers, and security.
12
+ # Projects can register additional helpers via Otto#register_response_helpers.
13
+ #
14
+ # @example Using Otto's response in route handlers
15
+ # def show(req, res)
16
+ # res.send_secure_cookie('session_id', token, 3600)
17
+ # res.send_csp_headers('text/html', nonce)
18
+ # res.no_cache!
19
+ # end
20
+ #
21
+ # @see Otto#register_response_helpers
22
+ class Response < Rack::Response
23
+ # Reference to the request object (needed by some response helpers)
24
+ # @return [Otto::Request]
12
25
  attr_accessor :request
13
26
 
14
27
  def send_secure_cookie(name, value, ttl, opts = {})
@@ -171,7 +184,7 @@ class Otto
171
184
  # # => "/myapp/admin/settings"
172
185
  def app_path(*paths)
173
186
  paths = paths.flatten.compact
174
- paths.unshift(request.env['SCRIPT_NAME']) if request.env['SCRIPT_NAME']
187
+ paths.unshift(request.env['SCRIPT_NAME']) if request&.env&.[]('SCRIPT_NAME')
175
188
  paths.join('/').gsub('//', '/')
176
189
  end
177
190
  end
@@ -11,9 +11,7 @@ class Otto
11
11
  def self.handle(result, response, context = {})
12
12
  # If a redirect has already been set, don't override with JSON
13
13
  # This allows controllers to conditionally redirect based on Accept header
14
- if response.status&.between?(300, 399) && response['Location']
15
- return
16
- end
14
+ return if response.status&.between?(300, 399) && response['Location']
17
15
 
18
16
  response['Content-Type'] = 'application/json'
19
17
 
@@ -9,7 +9,7 @@ class Otto
9
9
  # Handler for view/template responses
10
10
  class ViewHandler < BaseHandler
11
11
  def self.handle(result, response, context = {})
12
- if context[:logic_instance]&.respond_to?(:view)
12
+ if context[:logic_instance].respond_to?(:view)
13
13
  response.body = [context[:logic_instance].view.render]
14
14
  response['Content-Type'] = 'text/html' unless response['Content-Type']
15
15
  elsif result.respond_to?(:to_s)
data/lib/otto/route.rb CHANGED
@@ -100,10 +100,8 @@ class Otto
100
100
  # @return [Array] Rack response array [status, headers, body]
101
101
  def call(env, extra_params = {})
102
102
  extra_params ||= {}
103
- req = Rack::Request.new(env)
104
- res = Rack::Response.new
105
- req.extend Otto::RequestHelpers
106
- res.extend Otto::ResponseHelpers
103
+ req = otto.request_class.new(env)
104
+ res = otto.response_class.new
107
105
  res.request = req
108
106
 
109
107
  # Make security config available to response helpers
@@ -3,6 +3,7 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  require 'json'
6
+ require 'securerandom'
6
7
 
7
8
  class Otto
8
9
  module RouteHandlers
@@ -21,7 +22,20 @@ class Otto
21
22
  # @param extra_params [Hash] Additional parameters
22
23
  # @return [Array] Rack response array
23
24
  def call(env, extra_params = {})
24
- raise NotImplementedError, 'Subclasses must implement #call'
25
+ @start_time = Otto::Utils.now_in_μs
26
+ req = otto_instance ? otto_instance.request_class.new(env) : Otto::Request.new(env)
27
+ res = otto_instance ? otto_instance.response_class.new : Otto::Response.new
28
+
29
+ begin
30
+ setup_request_response(req, res, env, extra_params)
31
+ result, context = invoke_target(req, res)
32
+
33
+ handle_response(result, res, context) if route_definition.response_type != 'default'
34
+ rescue StandardError => e
35
+ handle_execution_error(e, env, req, res, @start_time)
36
+ end
37
+
38
+ finalize_response(res)
25
39
  end
26
40
 
27
41
  protected
@@ -32,15 +46,84 @@ class Otto
32
46
  @target_class ||= safe_const_get(route_definition.klass_name)
33
47
  end
34
48
 
35
- # Setup request and response with the same extensions and processing as Route#call
49
+ # Template method for subclasses to implement their invocation logic
50
+ # @param req [Rack::Request] Request object
51
+ # @param res [Rack::Response] Response object
52
+ # @return [Array] [result, context] where context is a hash for handle_response
53
+ def invoke_target(req, res)
54
+ raise NotImplementedError, 'Subclasses must implement #invoke_target'
55
+ end
56
+
57
+ # Handle errors during route execution
58
+ # @param error [StandardError] The error that occurred
59
+ # @param env [Hash] Rack environment
36
60
  # @param req [Rack::Request] Request object
37
61
  # @param res [Rack::Response] Response object
62
+ # @param start_time [Integer] Start time in microseconds
63
+ def handle_execution_error(error, env, _req, res, start_time)
64
+ if otto_instance
65
+ # Integrated context - let centralized error handler manage
66
+ env['otto.handler'] = handler_name
67
+ env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
68
+ raise error
69
+ else
70
+ # Direct testing context - handle locally
71
+ handle_local_error(error, env, res)
72
+ end
73
+ end
74
+
75
+ # Handle errors locally for testing context
76
+ # @param error [StandardError] The error that occurred
77
+ # @param env [Hash] Rack environment
78
+ # @param res [Rack::Response] Response object
79
+ def handle_local_error(error, env, res)
80
+ error_id = SecureRandom.hex(8)
81
+ Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
82
+ Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
83
+
84
+ res.status = 500
85
+
86
+ # Content negotiation for error response
87
+ # Route's response_type takes precedence over Accept header
88
+ route_def = env['otto.route_definition']
89
+ wants_json = (route_def&.response_type == 'json') ||
90
+ env['HTTP_ACCEPT'].to_s.include?('application/json')
91
+
92
+ if wants_json
93
+ res.headers['content-type'] = 'application/json'
94
+ error_data = {
95
+ error: 'Internal Server Error',
96
+ message: 'Server error occurred. Check logs for details.',
97
+ error_id: error_id,
98
+ }
99
+ res.write JSON.generate(error_data)
100
+ else
101
+ res.headers['content-type'] = 'text/plain'
102
+ if Otto.env?(:dev, :development)
103
+ res.write "Server error (ID: #{error_id}). Check logs for details."
104
+ else
105
+ res.write 'An error occurred. Please try again later.'
106
+ end
107
+ end
108
+
109
+ # Security headers are not available without an otto_instance
110
+ # (testing/local context). The RouteAuthWrapper handles security
111
+ # headers when otto_instance is present.
112
+ end
113
+
114
+ # Format the handler name for logging
115
+ # @return [String] Handler name in format "ClassName#method_name"
116
+ def handler_name
117
+ "#{target_class.name}##{route_definition.method_name}"
118
+ end
119
+
120
+ # Setup request and response with the same extensions and processing as Route#call
121
+ # @param req [Otto::Request] Request object
122
+ # @param res [Otto::Response] Response object
38
123
  # @param env [Hash] Rack environment
39
124
  # @param extra_params [Hash] Additional parameters
40
125
  def setup_request_response(req, res, env, extra_params)
41
- # Apply the same extensions as original Route#call
42
- req.extend Otto::RequestHelpers
43
- res.extend Otto::ResponseHelpers
126
+ # Set request reference (helpers are already included in class)
44
127
  res.request = req
45
128
 
46
129
  # Make security config available to response helpers
@@ -2,9 +2,6 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require 'json'
6
- require 'securerandom'
7
-
8
5
  require_relative 'base'
9
6
 
10
7
  class Otto
@@ -19,70 +16,15 @@ class Otto
19
16
  # Use this handler for endpoints requiring request-level control (logout,
20
17
  # session management, cookie manipulation, custom header handling).
21
18
  class ClassMethodHandler < BaseHandler
22
- def call(env, extra_params = {})
23
- start_time = Otto::Utils.now_in_μs
24
- req = Rack::Request.new(env)
25
- res = Rack::Response.new
26
-
27
- begin
28
- # Apply the same extensions and processing as original Route#call
29
- setup_request_response(req, res, env, extra_params)
30
-
31
- # Call class method directly (existing Otto behavior)
32
- result = target_class.send(route_definition.method_name, req, res)
33
-
34
- # Only handle response if response_type is not default
35
- if route_definition.response_type != 'default'
36
- handle_response(result, res,
37
- {
38
- class: target_class,
39
- request: req,
40
- })
41
- end
42
- rescue StandardError => e
43
- # Check if we're being called through Otto's integrated context (vs direct handler testing)
44
- # In integrated context, let Otto's centralized error handler manage the response
45
- # In direct testing context, handle errors locally for unit testing
46
- if otto_instance
47
- # Store handler context in env for centralized error handler
48
- handler_name = "#{target_class.name}##{route_definition.method_name}"
49
- env['otto.handler'] = handler_name
50
- env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
51
-
52
- raise e # Re-raise to let Otto's centralized error handler manage the response
53
- else
54
- # Direct handler testing context - handle errors locally with security improvements
55
- error_id = SecureRandom.hex(8)
56
- Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
57
- Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
58
-
59
- res.status = 500
60
-
61
- # Content negotiation for error response
62
- accept_header = env['HTTP_ACCEPT'].to_s
63
- if accept_header.include?('application/json')
64
- res.headers['content-type'] = 'application/json'
65
- error_data = {
66
- error: 'Internal Server Error',
67
- message: 'Server error occurred. Check logs for details.',
68
- error_id: error_id,
69
- }
70
- res.write JSON.generate(error_data)
71
- else
72
- res.headers['content-type'] = 'text/plain'
73
- res.write "Server error (ID: #{error_id}). Check logs for details."
74
- end
75
-
76
- # Add security headers if available
77
- if otto_instance.respond_to?(:security_config) && otto_instance.security_config
78
- otto_instance.security_config.security_headers.each do |header, value|
79
- res.headers[header] = value
80
- end
81
- end
82
- end
83
- end
84
-
85
- finalize_response(res)
19
+ protected
20
+
21
+ # Invoke the class method on the target class
22
+ # @param req [Rack::Request] Request object
23
+ # @param res [Rack::Response] Response object
24
+ # @return [Array] [result, context] for handle_response
25
+ def invoke_target(req, res)
26
+ result = target_class.send(route_definition.method_name, req, res)
27
+ [result, { class: target_class, request: req }]
86
28
  end
87
29
  end
88
30
  end