Otto v2.0.0 Migration Guide
Overview
This guide covers upgrading from Otto v1.x to v2.0.0. If you are already on v2.x, see the later guides in this directory (for example v2.3.0) for changes since.
Otto v2.0.0 is a major release introducing a modular architecture, security and privacy by default, and handler-level authentication. The main themes are:
- Modular Architecture: Core functionality extracted into focused modules (Router, Configuration, ErrorHandler, etc.) using composition patterns
- Security by Default: IP privacy middleware, configuration freezing, backtrace sanitization
- Privacy by Default: Public IP masking runs automatically; no original values stored
- Handler-Level Authentication: Auth moved from middleware to RouteAuthWrapper, executing after routing for precise control
- Improved Developer Experience: Unified middleware API, typed request/response helpers, structured logging
Breaking Changes Summary
| Change | Before | After |
|---|---|---|
| Middleware registration | otto.middleware_stack << |
otto.use() |
| Authentication location | Middleware (before routing) | RouteAuthWrapper (after routing) |
| Logic class constructor | initialize(session, user, params, locale) |
initialize(context, params, locale) |
| StrategyResult checks | success? / failure? |
authenticated? / auth_attempt_succeeded? |
| Request completion callback | Otto.on_request_complete (class) |
otto.on_request_complete (instance) |
| Timing precision | Milliseconds | Microseconds |
Quick Migration Checklist
- [ ] Replace
otto.middleware_stack <<withotto.use() - [ ] Remove
enable_authentication!calls (RouteAuthWrapper handles auth automatically) - [ ] Update Logic class constructors: 4 params to 3 params (
context, params, locale) - [ ] Replace
@session/@userwith@contextin Logic classes - [ ] Replace
success?/failure?withauthenticated?or type checks - [ ] Change
Otto.on_request_completetootto.on_request_complete(instance method) - [ ] Update timing expectations from milliseconds to microseconds
- [ ] Register custom error handlers before first request
- [ ] Register request/response helpers before first request
- [ ] Run full test suite and verify all routes work
Detailed Migration Sections
Middleware Configuration
The middleware stack API has been unified for consistency and performance.
Before:
otto.middleware_stack << SomeMiddleware
otto.middleware.add(AnotherMiddleware)
After:
otto.use(SomeMiddleware)
otto.use(AnotherMiddleware)
Key improvements:
- O(1) middleware lookup using Set-based tracking
- Memoized middleware list reduces array creation
- Prevents duplicate middleware registrations
- Use
otto.middleware.includes?()for stack checks
See v2.0.0-pre1 migration guide for detailed examples.
Authentication Setup
Authentication has moved from middleware to handler level via RouteAuthWrapper.
What changed:
AuthenticationMiddlewareremoved (executed before routing)enable_authentication!removed (no longer needed)- RouteAuthWrapper now wraps all routes automatically
env['otto.strategy_result']guaranteed present on all routes
Configuration:
# Add strategies (before first request)
otto.add_auth_strategy('session', SessionStrategy.new)
otto.add_auth_strategy('apikey', APIKeyStrategy.new(api_keys: ENV.fetch('API_KEYS').split(',')))
# Configure login redirect path (optional)
otto.auth_config[:login_path] = '/auth/login' # Default: '/signin'
Route definitions:
# Routes file - strategies execute left-to-right, first success wins
GET /api/data DataLogic#show auth=session,apikey
# With role requirements (OR logic)
GET /admin/users AdminLogic auth=session role=admin,editor
Logic Class Constructor Pattern
Logic classes now receive an immutable context object instead of separate parameters.
Before:
class MyLogic
def initialize(session, user, params, locale)
@session = session
@user = user
@params = params
@locale = locale
end
def raise_concerns
raise 'Access denied' unless @user&.dig('role') == 'admin'
end
end
After:
class MyLogic
def initialize(context, params, locale)
@context = context
@params = params
@locale = locale
end
def raise_concerns
raise 'Access denied' unless @context.has_role?('admin')
end
end
Context API:
@context.authenticated? # User in session (request state)
@context.anonymous? # Not authenticated
@context.auth_attempt_succeeded? # Auth strategy just succeeded (auth outcome)
@context.user_id # User ID
@context.user_name # User name
@context.session_id # Session ID
@context.has_role?('admin') # Role check
@context.('write') # Permission check
@context.roles # Array of roles
@context. # Array of permissions
See v2.0.0-pre1 migration guide for comprehensive examples.
StrategyResult Semantics
The success? and failure? methods have been removed. Use semantic methods instead.
Before:
if @strategy_result.success?
# Always true for StrategyResult, meaningless
end
After:
# For "user in session" checks (registration, profile access)
if @strategy_result.authenticated?
raise "Already signed up"
end
# For "just logged in" checks (redirects, welcome messages)
if @strategy_result.auth_attempt_succeeded?
redirect_to dashboard_path
end
# For type checking
if strategy_result.is_a?(Otto::Security::Authentication::StrategyResult)
# handle authenticated request
end
See v2.0.0-pre2 migration guide for detailed semantic explanations.
Error Handling
Register handlers for expected business logic errors to avoid 500 error logging.
otto = Otto.new('routes.txt')
# Register before first request
otto.register_error_handler(YourApp::NotFound, status: 404, log_level: :info)
otto.register_error_handler(YourApp::RateLimited, status: 429, log_level: :warn)
# With custom response handler
otto.register_error_handler(YourApp::ValidationError, status: 422) do |error, env|
{ errors: error.details }.to_json
end
Framework error classes now have proper HTTP status codes:
Otto::NotFoundError- 404Otto::BadRequestError- 400Otto::ForbiddenError- 403Otto::UnauthorizedError- 401Otto::PayloadTooLargeError- 413
Request/Response Helpers
Register application-specific helpers that integrate with Otto features.
module YourApp::RequestHelpers
def current_customer
user = strategy_result&.user
user.is_a?(YourApp::Customer) ? user : YourApp::Customer.anonymous
end
end
module YourApp::ResponseHelpers
def (customer)
('customer_id', value: customer.id, httponly: true)
end
end
otto = Otto.new('routes.txt')
otto.register_request_helpers(YourApp::RequestHelpers)
otto.register_response_helpers(YourApp::ResponseHelpers)
Helpers are included at class level, not per-request extension.
Reserved method names - Do not override these in helper modules:
- Request:
env,params,cookies,session,path,request_method,ip, etc. - Response:
status,headers,body,finish,write,redirect, etc. - Otto-specific:
request,app_path,masked_ip,hashed_ip,secure?,local?,ajax?
Request Completion Callbacks
Callbacks are now instance methods to prevent duplicate invocations in multi-app architectures.
Before:
Otto.on_request_complete { |req, res, duration| ... }
After:
otto.on_request_complete { |req, res, duration| ... }
The callback now receives a Rack::Response object instead of [status, headers, body] tuple.
Timing Precision
All timing now uses microseconds via Otto::Utils.now_in_μs.
Before: 15.2 (milliseconds as float)
After: 15200 (microseconds as integer)
Update any code that parses or displays timing values.
New Features to Adopt
IP Privacy Middleware
Automatically masks public IP addresses while preserving private/localhost IPs:
- Public IPs masked:
192.0.2.100becomes192.0.2.0 - Private IPs preserved:
127.0.0.1,192.168.x.x,10.x.x.x - Runs FIRST in middleware stack
No configuration needed - enabled by default.
Structured Logging
Otto.structured_log(:debug, "Route matched",
Otto::LoggingHelpers.request_context(env).merge(
type: 'literal',
handler: route.definition
)
)
# For timed operations
Otto::LoggingHelpers.log_timed_operation(:info, "Operation", env, key: value) do
perform_operation()
end
Configuration Freezing
Otto automatically freezes all configuration after the first request. Complete all setup before handling requests:
otto = Otto.new('routes.txt')
# All configuration must happen here
otto.add_auth_strategy('session', SessionStrategy.new)
otto.register_error_handler(MyError, status: 400)
otto.register_request_helpers(MyHelpers)
# Configuration freezes on first request
run otto
Base Error Classes
Subclass framework error classes for consistent HTTP semantics:
class RecordNotFound < Otto::NotFoundError; end # Returns 404
class InvalidInput < Otto::BadRequestError; end # Returns 400
class AccessDenied < Otto::ForbiddenError; end # Returns 403
Routes with JSON Response Type
Routes declaring response=json now return JSON errors instead of redirects:
# Routes file
GET /api/data ApiLogic#show auth=session response=json
Auth failures return 401 JSON instead of 302 redirect.
Testing Your Migration
# Run full test suite
bundle exec rspec
# Verify specific patterns were updated
grep -r "@strategy_result.success?" app/
grep -r "middleware_stack <<" config/
grep -r "Otto.on_request_complete" app/
Questions?
- Pre-release guides: v2.0.0-pre1, v2.0.0-pre2
- Source documentation:
lib/otto/security/authentication/strategy_result.rb,lib/otto/env_keys.rb - Issues: github.com/delano/otto/issues