Help us test the Hanami 3.0 release candidate

Today we’re sharing the release candidate for Hanami 3.0, feature-complete and ready for testing. We’d love your help to give it a good workout before our final release—which, all going well, will be in just a couple of weeks!

What’s coming in 3.0

There’s a lot I’m excited for in 3.0, but I’ll save the full announcement for when it ships. For now, here’s where we’d love your help with testing.

Built-in i18n

Internationalization is now a built-in feature. Bundle the i18n gem and Hanami loads your translations and makes translation and localization helpers available across your views and actions. See Previewing i18n integration in Hanami 3.0 for details.

First-class mailers

We’ve rebuilt Hanami Mailer and integrated it into the framework, with generators, templates, and SMTP delivery. See Previewing mailers in Hanami 3.0 for more.

Minitest support

We’ve created a new Hanami Minitest gem to give you a fully integrated Minitest setup. Activate it with hanami new --test=minitest. See all the details in Feedback on Hanami Minitest.

Memoized components

Container components are now memoized by default: each is resolved once and reused, instead of being built fresh each time. This is one of several performance improvements landing in 3.0, and a good one to test. If you have a component that cannot safely be memoized, you can opt it out.

Improved logging

Web and SQL logs are colorized by default in development. SQL queries are now logged as consistently structured entries, syntax highlighted when the rouge gem is bundled. SQL logs are now emitted at the :debug level for cleaner production logs, which you can adjust with config.db.log_level.

Hanami ensures that structured and tagged logging is available even if you configure your own alternative logger. Emit structured logs via keyword args on the main log methods, and tagged logs via tagged blocks. You can also now control the overall app log level via a HANAMI_LOG_LEVEL env var.

Smoother asset watching

hanami assets watch now keeps up with more of your changes: it picks up newly added or removed entry points, and reacts to edits to static assets like images and fonts. Previously, both meant restarting the watcher.

Other changes

Here are a few more changes worth flagging, especially if you’re upgrading an existing app:

  • The actions gem is renamed from hanami-controller to hanami-action.
  • The hanami-validations gem is retired. Hanami Action now checks for dry-validation directly.
  • Views default to undecorated exposures.
  • Request body parsing has moved into Hanami Action, driven by your formats config.
  • Hanami now requires Ruby 3.3 or newer.

Upgrading an existing app

Already on 2.3? Our 3.0 upgrade notes walk you through every step, from the required changes through to the optional new features above.

Try it out

You can step into the 3.0 future with just a few commands:

$ gem install hanami --pre
$ hanami new my_app
$ cd my_app
$ bin/hanami dev
$ open http://localhost:2300

Tell us how you go!

This is the moment your feedback makes the biggest difference. If you hit a snag or just want to share how it went, come talk to us on our forum or chat. We’d love to hear from you!

Thank you to everyone who’s helped on the road to 3.0, and thanks in advance to everyone about to help us test it. :cherry_blossom:


Originally posted on the Hanakai blog.

3 Likes

I’ve updated our app to 3.0.0-rc1 without too many issues, but there are two things a ran into.

1. SQL syntax highlighting in light themes

The default colour theme (gruvbox) of the rouge gem doesn’t play nice with my light theme. I had to set the HANAMI_SQL_THEME env var to make it readable. A few themes that work on a light background are gruvbox.light, github, pastie, and base16.

It’s all in the YARD docs on Hanami::logger::SQLFormatter, but perhaps that should be mentioned in the upgrade docs as well?

2. Encoding issues with file uploads

This is a bigger one. After upgrading, image uploads stopped working. This is the error:

