#2185·sinatra

`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

ruby
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.prototype
in '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

ruby
class App < Sinatra::Base
  register Sinatra::Namespace
  namespace '/admin' do
    use Logger, level: :debug
  end
  get('/') { 'ok' }
end

Same 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:

ruby
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)
end

sinatra-contrib/lib/sinatra/namespace.rb:355-357:

ruby
def method_missing(method, *args, &block)
  base.send(method, *args, &block)
end

extension.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 **kwargs to 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_keywords from Sinatra::Base.use, Sinatra::Base.new and Sinatra::Delegator. These two sites never used ruby2_keywords and are untouched by that work.

Tested on Sinatra 4.2.1, Ruby 4.0.6.