otto 2.0.0.pre8 → 2.0.0.pre9

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.
@@ -26,7 +26,7 @@ class Otto
26
26
  log_context = base_context.merge(
27
27
  error: error.message,
28
28
  error_class: error.class.name,
29
- error_id: error_id
29
+ error_id: error_id,
30
30
  )
31
31
  log_context[:handler] = env['otto.handler'] if env['otto.handler']
32
32
  log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
@@ -69,8 +69,7 @@ class Otto
69
69
  end
70
70
 
71
71
  # Content negotiation for built-in error response
72
- accept_header = env['HTTP_ACCEPT'].to_s
73
- return json_error_response(error_id) if accept_header.include?('application/json')
72
+ return json_error_response(error_id) if wants_json_response?(env)
74
73
 
75
74
  # Fallback to built-in error response
76
75
  @server_error || secure_error_response(error_id)
@@ -96,7 +95,7 @@ class Otto
96
95
  error: error.message,
97
96
  error_class: error.class.name,
98
97
  error_id: error_id,
99
- expected: true # Mark as expected error
98
+ expected: true # Mark as expected error
100
99
  )
101
100
  log_context[:handler] = env['otto.handler'] if env['otto.handler']
102
101
  log_context[:duration] = env['otto.handler_duration'] if env['otto.handler_duration']
@@ -146,14 +145,13 @@ class Otto
146
145
  response_body[:error_id] = error_id if Otto.env?(:dev, :development)
147
146
 
148
147
  # Content negotiation
149
- accept_header = env['HTTP_ACCEPT'].to_s
150
148
  status = handler_config[:status] || 500
151
149
 
152
- if accept_header.include?('application/json')
150
+ if wants_json_response?(env)
153
151
  body = JSON.generate(response_body)
154
152
  headers = {
155
153
  'content-type' => 'application/json',
156
- 'content-length' => body.bytesize.to_s
154
+ 'content-length' => body.bytesize.to_s,
157
155
  }.merge(@security_config.security_headers)
158
156
 
159
157
  [status, headers, [body]]
@@ -167,7 +165,7 @@ class Otto
167
165
 
168
166
  headers = {
169
167
  'content-type' => 'text/plain',
170
- 'content-length' => body.bytesize.to_s
168
+ 'content-length' => body.bytesize.to_s,
171
169
  }.merge(@security_config.security_headers)
172
170
 
173
171
  [status, headers, [body]]
@@ -211,6 +209,19 @@ class Otto
211
209
 
212
210
  [500, headers, [body]]
213
211
  end
212
+
213
+ private
214
+
215
+ # Determine if the client wants a JSON response
216
+ # Route's response_type declaration takes precedence over Accept header
217
+ #
218
+ # @param env [Hash] Rack environment
219
+ # @return [Boolean] true if JSON response is preferred
220
+ def wants_json_response?(env)
221
+ route_definition = env['otto.route_definition']
222
+ (route_definition&.response_type == 'json') ||
223
+ env['HTTP_ACCEPT'].to_s.include?('application/json')
224
+ end
214
225
  end
215
226
  end
216
227
  end
@@ -2,8 +2,6 @@
2
2
  #
3
3
  # frozen_string_literal: true
4
4
 
5
- require 'set'
6
-
7
5
  class Otto
8
6
  module Core
9
7
  # Provides deep freezing capability for configuration objects
@@ -73,7 +73,7 @@ class Otto
73
73
  when :last
74
74
  @stack << entry
75
75
  else
76
- @stack << entry # Default append
76
+ @stack << entry # Default append
77
77
  end
78
78
 
79
79
  @middleware_set.add(middleware_class)
@@ -114,15 +114,21 @@ class Otto
114
114
 
115
115
  # Check optimal order: rate_limit < auth < validation
116
116
  if rate_limit_pos && auth_pos && rate_limit_pos > auth_pos
117
- warnings << '[MCP Middleware] RateLimitMiddleware should come before TokenMiddleware for optimal performance'
117
+ warnings << <<~MSG.chomp
118
+ [MCP Middleware] RateLimitMiddleware should come before TokenMiddleware
119
+ MSG
118
120
  end
119
121
 
120
122
  if auth_pos && validation_pos && auth_pos > validation_pos
121
- warnings << '[MCP Middleware] TokenMiddleware should come before SchemaValidationMiddleware for optimal performance'
123
+ warnings << <<~MSG.chomp
124
+ [MCP Middleware] TokenMiddleware should come before SchemaValidationMiddleware
125
+ MSG
122
126
  end