web    | 
web    | Rack::QueryParser::InvalidParameterError: invalid %-encoding ("_csrf_token"
web    | 
web    | 065c572e97820e346d2dd74d1d7ad8f4c9393d5fda4ebe0df3a51a5aaa3b54d5
web    | ------geckoformboundary4ddee5bcf171547ad34cff64654aea37
web    | Content-Disposition: form-data; name="container_id"
web    | 
web    | 3
web    | ------geckoformboundary4ddee5bcf171547ad34cff64654aea37
web    | Content-Disposition: form-data; name="blueprint_handle"
web    | 
web    | image
web    | ------geckoformboundary4ddee5bcf171547ad34cff64654aea37
web    | Content-Disposition: form-data; name="block[alt]"
web    | 
web    | Moi
web    | ------geckoformboundary4ddee5bcf171547ad34cff64654aea37
web    | Content-Disposition: form-data; name="block[image_file]"; filename="dendro-wout.png"
web    | Content-Type: image/png
web    | 
web    | �PNG
web    | 
IHDRS,A���iCCPICC profile(�}�=H�@�_[�"U�v(␡:Y�p�*�B�Zu0��
                                                         �4$).��k��������� �����"%�/)������=���F��f��j��N)
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/4.0.0/uri/common.rb:464:in 'URI._decode_uri_component'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/4.0.0/uri/common.rb:431:in 'URI.decode_www_form_component'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:254:in 'Rack::QueryParser#unescape'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:245:in 'block (2 levels) in Rack::QueryParser#each_query_pair'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:245:in 'Array#map!'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:245:in 'block in Rack::QueryParser#each_query_pair'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:243:in 'Array#each'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:243:in 'Rack::QueryParser#each_query_pair'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/query_parser.rb:112:in 'Rack::QueryParser#parse_nested_query'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/utils.rb:107:in 'Rack::Utils.parse_nested_query'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-router-3.0.0.rc1/lib/hanami/router.rb:1044:in 'Hanami::Router#_params'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-router-3.0.0.rc1/lib/hanami/router.rb:117:in 'Hanami::Router#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/method_override.rb:28:in 'Rack::MethodOverride#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/static.rb:165:in 'Rack::Static#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/static.rb:165:in 'Rack::Static#call'
web    |   /home/wout/Code/ruby/fluck_cloud/config/app.rb:42:in 'call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/bun_bun_bundle-0.14.0/lib/bun_bun_bundle/dev_cache_middleware.rb:32:in 'BunBunBundle::DevCacheMiddleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-session-2.1.2/lib/rack/session/abstract/id.rb:274:in 'Rack::Session::Abstract::Persisted#context'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-session-2.1.2/lib/rack/session/abstract/id.rb:268:in 'Rack::Session::Abstract::Persisted#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-3.2.6/lib/rack/method_override.rb:28:in 'Rack::MethodOverride#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/better_errors-2.10.1/lib/better_errors/middleware.rb:87:in 'BetterErrors::Middleware#protected_app_call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/better_errors-2.10.1/lib/better_errors/middleware.rb:82:in 'BetterErrors::Middleware#better_errors_call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/better_errors-2.10.1/lib/better_errors/middleware.rb:60:in 'BetterErrors::Middleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-webconsole-3.0.0.rc1/lib/hanami/webconsole/middleware.rb:56:in 'Hanami::Webconsole::Middleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-3.0.0.rc1/lib/hanami/middleware/render_errors.rb:57:in 'Hanami::Middleware::RenderErrors#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/dry-monitor-1.0.1/lib/dry/monitor/rack/middleware.rb:36:in 'block in Dry::Monitor::Rack::Middleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/dry-monitor-1.0.1/lib/dry/monitor/clock.rb:15:in 'Dry::Monitor::Clock#measure'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/dry-monitor-1.0.1/lib/dry/monitor/rack/middleware.rb:36:in 'Dry::Monitor::Rack::Middleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-router-3.0.0.rc1/lib/hanami/middleware/app.rb:40:in 'Hanami::Middleware::App#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/hanami-3.0.0.rc1/lib/hanami/slice.rb:808:in 'Hanami::Slice::ClassMethods#call'
web    |   /home/wout/Code/ruby/fluck_cloud/lib/fluck_cloud/host_dispatch.rb:15:in 'FluckCloud::HostDispatch#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/roda-3.105.0/lib/roda/plugins/middleware.rb:162:in 'Roda::RodaPlugins::Middleware::Forwarder#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/rack-attack-6.8.0/lib/rack/attack.rb:129:in 'Rack::Attack#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/configuration.rb:305:in 'Puma::Configuration::ConfigMiddleware#call'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/response.rb:79:in 'block in Puma::Response#handle_request'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/thread_pool.rb:434:in 'Puma::ThreadPool#with_force_shutdown'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/response.rb:78:in 'Puma::Response#handle_request'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/server.rb:508:in 'Puma::Server#process_client'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/server.rb:263:in 'block in Puma::Server#run'
web    |   /home/wout/.rbenv/versions/4.0.3/lib/ruby/gems/4.0.0/gems/puma-8.0.2/lib/puma/thread_pool.rb:246:in 'block in Puma::ThreadPool#spawn_thread'
web    | [fluck_cloud] [INFO] [2026-06-20 10:15:42 +0200] POST 500 102ms 127.0.0.1 /the-one/blocks 810452

This is where it crashes:

It reads the entire body, which contains non-ASCII characters, and blows up. I’ve monkey patched it with the following:

module Hanami
  class Router
    private

    def _params(env, params)
      params ||= {}
      env[PARAMS] ||= {}

      if !env.key?(ROUTER_PARSED_BODY) &&
         env["CONTENT_TYPE"]&.start_with?("application/x-www-form-urlencoded") &&
         (input = env[::Rack::RACK_INPUT]) and input.rewind
        env[PARAMS].merge!(::Rack::Utils.parse_nested_query(input.read))
        input.rewind
      end

      env[PARAMS].merge!(::Rack::Utils.parse_nested_query(env[::Rack::QUERY_STRING]))
      env[PARAMS].merge!(params)
      env[PARAMS] = Params.deep_symbolize(env[PARAMS])
      env
    end
  end
end

So now it will never try to read multipart/form-data, only application/x-www-form-urlencoded.

With this fix, our test suite is back to green, files upload correctly, and everything seems to work as expected. I’m not sure if this would break anything else.
I did drop this patch into the hanami-router repo and the test suite still passes, so that’s promising at least :grinning_face_with_smiling_eyes:. But perhaps it could be cleaned up with a private predicate method for better readability?

Thank you so much for this, @wout! :heart_eyes:

Regarding the rouge color theme, I sorted that yesterday — Use Rouge theme that works on both light and dark by timriley · Pull Request #1602 · hanami/hanami · GitHub :slight_smile: Does "base16.solarized" seem better to you as a default?

That’s a great pickup with the router. :person_bowing: I’m going to sort this one out as a matter of priority. Sometime in the next couple days, hopefully. I’ll reply here again when it’s ready and hopefully you can give everything another test to confirm.

@wout I think we already have a fix for this in Fix nested form body parsing happening for all content types by cllns · Pull Request #305 · hanami/hanami-router · GitHub, if you wanted to give that one a try. I’ll be looking at making sure this is in good shape and then will merge it in the coming days.

1 Like

You’re welcome :slightly_smiling_face:.

Does "base16.solarized" seem better to you as a default?

Yes, that works. I went with pastie because it has a bit more contrast, which reads easier for me. But they’re both Level AA contrast for a11y, so I think it’s okay.

I think we already have a fix for this in Fix nested form body parsing happening for all content types by cllns · Pull Request #305 · hanami/hanami-router · GitHub, if you wanted to give that one a try. I’ll be looking at making sure this is in good shape and then will merge it in the coming days.

Confirmed! This fixes the issue. :tada:

I’ve just merged Fix nested form body parsing happening for all content types by cllns · Pull Request #305 · hanami/hanami-router · GitHub now (after making a few tweaks for readability), thanks for testing it, @wout! I also added a CHANGELOG note so this fix is made clear to others.

I also like pastie even better than base16.solarized for the syntax highlighting — I’ll make that the new default!

Done in Use “pastie” SQL log theme; rename theme env var by timriley · Pull Request #1607 · hanami/hanami.

I also updated the env var name to HANAMI_LOG_SYNTAX_THEME, making it possible for us to reuse this for other kinds of log highlighting in the future.

1 Like

I’ve just merged Fix nested form body parsing happening for all content types by cllns

Awesome, I’ll pull that in for now. Thanks for the quick fix!

I feel very dumb, but after having patched my Gemfile to

gem "hanami", "~> 3.0"
gem "hanami-action", "~> 3.0"
gem "hanami-assets", "~> 3.0"
gem "hanami-view", "~> 3.0"
gem "hanami-mailer", "~> 3.0"
gem "hanami-router", "~> 3.0"
gem "dry-validation"

When I run bundle update, I get:

Fetching gem metadata from https://rubygems.org/..........
Could not find gem 'hanami (~> 3.0)' in rubygems repository https://rubygems.org/ or installed locally.

The source contains the following gems matching 'hanami':
[all hanami versions here, cutted for the sake of clarity]
  * hanami-2.3.0
  * hanami-2.3.1
  * hanami-2.3.2
  * hanami-3.0.0.rc1

If I use 3.0.0.rc1 everywhere in the Gemfile, it works.

FYI:

$ ruby --version
ruby 3.4.5 (2025-07-16 revision 20cda200d3) +PRISM [x86_64-linux]

$ bundle --version
Bundler version 2.6.4

Did I get something wrong ? Thanks !

Same kind of error for npm after adding "hanami-assets": "^3.0.0"

npm update
npm error code ETARGET
npm error notarget No matching version found for hanami-assets@^3.0.0.
npm error notarget In most cases you or one of your dependencies are requesting
npm error notarget a package version that doesn't exist.

Thanks for the help

Also, when I try to put config.decorate_exposures = true in config/app.rb, I get this at puma startup:

.rbenv/versions/3.4.5/lib/ruby/gems/3.4.0/gems/hanami-3.0.0.rc1/lib/hanami/config.rb:556:
in 'Hanami::Config#method_missing': 
undefined method 'decorate_exposures=' for an instance of Hanami::Config (NoMethodError)
17:11:20 assets.1     | 	from config/app.rb:20:in '<class:App>'
17:11:20 assets.1     | 	from config/app.rb:6:in '<module:Deck>'
17:11:20 assets.1     | 	from config/app.rb:5:in '<top (required)>'
17:11:20 assets.1     | 	from .rbenv/versions/3.4.5/lib/ruby/3.4.0/bundled_gems.rb:82:in 'Kernel.require'

Hanami 3.0.0 isn’t out yet is all, 3.0.0.rc1 is the version we are testing, so nope you did nothing wrong.

See the released gem versions on their GitHub releases tab

1 Like

This one should be config.views.decorate_exposures = true. (Or config.decorate_exposures if you put it directly in a view class, as opposed to the app). Let us know how that goes!

And yep, like @zradford mentioned, we only have 3.0.0.rc1 out now (3.0.0-rc.1 for the hanami-assets npm package). All this testing of rc1 is really helpful to make sure our proper 3.0.0 release (in a week!) is as solid as possible. Thank you for your help here, @inouire! Please keep the feedback/questions coming :slight_smile: