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?

1 Like

I went back to this since it is bugging me. I do not think this is exactly a bug, but rather a problem of incomplete/erroneous documentation.

Few things:

Both examples (GitHub - hanami/router: Ruby/Rack HTTP router and V2.1: Parameters | Hanami Guides) are broken when you try them.

I forked hanami project and added those two specs:

#spec/integration/action/format_config_spec.rb
 it "handles a custom body_parser" do
    write "lib/simple_json_parser.rb" , <<~RUBY
      require "json"
      
      class SimpleJSONParser
        def mime_types
          ['application/json']
        end
      
        def parse(body)
          JSON.parse(body).reverse
        end
      end
    RUBY

    write "config/app.rb", <<~RUBY
      require "hanami"
      require "simple_json_parser"

      module TestApp
        class App < Hanami::App
          config.logger.stream = StringIO.new

          config.middleware.use :body_parser, SimpleJSONParser
        end
      end
    RUBY

    write "config/routes.rb", <<~RUBY
      module TestApp
        class Routes < Hanami::Routes
          post "/users", to: "users.create"
        end
      end
    RUBY

    write "app/action.rb", <<~RUBY
      # auto_register: false

      module TestApp
        class Action < Hanami::Action
        end
      end
    RUBY

    write "app/actions/users/create.rb", <<~RUBY
      module TestApp
        module Actions
          module Users
            class Create < TestApp::Action
              def handle(req, res)
                res.body = req.params[:users].join("-")
              end
            end
          end
        end
      end
    RUBY

    require "hanami/boot"

    post(
      "/users",
      JSON.generate("users" => %w[jane john jade joe]),
      "CONTENT_TYPE" => "application/json"
    )

    expect(last_response).to be_successful
    expect(last_response.body).to eql(["joe", "jade", "john", "jane"])
  end

  it "handles a custom body_parser that is initialized" do
    write "lib/simple_json_parser.rb" , <<~RUBY
      require "json"
      
      class SimpleJSONParser
        def mime_types
          ['application/json']
        end
      
        def parse(body)
          JSON.parse(body).reverse
        end
      end
    RUBY

    write "config/app.rb", <<~RUBY
      require "hanami"
      require "simple_json_parser"

      module TestApp
        class App < Hanami::App
          config.logger.stream = StringIO.new

          config.middleware.use :body_parser, SimpleJSONParser.new
        end
      end
    RUBY

    write "config/routes.rb", <<~RUBY
      module TestApp
        class Routes < Hanami::Routes
          post "/users", to: "users.create"
        end
      end
    RUBY

    write "app/action.rb", <<~RUBY
      # auto_register: false

      module TestApp
        class Action < Hanami::Action
        end
      end
    RUBY

    write "app/actions/users/create.rb", <<~RUBY
      module TestApp
        module Actions
          module Users
            class Create < TestApp::Action
              def handle(req, res)
                res.body = req.params[:users].join("-")
              end
            end
          end
        end
      end
    RUBY

    require "hanami/boot"

    post(
      "/users",
      JSON.generate("users" => %w[jane john jade joe]),
      "CONTENT_TYPE" => "application/json"
    )

    expect(last_response).to be_successful
    expect(last_response.body).to eql(["joe", "jade", "john", "jane")
  end

Both fail with


     NoMethodError:
       undefined method `empty?' for #<SimpleJSONParser:0x0000763855b3ef80>
     # ./lib/hanami/slice/routing/middleware/stack.rb:128:in `new'
     # ./lib/hanami/slice/routing/middleware/stack.rb:128:in `to_rack_app'

Now this is already a problem, since those examples are exactly like the one that are presented in the documentation, so this is confusing.

But the initial solution I mentioned above, of putting the class/instance in an array does not always pass the test.

What does makes the tests pass is passing:
[json: SimpleJSONParser]

(for the json example)

Inspecting the code I can’t really understand why the .empty? lib/hanami/middleware/body_parser/class_interface.rb:56 always expects an array. Two examples I listed, clearly show that no array is required to handle custom parsers, for custom formats or not (one example shows XML, other one shows custom format FOO).

I think better explanation of the DSL required in this feature would clear this stuff out, and probably an update to the documentation?

@timriley I know you probably got a lot going on, but would greatly appreciate your input here.

Also moved to Custom body parser issue · Issue #1377 · hanami/hanami · GitHub some time ago