123
127
 
124
128
  if rate_limit_pos && validation_pos && rate_limit_pos > validation_pos
125
- warnings << '[MCP Middleware] RateLimitMiddleware should come before SchemaValidationMiddleware for optimal performance'
129
+ warnings << <<~MSG.chomp
130
+ [MCP Middleware] RateLimitMiddleware should come before SchemaValidationMiddleware
131
+ MSG
126
132
  end
127
133
 
128
134
  warnings
@@ -198,8 +204,8 @@ class Otto
198
204
  @stack.map do |entry|
199
205
  {
200
206
  middleware: entry[:middleware],
201
- args: entry[:args],
202
- options: entry[:options],
207
+ args: entry[:args],
208
+ options: entry[:options],
203
209
  }
204
210
  end
205
211
  end
@@ -225,8 +231,6 @@ class Otto
225
231
  @stack.reverse_each(&)
226
232
  end
227
233
 
228
-
229
-
230
234
  private
231
235
 
232
236
  def middleware_needs_config?(middleware_class)
@@ -36,28 +36,28 @@ class Otto
36
36
  route.otto = self
37
37
  path_clean = path.gsub(%r{/$}, '')
38
38
  @route_definitions[route.definition] = route
39
- Otto.structured_log(:debug, "Route loaded",
40
- {
41
- pattern: route.pattern.source,
42
- verb: route.verb,
43
- definition: route.definition,
44
- type: 'pattern'
45
- }
46
- ) if Otto.debug
39
+ if Otto.debug
40
+ Otto.structured_log(:debug, 'Route loaded',
41
+ {
42
+ pattern: route.pattern.source,
43
+ verb: route.verb,
44
+ definition: route.definition,
45
+ type: 'pattern',
46
+ })
47
+ end
47
48
  @routes[route.verb] ||= []
48
49
  @routes[route.verb] << route
49
50
  @routes_literal[route.verb] ||= {}
50
51
  @routes_literal[route.verb][path_clean] = route
51
52
  rescue StandardError => e
52
- Otto.structured_log(:error, "Route load failed",
53
+ Otto.structured_log(:error, 'Route load failed',
53
54
  {
54
- path: path,
55
+ path: path,
55
56
  verb: verb,
56
57
  definition: definition,
57
58
  error: e.message,
58
- error_class: e.class.name
59
- }
60
- )
59
+ error_class: e.class.name,
60
+ })
61
61
  Otto.logger.debug e.backtrace.join("\n") if Otto.debug
62
62
  end
63
63
  self
@@ -96,30 +96,27 @@ class Otto
96
96
  literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
97
97
 
98
98
  if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
99
- Otto.structured_log(:debug, "Route matched",
99
+ Otto.structured_log(:debug, 'Route matched',
100
100
  Otto::LoggingHelpers.request_context(env).merge(
101
101
  type: 'static_cached',
102
102
  base_path: base_path
103
- )
104
- )
103
+ ))
105
104
  static_route.call(env)
106
105
  elsif literal_routes.has_key?(path_info_clean)
107
106
  route = literal_routes[path_info_clean]
108
- Otto.structured_log(:debug, "Route matched",
107
+ Otto.structured_log(:debug, 'Route matched',
109
108
  Otto::LoggingHelpers.request_context(env).merge(
110
109
  type: 'literal',
111
110
  handler: route.route_definition.definition,
112
111
  auth_strategy: route.route_definition.auth_requirement || 'none'
113
- )
114
- )
112
+ ))
115
113
  route.call(env)
116
114
  elsif static_route && http_verb == :GET && safe_file?(path_info)
117
- Otto.structured_log(:debug, "Route matched",
115
+ Otto.structured_log(:debug, 'Route matched',
118
116
  Otto::LoggingHelpers.request_context(env).merge(
119
117
  type: 'static_new',
120
118
  base_path: base_path
121
- )
122
- )
119
+ ))
123
120
  routes_static[:GET][base_path] = base_path
124
121
  static_route.call(env)
125
122
  else
@@ -165,14 +162,13 @@ class Otto
165
162
  found_route = route
166
163
 
167
164
  # Log successful route match
168
- Otto.structured_log(:debug, "Route matched",
165
+ Otto.structured_log(:debug, 'Route matched',
169
166
  Otto::LoggingHelpers.request_context(env).merge(
170
167
  pattern: route.pattern.source,
171
168
  handler: route.route_definition.definition,
172
169
  auth_strategy: route.route_definition.auth_requirement || 'none',
173
170
  route_params: extra_params
174
- )
175
- )
171
+ ))
176
172
  break
