Rails and hanami-view don't get along

After more help from @cllns in Trouble testing the result of an operation - #2 by cllns I am now looking at a gem dependency incompatibility between Rails and hanami-view and Hanami-controller. Here is a minimal repro:

rm -rf hanami_sample3

rails new hanami_sample3
cd hanami_sample3

rails generate rspec:install
bundle exec rails g scaffold team name:string

echo '
gem "hanami", github: "hanami/hanami", branch: "fix-error-in-operation-extension-for-no-db-apps"
gem "hanami-controller", "~> 2.2"
gem "hanami-view", "~> 2.2"
gem "dry-operation"
' >> Gemfile

bundle



cd tmp
bundle exec hanami new SportsballHanami
cd sportsball_hanami
bundle exec hanami generate slice HanamiPredictor
mv slices ../..

cd ../..


echo '# typed: ignore

require "dry/monads"
require "dry/operation"
require "hanami/action"
require "hanami/view"

module SportsballHanami
  class App < Hanami::App
    prepare_container do |container|
      container.autoloader.ignore("app")
    end
  end

  class Action < Hanami::Action
    include Dry::Monads[:result]
  end
end

Hanami.boot

module SportsballHanami
  class View < Hanami::View
  end

  class Operation < Dry::Operation
  end
end

HanamiPredictor::Slice.boot

p Hanami.app.slices[:hanami_predictor].keys

' > config/initializers/load_hanami.rb


bin/rails runner 'puts "Hello, world!"'
bin/rails db:migrate
bin/rails s

Navigate to http://localhost:3000/teams (edit: as @alassek points out, this page still works… but navigate to the new team page to see the error) and see the following error:

