`Sinatra::Extension` and `Sinatra::Namespace` collapse keyword arguments into a positional Hash
Author: SeanLFCreated Jul 23, 2026Updated Jul 27, 2026
Middleware keyword arguments are lost when use is called inside a Sinatra::Extension body or a namespace block. Middleware that declares real keyword parameters raises ArgumentError.
Both are in sinatra-contrib. Neither is a regression; both reproduce on main.
Repro 1: Sinatra::Extension
require 'sinatra/base'
require 'sinatra/extension'
class Logger
def initialize(app, level: :info)
@app, @level = app, level
end
def call(env) = @app.call(env)
end
module MyExtension
extend Sinatra::Extension
use Logger, level: :debug
end
class App < Sinatra::Base
register MyExtension
get('/') { 'ok' }
end
App.prototypein 'Logger#initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)
caller: rack-3.2.6/lib/rack/builder.rb:164
| @use << proc { |app| middleware.new(app, *args, &block) }Repro 2: Sinatra::Namespace
class App < Sinatra::Base
register Sinatra::Namespace
namespace '/admin' do
use Logger, level: :debug
end
get('/') { 'ok' }
endSame ArgumentError.
Cause
Both forward through *args with no **kwargs, so keywords arrive as a trailing positional Hash.
sinatra-contrib/lib/sinatra/extension.rb:71-88 records and replays every DSL call:
def record(method, *args, &block)
recorded_methods << [method, args, block]
end
def replay(object)
recorded_methods.each { |m, a, b| object.send(m, *a, &b) }
end
def method_missing(method, *args, &block)
return super unless Sinatra::Base.respond_to? method
record(method, *args, &block)
DontCall.new(method)
endsinatra-contrib/lib/sinatra/namespace.rb:355-357:
def method_missing(method, *args, &block)
base.send(method, *args, &block)
endextension.rb affects every recorded method, not just use.
Notes
- Middleware taking an optional positional Hash (
def initialize(app, options = {})) is unaffected, which is why this has gone unnoticed. - A fix is not a breaking change. Adding
**kwargsto these forwarding paths leaves optional-Hash middleware receiving exactly what it receives today; it only stops middleware with real keyword parameters from raising. - Related but separate: #2184 removes
ruby2_keywordsfromSinatra::Base.use,Sinatra::Base.newandSinatra::Delegator. These two sites never usedruby2_keywordsand are untouched by that work.
Tested on Sinatra 4.2.1, Ruby 4.0.6.
Source: sinatra/sinatra