177
173
  end
178
174
 
@@ -180,19 +176,17 @@ class Otto
180
176
  if found_route
181
177
  # Log 404 route usage if we fell back to it
182
178
  if found_route == literal_routes['/404']
183
- Otto.structured_log(:info, "Route not found",
179
+ Otto.structured_log(:info, 'Route not found',
184
180
  Otto::LoggingHelpers.request_context(env).merge(
185
181
  fallback_to: '404_route'
186
- )
187
- )
182
+ ))
188
183
  end
189
184
  found_route.call env, extra_params
190
185
  else
191
- Otto.structured_log(:info, "Route not found",
186
+ Otto.structured_log(:info, 'Route not found',
192
187
  Otto::LoggingHelpers.request_context(env).merge(
193
188
  fallback_to: 'default_not_found'
194
- )
195
- )
189
+ ))
196
190
  @not_found || Otto::Static.not_found
197
191
  end
198
192
  end
@@ -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
@@ -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
@@ -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)
@@ -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 = Rack::Request.new(env)
27
+ res = Rack::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,6 +46,77 @@ class Otto
32
46
  @target_class ||= safe_const_get(route_definition.klass_name)
33
47
  end
34
48
 
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
60
+ # @param req [Rack::Request] Request object
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
+
35
120
  # Setup request and response with the same extensions and processing as Route#call
36
121
  # @param req [Rack::Request] Request object
37
122
  # @param res [Rack::Response] Response object
@@ -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
@@ -1,7 +1,6 @@
1
1
  # lib/otto/route_handlers/instance_method.rb
2
2
  #
3
3
  # frozen_string_literal: true
4
- require 'securerandom'
5
4
 
6
5
  require_relative 'base'
7
6
 
@@ -17,62 +16,16 @@ class Otto
17
16
  # Use this handler for endpoints requiring request-level control (logout,
18
17
  # session management, cookie manipulation, custom header handling).
19
18
  class InstanceMethodHandler < BaseHandler
20
- def call(env, extra_params = {})
21
- start_time = Otto::Utils.now_in_μs
22
- req = Rack::Request.new(env)
23
- res = Rack::Response.new
24
-
25
- begin
26
- # Apply the same extensions and processing as original Route#call
27
- setup_request_response(req, res, env, extra_params)
28
-
29
- # Create instance and call method (existing Otto behavior)
30
- instance = target_class.new(req, res)
31
- result = instance.send(route_definition.method_name)
32
-
33
- # Only handle response if response_type is not default
34
- if route_definition.response_type != 'default'
35
- handle_response(result, res, {
36
- instance: instance,
37
- request: req,
38
- })
39
- end
40
- rescue StandardError => e
41
- # Check if we're being called through Otto's integrated context (vs direct handler testing)
42
- # In integrated context, let Otto's centralized error handler manage the response
43
- # In direct testing context, handle errors locally for unit testing
44
- if otto_instance
45
- # Store handler context in env for centralized error handler
46
- handler_name = "#{target_class}##{route_definition.method_name}"
47
- env['otto.handler'] = handler_name
48
- env['otto.handler_duration'] = Otto::Utils.now_in_μs - start_time
49
-
50
- raise e # Re-raise to let Otto's centralized error handler manage the response
51
- else
52
- # Direct handler testing context - handle errors locally with security improvements
53
- error_id = SecureRandom.hex(8)
54
- Otto.logger.error "[#{error_id}] #{e.class}: #{e.message}"
55
- Otto.logger.debug "[#{error_id}] Backtrace: #{e.backtrace.join("\n")}" if Otto.debug
56
-
57
- res.status = 500
58
- res.headers['content-type'] = 'text/plain'
59
-
60
- if Otto.env?(:dev, :development)
61
- res.write "Server error (ID: #{error_id}). Check logs for details."
62
- else
63
- res.write 'An error occurred. Please try again later.'
64
- end
65
-
66
- # Add security headers if available
67
- if otto_instance.respond_to?(:security_config) && otto_instance.security_config
68
- otto_instance.security_config.security_headers.each do |header, value|
69
- res.headers[header] = value
70
- end
71
- end
72
- end
73
- end
74
-
75
- finalize_response(res)
19
+ protected
20
+
21
+ # Invoke the instance 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
+ instance = target_class.new(req, res)
27
+ result = instance.send(route_definition.method_name)
28
+ [result, { instance: instance, request: req }]
76
29
  end
77
30
  end
78
31
  end