Agent skill
rack-middleware
Rack middleware development, configuration, and integration patterns. Use when working with middleware stacks or creating custom middleware.
Stars
163
Forks
31
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/rack-middleware-geoffjay-claude-plugins
SKILL.md
Rack Middleware Skill
Tier 1: Quick Reference - Middleware Basics
Middleware Structure
ruby
class MyMiddleware
def initialize(app, options = {})
@app = app
@options = options
end
def call(env)
# Before request
# Modify env if needed
# Call next middleware
status, headers, body = @app.call(env)
# After request
# Modify response if needed
[status, headers, body]
end
end
# Usage
use MyMiddleware, option: 'value'
Common Middleware
ruby
# Session management
use Rack::Session::Cookie, secret: ENV['SESSION_SECRET']
# Security
use Rack::Protection
# Compression
use Rack::Deflater
# Logging
use Rack::CommonLogger
# Static files
use Rack::Static, urls: ['/css', '/js'], root: 'public'
Middleware Ordering
ruby
# config.ru - Correct order
use Rack::Deflater # 1. Compression
use Rack::Static # 2. Static files
use Rack::CommonLogger # 3. Logging
use Rack::Session::Cookie # 4. Sessions
use Rack::Protection # 5. Security
use CustomAuth # 6. Authentication
run Application # 7. Application
Request/Response Access
ruby
class SimpleMiddleware
def initialize(app)
@app = app
end
def call(env)
# Access request via env hash
method = env['REQUEST_METHOD']
path = env['PATH_INFO']
query = env['QUERY_STRING']
# Or use Rack::Request
request = Rack::Request.new(env)
params = request.params
# Process request
status, headers, body = @app.call(env)
# Modify response
headers['X-Custom-Header'] = 'value'
[status, headers, body]
end
end
Tier 2: Detailed Instructions - Advanced Middleware
Custom Middleware Development
Request Logging Middleware:
ruby
require 'logger'
class RequestLogger
def initialize(app, options = {})
@app = app
@logger = options[:logger] || Logger.new(STDOUT)
@skip_paths = options[:skip_paths] || []
end
def call(env)
return @app.call(env) if skip_logging?(env)
start_time = Time.now
request = Rack::Request.new(env)
log_request_start(request)
status, headers, body = @app.call(env)
duration = Time.now - start_time
log_request_end(request, status, duration)
[status, headers, body]
rescue StandardError => e
log_error(request, e)
raise
end
private
def skip_logging?(env)
path = env['PATH_INFO']
@skip_paths.any? { |skip| path.start_with?(skip) }
end
def log_request_start(request)
@logger.info({
event: 'request.start',
method: request.request_method,
path: request.path,
ip: request.ip,
user_agent: request.user_agent
}.to_json)
end
def log_request_end(request, status, duration)
@logger.info({
event: 'request.end',
method: request.request_method,
path: request.path,
status: status,
duration: duration.round(3)
}.to_json)
end
def log_error(request, error)
@logger.error({
event: 'request.error',
method: request.request_method,
path: request.path,
error: error.class.name,
message: error.message,
backtrace: error.backtrace[0..5]
}.to_json)
end
end
# Usage
use RequestLogger, skip_paths: ['/health', '/metrics']
Authentication Middleware:
ruby
class TokenAuthentication
def initialize(app, options = {})
@app = app
@token_header = options[:header] || 'HTTP_AUTHORIZATION'
@skip_paths = options[:skip_paths] || []
@realm = options[:realm] || 'Application'
end
def call(env)
return @app.call(env) if skip_authentication?(env)
token = extract_token(env)
if valid_token?(token)
user = find_user_by_token(token)
env['current_user'] = user
@app.call(env)
else
unauthorized_response
end
end
private
def skip_authentication?(env)
path = env['PATH_INFO']
method = env['REQUEST_METHOD']
# Skip for public paths
@skip_paths.any? { |skip| path.start_with?(skip) } ||
# Skip for OPTIONS (CORS preflight)
method == 'OPTIONS'
end
def extract_token(env)
auth_header = env[@token_header]
return nil unless auth_header
# Support "Bearer TOKEN" format
if auth_header.start_with?('Bearer ')
auth_header.split(' ', 2).last
else
auth_header
end
end
def valid_token?(token)
return false unless token
# Implement your token validation logic
# This is a placeholder
token.length >= 32
end
def find_user_by_token(token)
# Implement your user lookup logic
# This is a placeholder
{ id: 1, email: 'user@example.com' }
end
def unauthorized_response
[
401,
{
'Content-Type' => 'application/json',
'WWW-Authenticate' => "Bearer realm=\"#{@realm}\""
},
['{"error": "Unauthorized"}']
]
end
end
# Usage
use TokenAuthentication,
skip_paths: ['/login', '/register', '/public']
Caching Middleware:
ruby
require 'digest/md5'
class SimpleCache
def initialize(app, options = {})
@app = app
@cache = {}
@ttl = options[:ttl] || 300 # 5 minutes
@cache_methods = options[:methods] || ['GET']
end
def call(env)
request = Rack::Request.new(env)
return @app.call(env) unless cacheable?(request)
cache_key = generate_cache_key(env)
if cached_response = get_from_cache(cache_key)
return cached_response
end
status, headers, body = @app.call(env)
if cacheable_response?(status)
cache_response(cache_key, [status, headers, body])
end
[status, headers, body]
end
private
def cacheable?(request)
@cache_methods.include?(request.request_method)
end
def cacheable_response?(status)
status == 200
end
def generate_cache_key(env)
# Include method, path, and query string
Digest::MD5.hexdigest([
env['REQUEST_METHOD'],
env['PATH_INFO'],
env['QUERY_STRING']
].join('|'))
end
def get_from_cache(key)
entry = @cache[key]
return nil unless entry
# Check if cache entry is still valid
if Time.now - entry[:cached_at] <= @ttl
entry[:response]
else
@cache.delete(key)
nil
end
end
def cache_response(key, response)
@cache[key] = {
response: response,
cached_at: Time.now
}
end
end
# Usage with Redis for distributed caching
class RedisCache
def initialize(app, options = {})
@app = app
@redis = Redis.new(url: options[:redis_url])
@ttl = options[:ttl] || 300
@namespace = options[:namespace] || 'cache'
end
def call(env)
request = Rack::Request.new(env)
return @app.call(env) unless request.get?
cache_key = generate_cache_key(env)
if cached = @redis.get(cache_key)
return Marshal.load(cached)
end
status, headers, body = @app.call(env)
if status == 200
@redis.setex(cache_key, @ttl, Marshal.dump([status, headers, body]))
end
[status, headers, body]
end
private
def generate_cache_key(env)
"#{@namespace}:#{Digest::MD5.hexdigest(env['PATH_INFO'] + env['QUERY_STRING'])}"
end
end
Request Transformation Middleware:
ruby
class JSONBodyParser
def initialize(app)
@app = app
end
def call(env)
if json_request?(env)
body = env['rack.input'].read
env['rack.input'].rewind
begin
parsed = JSON.parse(body)
env['rack.request.form_hash'] = parsed
env['parsed_json'] = parsed
rescue JSON::ParserError => e
return error_response('Invalid JSON', 400)
end
end
@app.call(env)
end
private
def json_request?(env)
content_type = env['CONTENT_TYPE']
content_type && content_type.include?('application/json')
end
def error_response(message, status)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: message }.to_json]
]
end
end
# XML Parser
class XMLBodyParser
def initialize(app)
@app = app
end
def call(env)
if xml_request?(env)
body = env['rack.input'].read
env['rack.input'].rewind
begin
parsed = Hash.from_xml(body)
env['rack.request.form_hash'] = parsed
env['parsed_xml'] = parsed
rescue StandardError => e
return error_response('Invalid XML', 400)
end
end
@app.call(env)
end
private
def xml_request?(env)
content_type = env['CONTENT_TYPE']
content_type && (content_type.include?('application/xml') ||
content_type.include?('text/xml'))
end
def error_response(message, status)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: message }.to_json]
]
end
end
Middleware Ordering Patterns
Security-First Stack:
ruby
# config.ru
# 1. SSL redirect (production only)
use Rack::SSL if ENV['RACK_ENV'] == 'production'
# 2. Rate limiting (before everything else)
use Rack::Attack
# 3. Security headers
use SecurityHeaders
# 4. CORS (for API applications)
use Rack::Cors do
allow do
origins '*'
resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options]
end
end
# 5. Compression
use Rack::Deflater
# 6. Static files
use Rack::Static, urls: ['/public'], root: 'public'
# 7. Logging
use Rack::CommonLogger
# 8. Request parsing
use JSONBodyParser
# 9. Sessions
use Rack::Session::Cookie,
secret: ENV['SESSION_SECRET'],
same_site: :strict,
httponly: true,
secure: ENV['RACK_ENV'] == 'production'
# 10. Protection (CSRF, etc.)
use Rack::Protection
# 11. Authentication
use TokenAuthentication, skip_paths: ['/login', '/public']
# 12. Performance monitoring
use PerformanceMonitor
# 13. Application
run Application
API-Focused Stack:
ruby
# config.ru for API
# 1. CORS first for preflight
use Rack::Cors do
allow do
origins ENV.fetch('ALLOWED_ORIGINS', '*').split(',')
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options],
credentials: true,
max_age: 86400
end
end
# 2. Rate limiting
use Rack::Attack
# 3. Compression
use Rack::Deflater
# 4. Logging (structured JSON logs)
use RequestLogger
# 5. Request parsing
use JSONBodyParser
# 6. Authentication
use TokenAuthentication, skip_paths: ['/auth']
# 7. Caching
use RedisCache, ttl: 300
# 8. Application
run API
Conditional Middleware
Environment-Based:
ruby
class ConditionalMiddleware
def initialize(app, condition, middleware, *args)
@app = if condition.call
middleware.new(app, *args)
else
app
end
end
def call(env)
@app.call(env)
end
end
# Usage
use ConditionalMiddleware,
-> { ENV['RACK_ENV'] == 'development' },
Rack::ShowExceptions
use ConditionalMiddleware,
-> { ENV['ENABLE_PROFILING'] == 'true' },
RackMiniProfiler
Path-Based:
ruby
class PathBasedMiddleware
def initialize(app, pattern, middleware, *args)
@app = app
@pattern = pattern
@middleware = middleware.new(app, *args)
end
def call(env)
if env['PATH_INFO'].match?(@pattern)
@middleware.call(env)
else
@app.call(env)
end
end
end
# Usage
use PathBasedMiddleware, %r{^/api}, CacheMiddleware, ttl: 300
use PathBasedMiddleware, %r{^/admin}, AdminAuth
Error Handling Middleware
ruby
class ErrorHandler
def initialize(app, options = {})
@app = app
@logger = options[:logger] || Logger.new(STDOUT)
@error_handlers = options[:handlers] || {}
end
def call(env)
@app.call(env)
rescue StandardError => e
handle_error(env, e)
end
private
def handle_error(env, error)
request = Rack::Request.new(env)
# Log error
@logger.error({
error: error.class.name,
message: error.message,
path: request.path,
method: request.request_method,
backtrace: error.backtrace[0..10]
}.to_json)
# Custom handler for specific error types
if handler = @error_handlers[error.class]
return handler.call(error)
end
# Default error response
status = status_for_error(error)
[
status,
{ 'Content-Type' => 'application/json' },
[{ error: error.message, type: error.class.name }.to_json]
]
end
def status_for_error(error)
case error
when ArgumentError, ValidationError
400
when NotFoundError
404
when AuthorizationError
403
when AuthenticationError
401
else
500
end
end
end
# Usage
use ErrorHandler,
handlers: {
ValidationError => ->(e) {
[422, { 'Content-Type' => 'application/json' },
[{ error: e.message, details: e.details }.to_json]]
}
}
Tier 3: Resources & Examples
Complete Middleware Examples
Performance Monitoring:
ruby
class PerformanceMonitor
def initialize(app, options = {})
@app = app
@threshold = options[:threshold] || 1.0 # 1 second
@logger = options[:logger] || Logger.new(STDOUT)
end
def call(env)
start_time = Time.now
memory_before = memory_usage
status, headers, body = @app.call(env)
duration = Time.now - start_time
memory_after = memory_usage
memory_delta = memory_after - memory_before
# Add performance headers
headers['X-Runtime'] = duration.to_s
headers['X-Memory-Delta'] = memory_delta.to_s
# Log slow requests
if duration > @threshold
log_slow_request(env, duration, memory_delta)
end
[status, headers, body]
end
private
def memory_usage
`ps -o rss= -p #{Process.pid}`.to_i / 1024.0 # MB
end
def log_slow_request(env, duration, memory)
@logger.warn({
event: 'slow_request',
method: env['REQUEST_METHOD'],
path: env['PATH_INFO'],
duration: duration.round(3),
memory_delta: memory.round(2)
}.to_json)
end
end
Request ID Tracking:
ruby
class RequestID
def initialize(app, options = {})
@app = app
@header = options[:header] || 'X-Request-ID'
end
def call(env)
request_id = env["HTTP_#{@header.upcase.tr('-', '_')}"] || generate_id
env['request.id'] = request_id
status, headers, body = @app.call(env)
headers[@header] = request_id
[status, headers, body]
end
private
def generate_id
SecureRandom.uuid
end
end
Response Modification:
ruby
class ResponseTransformer
def initialize(app, &block)
@app = app
@transformer = block
end
def call(env)
status, headers, body = @app.call(env)
if should_transform?(headers)
body = transform_body(body)
end
[status, headers, body]
end
private
def should_transform?(headers)
headers['Content-Type']&.include?('application/json')
end
def transform_body(body)
content = body.is_a?(Array) ? body.join : body.read
transformed = @transformer.call(content)
[transformed]
end
end
# Usage
use ResponseTransformer do |body|
data = JSON.parse(body)
data['timestamp'] = Time.now.to_i
data.to_json
end
Testing Middleware
ruby
RSpec.describe RequestLogger do
let(:app) { ->(env) { [200, {}, ['OK']] } }
let(:logger) { double('Logger', info: nil, error: nil) }
let(:middleware) { RequestLogger.new(app, logger: logger) }
let(:request) { Rack::MockRequest.new(middleware) }
describe 'request logging' do
it 'logs request start' do
expect(logger).to receive(:info).with(hash_including(event: 'request.start'))
request.get('/')
end
it 'logs request end with duration' do
expect(logger).to receive(:info).with(hash_including(
event: 'request.end',
duration: kind_of(Numeric)
))
request.get('/')
end
it 'includes request details' do
expect(logger).to receive(:info).with(hash_including(
method: 'GET',
path: '/test'
))
request.get('/test')
end
end
describe 'error logging' do
let(:app) { ->(env) { raise StandardError, 'Test error' } }
it 'logs errors' do
expect(logger).to receive(:error).with(hash_including(
event: 'request.error',
error: 'StandardError'
))
expect { request.get('/') }.to raise_error(StandardError)
end
end
describe 'skip paths' do
let(:middleware) { RequestLogger.new(app, logger: logger, skip_paths: ['/health']) }
it 'skips logging for configured paths' do
expect(logger).not_to receive(:info)
request.get('/health')
end
end
end
Additional Resources
- Middleware Template:
assets/middleware-template.rb- Boilerplate for new middleware - Middleware Examples:
assets/middleware-examples/- Collection of useful middleware - Configuration Guide:
assets/configuration-guide.md- Best practices for middleware configuration - Performance Guide:
references/performance-optimization.md- Optimizing middleware performance - Testing Guide:
references/middleware-testing.md- Comprehensive testing strategies
Didn't find tool you were looking for?