This guide helps you migrate from deprecated Proc constraints to secure alternatives in Ruby Routes.
Proc constraints pose significant security risks:
- Code Execution: Can run arbitrary code
- Injection Attacks: Vulnerable to malicious input
- DoS Attacks: Can cause infinite loops or resource exhaustion
- Audit Difficulty: Hard to analyze for security issues
# ❌ Before (Proc)
constraints: { id: ->(v) { v.to_i > 0 } }
constraints: { age: ->(v) { v.to_i.between?(18, 120) } }
constraints: { price: ->(v) { v.to_f >= 0.01 } }
# ✅ After (Hash constraints)
constraints: { id: { range: 1..Float::INFINITY } }
constraints: { age: { range: 18..120 } }
constraints: { price: { range: 0.01..Float::INFINITY } }# ❌ Before (Proc)
constraints: { username: ->(v) { v.length >= 3 } }
constraints: { title: ->(v) { v.length.between?(5, 100) } }
constraints: { code: ->(v) { v.length == 6 } }
# ✅ After (Hash constraints)
constraints: { username: { min_length: 3 } }
constraints: { title: { min_length: 5, max_length: 100 } }
constraints: { code: { min_length: 6, max_length: 6 } }# ❌ Before (Proc)
constraints: { status: ->(v) { %w[active inactive pending].include?(v) } }
constraints: { role: ->(v) { !%w[admin superuser].include?(v) } }
# ✅ After (Hash constraints)
constraints: { status: { in: %w[active inactive pending] } }
constraints: { role: { not_in: %w[admin superuser] } }# ❌ Before (Proc)
constraints: { email: ->(v) { v.match?(/\A[^@]+@[^@]+\.[^@]+\z/) } }
constraints: { slug: ->(v) { v.match?(/\A[a-z0-9-]+\z/) } }
constraints: { uuid: ->(v) { v.match?(/\A[0-9a-f-]{36}\z/i) } }
# ✅ After (Built-in types)
constraints: { email: :email }
constraints: { slug: :slug }
constraints: { uuid: :uuid }
# ✅ Or hash constraints for custom patterns
constraints: {
custom_id: { format: /\A[A-Z]{2}\d{6}\z/ }
}# ❌ Before (Proc)
constraints: {
username: ->(v) {
v.length >= 3 &&
v.length <= 20 &&
v.match?(/\A[a-zA-Z0-9_]+\z/) &&
!%w[admin root].include?(v)
}
}
# ✅ After (Combined hash constraints)
constraints: {
username: {
min_length: 3,
max_length: 20,
format: /\A[a-zA-Z0-9_]+\z/,
not_in: %w[admin root]
}
}Search your codebase for Proc constraints:
# Find Proc constraints in your routes
grep -r "constraints.*->" app/
grep -r "constraints.*proc" app/
grep -r "constraints.*lambda" app/For each Proc constraint, determine what it's validating:
- Numeric ranges?
- String length?
- Format patterns?
- Allowed/forbidden values?
Use this decision tree:
Is it validating...
├── Email format? → Use :email
├── UUID format? → Use :uuid
├── Slug format? → Use :slug
├── Integer format? → Use :int
├── Alphabetic only? → Use :alpha
├── Alphanumeric only? → Use :alphanumeric
├── Numeric range? → Use { range: min..max }
├── String length? → Use { min_length: X, max_length: Y }
├── Allowed values? → Use { in: [...] }
├── Forbidden values? → Use { not_in: [...] }
├── Custom pattern? → Use { format: /regex/ }
└── Multiple conditions? → Combine hash options
# Create a test to verify behavior matches
RSpec.describe "Constraint Migration" do
it "maintains same validation behavior" do
# Old constraint (for reference)
old_constraint = ->(v) { v.to_i.between?(1, 100) }
# New constraint
route = RubyRoutes::RadixTree.new('/test/:num', to: 'test#show',
constraints: { num: { range: 1..100 } })
# Test valid values
expect(route.extract_params('/test/50')['num']).to eq('50')
# Test invalid values
expect { route.extract_params('/test/150') }
.to raise_error(RubyRoutes::ConstraintViolation)
end
end- Deploy the changes
- Monitor for
ConstraintViolationexceptions - Check logs for any unexpected behavior
- Verify performance hasn't degraded
# ❌ Before
routes.draw do
get '/products/:id', to: 'products#show',
constraints: { id: ->(v) { v.to_i > 0 } }
get '/categories/:slug', to: 'categories#show',
constraints: { slug: ->(v) { v.match?(/\A[a-z0-9-]+\z/) } }
get '/users/:email', to: 'users#show',
constraints: { email: ->(v) { v.include?('@') } }
end
# ✅ After
routes.draw do
get '/products/:id', to: 'products#show',
constraints: { id: :int }
get '/categories/:slug', to: 'categories#show',
constraints: { slug: :slug }
get '/users/:email', to: 'users#show',
constraints: { email: :email }
end# ❌ Before
routes.draw do
namespace :api do
get '/:version/users/:id', to: 'users#show',
constraints: {
version: ->(v) { %w[v1 v2 v3].include?(v) },
id: ->(v) { v.to_i.between?(1, 999999) }
}
end
end
# ✅ After
routes.draw do
namespace :api do
get '/:version/users/:id', to: 'users#show',
constraints: {
version: { in: %w[v1 v2 v3] },
id: { range: 1..999999 }
}
end
endIf your Proc has complex business logic:
# ❌ Complex Proc
constraints: {
code: ->(v) {
return false unless v.length == 8
return false unless v[0..1].match?(/[A-Z]{2}/)
return false unless v[2..7].match?(/\d{6}/)
!FORBIDDEN_PREFIXES.include?(v[0..1])
}
}Solution: Break it down into multiple hash constraints:
# ✅ Multiple hash constraints
constraints: {
code: {
min_length: 8,
max_length: 8,
format: /\A[A-Z]{2}\d{6}\z/,
not_in: FORBIDDEN_CODES
}
}If your Proc uses dynamic data:
# ❌ Dynamic Proc
constraints: {
user_id: ->(v) { User.exists?(id: v.to_i) }
}Solution: Move validation to controller:
# ✅ Controller validation
constraints: { user_id: :int }
# In controller:
def show
@user = User.find(params[:user_id])
rescue ActiveRecord::RecordNotFound
render_not_found
endAfter migration, you'll see:
- Faster routing: Built-in constraints are highly optimized
- Better security: No code execution risks
- Easier debugging: Clear constraint definitions
- Better caching: Constraints can be cached more effectively
If you encounter issues during migration:
- Check the Constraints Documentation
- Review common patterns above
- Create tests to verify behavior
- Consider moving complex logic to controllers
Remember: The goal is to maintain the same validation behavior while eliminating security risks.