undefined method `safe_concat' for "<form action=\"/teams\" accept-charset=\"UTF-8\" method=\"post\"><input type=\"hidden\" name=\"authenticity_token\" value=\"rO7x6c8gR4aowONfekEg6ZbzLCTCF_iBmBKoeDr_hF0p958zbP8jzxw-x_KJRtOfDro3GlSnHoswMjcY8R4w9Q\" autocomplete=\"off\" />\n\n  <div>\n    <label style=\"display: block\" for=\"team_name\">Name</label>\n    <input type=\"text\" name=\"team[name]\" id=\"team_name\" />\n  </div>\n\n  <div>\n    <input type=\"submit\" name=\"commit\" value=\"Create Team\" data-disable-with=\"Create Team\" />\n  </div>\n":String

When I make the following changes, the app works again (I am not sure this is minimal):

  • comment out hanami-controller and hanami-view in the Gemfile
  • remove the require of hanami/action and hanami/view in the initializer
  • remove the subclassing of Hanami::Action and Hanami::View

My current theory is that it is the version of rack or rackup which are held back a major version by hanami-controller depending on rack (~> 2.0). If that were the only explanation though, I am not sure why I have to remove the view pieces above as well…

Following your steps, I was unable to reproduce.

Started GET "/teams" for 127.0.0.1 at 2025-02-16 18:07:40 -0600
  ActiveRecord::SchemaMigration Load (0.0ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC /*application='Viewtest'*/
Processing by TeamsController#index as HTML
  Rendering layout layouts/application.html.erb
  Rendering teams/index.html.erb within layouts/application
  Team Load (0.4ms)  SELECT "teams".* FROM "teams" /*action='index',application='Viewtest',controller='teams'*/
  ↳ app/views/teams/index.html.erb:8
  Rendered teams/index.html.erb within layouts/application (Duration: 2.3ms | GC: 0.0ms)
  Rendered layout layouts/application.html.erb (Duration: 6.7ms | GC: 0.0ms)
Completed 200 OK in 58ms (Views: 8.8ms | ActiveRecord: 0.5ms (1 query, 0 cached) | GC: 0.0ms)

This was a fresh install of:

  • Ruby 3.4.2
  • Rails 8.0.1
  • Hanami 2.2.1
GIT
  remote: https://github.com/hanami/hanami.git
  revision: 27a002cd91c24b67ab67ab7f11698314b90dd446
  branch: fix-error-in-operation-extension-for-no-db-apps
  specs:
    hanami (2.2.1)
      bundler (>= 1.16, < 3)
      dry-configurable (~> 1.0, >= 1.2.0, < 2)
      dry-core (~> 1.0, < 2)
      dry-inflector (~> 1.0, >= 1.1.0, < 2)
      dry-logger (~> 1.0, < 2)
      dry-monitor (~> 1.0, >= 1.0.1, < 2)
      dry-system (~> 1.1)
      hanami-cli (~> 2.2.1)
      hanami-utils (~> 2.2)
      json (>= 2.7.2)
      zeitwerk (~> 2.6)

GEM
  remote: https://rubygems.org/
  specs:
    actioncable (8.0.1)
      actionpack (= 8.0.1)
      activesupport (= 8.0.1)
      nio4r (~> 2.0)
      websocket-driver (>= 0.6.1)
      zeitwerk (~> 2.6)
    actionmailbox (8.0.1)
      actionpack (= 8.0.1)
      activejob (= 8.0.1)
      activerecord (= 8.0.1)
      activestorage (= 8.0.1)
      activesupport (= 8.0.1)
      mail (>= 2.8.0)
    actionmailer (8.0.1)
      actionpack (= 8.0.1)
      actionview (= 8.0.1)
      activejob (= 8.0.1)
      activesupport (= 8.0.1)
      mail (>= 2.8.0)
      rails-dom-testing (~> 2.2)
    actionpack (8.0.1)
      actionview (= 8.0.1)
      activesupport (= 8.0.1)
      nokogiri (>= 1.8.5)
      rack (>= 2.2.4)
      rack-session (>= 1.0.1)
      rack-test (>= 0.6.3)
      rails-dom-testing (~> 2.2)
      rails-html-sanitizer (~> 1.6)
      useragent (~> 0.16)
    actiontext (8.0.1)
      actionpack (= 8.0.1)
      activerecord (= 8.0.1)
      activestorage (= 8.0.1)
      activesupport (= 8.0.1)
      globalid (>= 0.6.0)
      nokogiri (>= 1.8.5)
    actionview (8.0.1)
      activesupport (= 8.0.1)
      builder (~> 3.1)
      erubi (~> 1.11)
      rails-dom-testing (~> 2.2)
      rails-html-sanitizer (~> 1.6)
    activejob (8.0.1)
      activesupport (= 8.0.1)
      globalid (>= 0.3.6)
    activemodel (8.0.1)
      activesupport (= 8.0.1)
    activerecord (8.0.1)
      activemodel (= 8.0.1)
      activesupport (= 8.0.1)
      timeout (>= 0.4.0)
    activestorage (8.0.1)
      actionpack (= 8.0.1)
      activejob (= 8.0.1)
      activerecord (= 8.0.1)
      activesupport (= 8.0.1)
      marcel (~> 1.0)
    activesupport (8.0.1)
      base64
      benchmark (>= 0.3)
      bigdecimal
      concurrent-ruby (~> 1.0, >= 1.3.1)
      connection_pool (>= 2.2.5)
      drb
      i18n (>= 1.6, < 2)
      logger (>= 1.4.2)
      minitest (>= 5.1)
      securerandom (>= 0.3)
      tzinfo (~> 2.0, >= 2.0.5)
      uri (>= 0.13.1)
    addressable (2.8.7)
      public_suffix (>= 2.0.2, < 7.0)
    ast (2.4.2)
    base64 (0.2.0)
    bcrypt_pbkdf (1.1.1)
    benchmark (0.4.0)
    bigdecimal (3.1.9)
    bindex (0.8.1)
    bootsnap (1.18.4)
      msgpack (~> 1.2)
    brakeman (7.0.0)
      racc
    builder (3.3.0)
    capybara (3.40.0)
      addressable
      matrix
      mini_mime (>= 0.1.3)
      nokogiri (~> 1.11)
      rack (>= 1.6.0)
      rack-test (>= 0.6.3)
      regexp_parser (>= 1.5, < 3.0)
      xpath (~> 3.2)
    concurrent-ruby (1.3.5)
    connection_pool (2.5.0)
    crass (1.0.6)
    date (3.4.1)
    debug (1.10.0)
      irb (~> 1.10)
      reline (>= 0.3.8)
    diff-lcs (1.6.0)
    dotenv (3.1.7)
    drb (2.2.1)
    dry-auto_inject (1.1.0)
      dry-core (~> 1.1)
      zeitwerk (~> 2.6)
    dry-cli (1.2.0)
    dry-configurable (1.3.0)
      dry-core (~> 1.1)
      zeitwerk (~> 2.6)
    dry-core (1.1.0)
      concurrent-ruby (~> 1.0)
      logger
      zeitwerk (~> 2.6)
    dry-events (1.1.0)
      concurrent-ruby (~> 1.0)
      dry-core (~> 1.1)
    dry-files (1.1.0)
    dry-inflector (1.2.0)
    dry-logger (1.0.4)
    dry-monads (1.7.1)
      concurrent-ruby (~> 1.0)
      dry-core (~> 1.1)
      zeitwerk (~> 2.6)
    dry-monitor (1.0.1)
      dry-configurable (~> 1.0, < 2)
      dry-core (~> 1.0, < 2)
      dry-events (~> 1.0, < 2)
    dry-operation (1.0.0)
      dry-monads (~> 1.6)
      zeitwerk (~> 2.6)
    dry-system (1.2.2)
      dry-auto_inject (~> 1.1)
      dry-configurable (~> 1.3)
      dry-core (~> 1.1)
      dry-inflector (~> 1.1)
    dry-transformer (1.0.1)
      zeitwerk (~> 2.6)
    ed25519 (1.3.0)
    erubi (1.13.1)
    et-orbi (1.2.11)
      tzinfo
    fugit (1.11.1)
      et-orbi (~> 1, >= 1.2.11)
      raabro (~> 1.4)
    globalid (1.2.1)
      activesupport (>= 6.1)
    hanami-cli (2.2.1)
      bundler (~> 2.1)
      dry-cli (~> 1.0, >= 1.1.0)
      dry-files (~> 1.0, >= 1.0.2, < 2)
      dry-inflector (~> 1.0, < 2)
      rake (~> 13.0)
      zeitwerk (~> 2.6)
    hanami-controller (2.2.0)
      dry-configurable (~> 1.0, < 2)
      dry-core (~> 1.0)
      hanami-utils (~> 2.2)
      rack (~> 2.0)
      zeitwerk (~> 2.6)
    hanami-utils (2.2.0)
      concurrent-ruby (~> 1.0)
      dry-core (~> 1.0, < 2)
      dry-transformer (~> 1.0, < 2)
    hanami-view (2.2.0)
      dry-configurable (~> 1.0)
      dry-core (~> 1.0)
      dry-inflector (~> 1.0, < 2)
      temple (~> 0.10.0, >= 0.10.2)
      tilt (~> 2.3)
      zeitwerk (~> 2.6)
    i18n (1.14.7)
      concurrent-ruby (~> 1.0)
    importmap-rails (2.1.0)
      actionpack (>= 6.0.0)
      activesupport (>= 6.0.0)
      railties (>= 6.0.0)
    io-console (0.8.0)
    irb (1.15.1)
      pp (>= 0.6.0)
      rdoc (>= 4.0.0)
      reline (>= 0.4.2)
    jbuilder (2.13.0)
      actionview (>= 5.0.0)
      activesupport (>= 5.0.0)
    json (2.10.1)
    kamal (2.5.2)
      activesupport (>= 7.0)
      base64 (~> 0.2)
      bcrypt_pbkdf (~> 1.0)
      concurrent-ruby (~> 1.2)
      dotenv (~> 3.1)
      ed25519 (~> 1.2)
      net-ssh (~> 7.3)
      sshkit (>= 1.23.0, < 2.0)
      thor (~> 1.3)
      zeitwerk (>= 2.6.18, < 3.0)
    language_server-protocol (3.17.0.4)
    lint_roller (1.1.0)
    logger (1.6.6)
    loofah (2.24.0)
      crass (~> 1.0.2)
      nokogiri (>= 1.12.0)
    mail (2.8.1)
      mini_mime (>= 0.1.1)
      net-imap
      net-pop
      net-smtp
    marcel (1.0.4)
    matrix (0.4.2)
    mini_mime (1.1.5)
    minitest (5.25.4)
    msgpack (1.8.0)
    net-imap (0.5.6)
      date
      net-protocol
    net-pop (0.1.2)
      net-protocol
    net-protocol (0.2.2)
      timeout
    net-scp (4.1.0)
      net-ssh (>= 2.6.5, < 8.0.0)
    net-sftp (4.0.0)
      net-ssh (>= 5.0.0, < 8.0.0)
    net-smtp (0.5.1)
      net-protocol
    net-ssh (7.3.0)
    nio4r (2.7.4)
    nokogiri (1.18.2-aarch64-linux-gnu)
      racc (~> 1.4)
    nokogiri (1.18.2-aarch64-linux-musl)
      racc (~> 1.4)
    nokogiri (1.18.2-arm-linux-gnu)
      racc (~> 1.4)
    nokogiri (1.18.2-arm-linux-musl)
      racc (~> 1.4)
    nokogiri (1.18.2-arm64-darwin)
      racc (~> 1.4)
    nokogiri (1.18.2-x86_64-linux-gnu)
      racc (~> 1.4)
    nokogiri (1.18.2-x86_64-linux-musl)
      racc (~> 1.4)
    ostruct (0.6.1)
    parallel (1.26.3)
    parser (3.3.7.1)
      ast (~> 2.4.1)
      racc
    pp (0.6.2)
      prettyprint
    prettyprint (0.2.0)
    propshaft (1.1.0)
      actionpack (>= 7.0.0)
      activesupport (>= 7.0.0)
      rack
      railties (>= 7.0.0)
    psych (5.2.3)
      date
      stringio
    public_suffix (6.0.1)
    puma (6.6.0)
      nio4r (~> 2.0)
    raabro (1.4.0)
    racc (1.8.1)
    rack (2.2.11)
    rack-session (1.0.2)
      rack (< 3)
    rack-test (2.2.0)
      rack (>= 1.3)
    rackup (1.0.1)
      rack (< 3)
      webrick
    rails (8.0.1)
      actioncable (= 8.0.1)
      actionmailbox (= 8.0.1)
      actionmailer (= 8.0.1)
      actionpack (= 8.0.1)
      actiontext (= 8.0.1)
      actionview (= 8.0.1)
      activejob (= 8.0.1)
      activemodel (= 8.0.1)
      activerecord (= 8.0.1)
      activestorage (= 8.0.1)
      activesupport (= 8.0.1)
      bundler (>= 1.15.0)
      railties (= 8.0.1)
    rails-dom-testing (2.2.0)
      activesupport (>= 5.0.0)
      minitest
      nokogiri (>= 1.6)
    rails-html-sanitizer (1.6.2)
      loofah (~> 2.21)
      nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
    railties (8.0.1)
      actionpack (= 8.0.1)
      activesupport (= 8.0.1)
      irb (~> 1.13)
      rackup (>= 1.0.0)
      rake (>= 12.2)
      thor (~> 1.0, >= 1.2.2)
      zeitwerk (~> 2.6)
    rainbow (3.1.1)
    rake (13.2.1)
    rdoc (6.12.0)
      psych (>= 4.0.0)
    regexp_parser (2.10.0)
    reline (0.6.0)
      io-console (~> 0.5)
    rexml (3.4.1)
    rspec-core (3.13.3)
      rspec-support (~> 3.13.0)
    rspec-expectations (3.13.3)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.13.0)
    rspec-mocks (3.13.2)
      diff-lcs (>= 1.2.0, < 2.0)
      rspec-support (~> 3.13.0)
    rspec-rails (7.1.1)
      actionpack (>= 7.0)
      activesupport (>= 7.0)
      railties (>= 7.0)
      rspec-core (~> 3.13)
      rspec-expectations (~> 3.13)
      rspec-mocks (~> 3.13)
      rspec-support (~> 3.13)
    rspec-support (3.13.2)
    rubocop (1.72.1)
      json (~> 2.3)
      language_server-protocol (~> 3.17.0.2)
      lint_roller (~> 1.1.0)
      parallel (~> 1.10)
      parser (>= 3.3.0.2)
      rainbow (>= 2.2.2, < 4.0)
      regexp_parser (>= 2.9.3, < 3.0)
      rubocop-ast (>= 1.38.0, < 2.0)
      ruby-progressbar (~> 1.7)
      unicode-display_width (>= 2.4.0, < 4.0)
    rubocop-ast (1.38.0)
      parser (>= 3.3.1.0)
    rubocop-minitest (0.37.1)
      lint_roller (~> 1.1)
      rubocop (>= 1.72.1, < 2.0)
      rubocop-ast (>= 1.38.0, < 2.0)
    rubocop-performance (1.24.0)
      lint_roller (~> 1.1)
      rubocop (>= 1.72.1, < 2.0)
      rubocop-ast (>= 1.38.0, < 2.0)
    rubocop-rails (2.30.0)
      activesupport (>= 4.2.0)
      lint_roller (~> 1.1)
      rack (>= 1.1)
      rubocop (>= 1.72.1, < 2.0)
      rubocop-ast (>= 1.38.0, < 2.0)
    rubocop-rails-omakase (1.0.0)
      rubocop
      rubocop-minitest
      rubocop-performance
      rubocop-rails
    ruby-progressbar (1.13.0)
    rubyzip (2.4.1)
    securerandom (0.4.1)
    selenium-webdriver (4.28.0)
      base64 (~> 0.2)
      logger (~> 1.4)
      rexml (~> 3.2, >= 3.2.5)
      rubyzip (>= 1.2.2, < 3.0)
      websocket (~> 1.0)
    solid_cable (3.0.7)
      actioncable (>= 7.2)
      activejob (>= 7.2)
      activerecord (>= 7.2)
      railties (>= 7.2)
    solid_cache (1.0.7)
      activejob (>= 7.2)
      activerecord (>= 7.2)
      railties (>= 7.2)
    solid_queue (1.1.3)
      activejob (>= 7.1)
      activerecord (>= 7.1)
      concurrent-ruby (>= 1.3.1)
      fugit (~> 1.11.0)
      railties (>= 7.1)
      thor (~> 1.3.1)
    sqlite3 (2.5.0-aarch64-linux-gnu)
    sqlite3 (2.5.0-aarch64-linux-musl)
    sqlite3 (2.5.0-arm-linux-gnu)
    sqlite3 (2.5.0-arm-linux-musl)
    sqlite3 (2.5.0-arm64-darwin)
    sqlite3 (2.5.0-x86_64-linux-gnu)
    sqlite3 (2.5.0-x86_64-linux-musl)
    sshkit (1.24.0)
      base64
      logger
      net-scp (>= 1.1.2)
      net-sftp (>= 2.1.2)
      net-ssh (>= 2.8.0)
      ostruct
    stimulus-rails (1.3.4)
      railties (>= 6.0.0)
    stringio (3.1.3)
    temple (0.10.3)
    thor (1.3.2)
    thruster (0.1.11)
    thruster (0.1.11-aarch64-linux)
    thruster (0.1.11-arm64-darwin)
    thruster (0.1.11-x86_64-linux)
    tilt (2.6.0)
    timeout (0.4.3)
    turbo-rails (2.0.11)
      actionpack (>= 6.0.0)
      railties (>= 6.0.0)
    tzinfo (2.0.6)
      concurrent-ruby (~> 1.0)
    unicode-display_width (3.1.4)
      unicode-emoji (~> 4.0, >= 4.0.4)
    unicode-emoji (4.0.4)
    uri (1.0.2)
    useragent (0.16.11)
    web-console (4.2.1)
      actionview (>= 6.0.0)
      activemodel (>= 6.0.0)
      bindex (>= 0.4.0)
      railties (>= 6.0.0)
    webrick (1.9.1)
    websocket (1.2.11)
    websocket-driver (0.7.7)
      base64
      websocket-extensions (>= 0.1.0)
    websocket-extensions (0.1.5)
    xpath (3.2.0)
      nokogiri (~> 1.8)
    zeitwerk (2.7.1)

PLATFORMS
  aarch64-linux
  aarch64-linux-gnu
  aarch64-linux-musl
  arm-linux-gnu
  arm-linux-musl
  arm64-darwin-24
  x86_64-linux
  x86_64-linux-gnu
  x86_64-linux-musl

DEPENDENCIES
  bootsnap
  brakeman
  capybara
  debug
  dry-operation
  hanami!
  hanami-controller (~> 2.2)
  hanami-view (~> 2.2)
  importmap-rails
  jbuilder
  kamal
  propshaft
  puma (>= 5.0)
  rails (~> 8.0.1)
  rspec-rails
  rubocop-rails-omakase
  selenium-webdriver
  solid_cable
  solid_cache
  solid_queue
  sqlite3 (>= 2.1)
  stimulus-rails
  thruster
  turbo-rails
  tzinfo-data
  web-console

BUNDLED WITH
   2.6.3

Oops, I will update the original post. Yes, I see the teams list page working, but when you click “new team” it breaks with the error (for me) as described above.

I reckon you’ve found a conflict between Hanami::View’s String extension and Rails’ own.

In Hanami::View we have the following extension, which adds a single #html_safe method to String:

# Comments and spurious code removed so you can quickly get the gist.

module Hanami
  class View
    module HTML
      class SafeString < String
        # ...
      end

      module StringExtensions
        def html_safe
          Hanami::View::HTML::SafeString.new(self)
        end
      end
    end
  end
end

class String
  # Prepend our `#html_safe` method so that it takes precedence over Active Support's. When both
  # methods are loaded, the more likely scenario is that the user will want Hanami's, since in the
  # context of a Hanami app, Active Support is more likely to be loaded incidentally, as a
  # transitive dependency of another gem.
  #
  # Having our `#html_safe` available via this module also means that a user can also choose to
  # _undefine_ this method within the module if they'd rather use Active Support's.
  prepend Hanami::View::HTML::StringExtensions
