Custom `body_parser` from Hanami 2.1 guides

Hi Hanami friends!

I have a Hanami app I’ve been working on, but I’m having trouble setting up a custom body_parser as described in Hanami Guides.

I have defined a trivial example parser in lib/simple_json_parser.rb of my app, similar to the guide’s example.

# in lib/simple_json_parser.rb

require 'json'

class SimpleJSONParser
  def mime_types
    ['application/json']
  end

  def parse(body)
    JSON.parse(body)
  end
end

Next, I hooked up the parser into my app via config/app.rb, as described in the example.

require 'hanami'
require 'simple_json_parser'

module FancyApp
  class App < Hanami::App
    config.middleware.use :body_parser, SimpleJSONParser.new
  end
end

Unfortunately this doesn’t seem to work. I get the following stacktrace upon receiving a request.

NoMethodError: undefined method `empty?' for an instance of SimpleJSONParser
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/body_parser/class_interface.rb:56:in `build_parsers'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/body_parser/class_interface.rb:32:in `new'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:158:in `block in use'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `block in to_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `each'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `inject'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `to_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:30:in `block in initialize'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:21:in `each'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:21:in `initialize'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/routing/middleware/stack.rb:128:in `new'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/routing/middleware/stack.rb:128:in `to_rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/router.rb:82:in `to_rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice.rb:741:in `rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice.rb:758:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/static.rb:161:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/tempfile_reaper.rb:15:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/lint.rb:50:in `_call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/lint.rb:38:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/show_exceptions.rb:23:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/common_logger.rb:38:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/content_length.rb:17:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/configuration.rb:272:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:100:in `block in handle_request'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:378:in `with_force_shutdown'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:99:in `handle_request'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/server.rb:464:in `process_client'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/server.rb:245:in `block in run'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:155:in `block in spawn_thread'
::1 - - [23/Mar/2024:12:39:01 -0400] "POST /api/discord-interactions HTTP/1.1" 500 3781 0.0266

I poked around Hanami’s router repo finding some of the files referenced in the stacktrack ([1] and [2]), but nothing directly leaps out at me.

I also had a look at router’s README which shows similar instructions (the difference is that the class inherits from Hanami::Middleware::BodyParser::Parser) for custom parsers, but no joy from that either.

I would deeply appreciate any direction in fixing this problem. Have I stumbled across a bug?

Thank you in advance for any help! Have a great weekend!

I believe you should not pass an instance of the parser, but the class itself.

config.middleware.use :body_parser, SimpleJSONParser

A hint is that the files you linked, do actually build the parser, initialize it and all:

I tried this as well, but I get a similar stacktrace.

module FancyApp
  class App < Hanami::App
    config.middleware.use :body_parser, SimpleJSONParser
  end
end

results:

NoMethodError: undefined method `empty?' for class SimpleJSONParser
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/body_parser/class_interface.rb:56:in `build_parsers'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/body_parser/class_interface.rb:32:in `new'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:158:in `block in use'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `block in to_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `each'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `inject'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/builder.rb:235:in `to_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:30:in `block in initialize'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:21:in `each'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-router-2.1.0/lib/hanami/middleware/app.rb:21:in `initialize'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/routing/middleware/stack.rb:128:in `new'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/routing/middleware/stack.rb:128:in `to_rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice/router.rb:82:in `to_rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice.rb:741:in `rack_app'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/hanami-2.1.0/lib/hanami/slice.rb:758:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/static.rb:161:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/tempfile_reaper.rb:15:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/lint.rb:50:in `_call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/lint.rb:38:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/show_exceptions.rb:23:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/common_logger.rb:38:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/rack-2.2.8.1/lib/rack/content_length.rb:17:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/configuration.rb:272:in `call'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:100:in `block in handle_request'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:378:in `with_force_shutdown'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/request.rb:99:in `handle_request'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/server.rb:464:in `process_client'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/server.rb:245:in `block in run'
        /home/user/.asdf/installs/ruby/3.3.0/lib/ruby/gems/3.3.0/gems/puma-6.4.2/lib/puma/thread_pool.rb:155:in `block in spawn_thread'
::1 - - [25/Mar/2024:06:32:25 -0400] "POST /api/discord-interactions HTTP/1.1" 500 3772 0.0271

Interesting. Looking deeper into the source code, it seems that it does expect something does react to .empty?
If you wrap the class in an array, it does not crash anymore.
I’ve tried the example with xml GitHub - hanami/router: Ruby/Rack HTTP router from here, and it produces the same error.
I think simply passing the class should be the expected interface for this. So this does indeed look like a bug to me.

@timriley sorry for tagging you again, twice in one day :slight_smile: if this is a bug, would you be interested in some help in handling this?