require_relative '../lib/otto'
module MyApp
class MissingResourceError < StandardError; end
class ExpiredResourceError < StandardError; end
class RateLimitError < StandardError
attr_reader :retry_after
def initialize(message, retry_after: 60)
super(message)
@retry_after = retry_after
end
end
end
routes_content = <<~ROUTES
GET /resource/:id ResourceHandler.show
POST /action ActionHandler.process
ROUTES
File.write('/tmp/otto_error_routes.txt', routes_content)
class ResourceHandler
def self.show(req, res)
resource_id = req.params[:id]
raise MyApp::MissingResourceError, "Resource #{resource_id} not found" if resource_id == 'missing'
raise MyApp::ExpiredResourceError, "Resource #{resource_id} expired" if resource_id == 'expired'
res.status = 200
res.['Content-Type'] = 'application/json'
res.write(JSON.generate({ id: resource_id, name: "Resource #{resource_id}" }))
end
end
class ActionHandler
def self.process(req, res)
raise MyApp::RateLimitError.new('Too many requests', retry_after: 120)
end
end
otto = Otto.new('/tmp/otto_error_routes.txt')
puts "Registering error handlers..."
otto.register_error_handler(MyApp::MissingResourceError, status: 404, log_level: :info)
otto.register_error_handler(MyApp::ExpiredResourceError, status: 410, log_level: :info)
otto.register_error_handler(MyApp::RateLimitError, status: 429, log_level: :warn) do |error, req|
{
error: 'RateLimited',
message: error.message,
retry_after: error.retry_after,
path: req.path
}
end
puts "\n=== Test 1: Missing Resource (404) ==="
env = {
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/resource/missing',
'HTTP_ACCEPT' => 'application/json',
'REMOTE_ADDR' => '127.0.0.1'
}
status, , body = otto.call(env)
puts "Status: #{status}"
puts "Content-Type: #{['content-type']}"
puts "Body: #{body.first}"
puts "Log level: INFO (not ERROR)"
puts "\n=== Test 2: Expired Resource (410) ==="
env['PATH_INFO'] = '/resource/expired'
status, , body = otto.call(env)
puts "Status: #{status}"
puts "Content-Type: #{['content-type']}"
puts "Body: #{body.first}"
puts "Log level: INFO (not ERROR)"
puts "\n=== Test 3: Rate Limited (429) with custom handler ==="
env = {
'REQUEST_METHOD' => 'POST',
'PATH_INFO' => '/action',
'HTTP_ACCEPT' => 'application/json',
'REMOTE_ADDR' => '127.0.0.1'
}
status, , body = otto.call(env)
puts "Status: #{status}"
puts "Content-Type: #{['content-type']}"
puts "Body: #{body.first}"
puts "Log level: WARN (not ERROR)"
puts "Custom fields: retry_after included"
puts "\n=== Test 4: Successful Request ==="
env = {
'REQUEST_METHOD' => 'GET',
'PATH_INFO' => '/resource/123',
'HTTP_ACCEPT' => 'application/json',
'REMOTE_ADDR' => '127.0.0.1'
}
status, , body = otto.call(env)
puts "Status: #{status}"
puts "Content-Type: #{['content-type']}"
puts "Body: #{body.first}"
puts "\n=== Benefits ==="
puts "✓ Expected errors return proper HTTP status codes (not 500)"
puts "✓ Logged at INFO/WARN level (not ERROR)"
puts "✓ No backtrace spam for expected conditions"
puts "✓ Still generates error IDs for correlation"
puts "✓ Custom response handlers for complex error data"
puts "✓ Content negotiation (JSON/plain text) automatic"
File.delete('/tmp/otto_error_routes.txt') if File.exist?('/tmp/otto_error_routes.txt')