end

What I think we’ve discovered here is that my assumption as stated in code comment above is incorrect. Here you’re intermingling Hanami with Rails, and you actually want Rails’ version of the string extensions. This makes sense, because Hanami’s extension provides only a subset of the functionality provided by Rails’ extension, and Rails needs all of it. Since we only care about #html_safe, I reckon Rails’ HTML-safe strings should still work fine within Hanami apps.

Fortunately for you, we did provide for a quick way to remove our string extensions. Whack this in an initializer or similar:

Hanami::View::HTML::StringExtensions.undef_method(:html_safe)

This will ensure we don’t clobber Rails’ String extensions, and should hopefully get you past your errors.

Would you mind giving that a go, @stephan, and letting us know how you fare?

If this works for you, then I think we should change our String patching strategy in Hanami::View so we no longer “win” when there are conflicting patches.

1 Like

We should probably treat presence of ActiveSupport and presence of Rails as different cases.

This appears to clobber the method entirely. May need to gate this behavior before it gets prepended.

So there’s no String#html_safe at all after running this? If so, not ideal!

This would be another workaround to use until we can make adjustments inside Hanami::View itself:

Hanami::View::HTML::StringExtensions.class_eval do
  def html_safe
    ActiveSupport::SafeBuffer.new(self)
  end
end
1 Like

This works for me!

1 Like

I can also confirm that this works! Thank you both!

In my larger example I did find the connection to rack I suspected in the topic: The downgrading of rack and rackup due to the dependency of hanami-controller causes some system tests to fail with:

NoMethodError:
            undefined method `register' for Rackup::Handler:Module

This had been found previously (System spec failures after upgrading to 1.0.1 · Issue #19 · rack/rackup · GitHub), has a fix in puma (https://github.com/puma/puma/pull/3532) which was part of the 6.5.0 upgrade. After bundle update upgraded my puma from 6.4 to 6.6… things work!

1 Like