Callbacks

If we want to execute some logic before and/or after #call is executed, we can use a callback. Callbacks are useful to declutter code for common tasks like checking if a user is signed in, set a record, handle 404 responses or tidy up the response.

The corresponding DSL methods are before and after. These methods each accept a symbol that is the name of the method that we want to call, or an anonymous proc.

Methods

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. before :track_remote_ip
  8. def call(params)
  9. # ...
  10. end
  11. private
  12. def track_remote_ip
  13. @remote_ip = request.ip
  14. # ...
  15. end
  16. end
  17. end
  18. end
  19. end

With the code above, we are tracking the remote IP address for analytics purposes. Because it isn’t strictly related to our business logic, we move it to a callback.

A callback method can optionally accept an argument: params.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. before :validate_params
  8. def call(params)
  9. # ...
  10. end
  11. private
  12. def validate_params(params)
  13. # ...
  14. end
  15. end
  16. end
  17. end
  18. end

Proc

The examples above can be rewritten with anonymous procs. They are bound to the instance context of the action.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. before { @remote_ip = request.ip }
  8. def call(params)
  9. # @remote_ip is available here
  10. # ...
  11. end
  12. end
  13. end
  14. end
  15. end

A callback proc can bound an optional argument: params.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. before {|params| params.valid? }
  8. def call(params)
  9. # ...
  10. end
  11. end
  12. end
  13. end
  14. end

Don’t use callbacks for model domain logic operations like sending emails. This is an antipattern that causes a lot of problems for code maintenance, testability and accidental side effects.

Halt

Using exceptions for control flow is expensive for the Ruby VM. There is a lightweight alternative that our language supports: signals (see throw and catch).

Hanami takes advantage of this mechanism to provide faster control flow in our actions via #halt.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. def call(params)
  8. halt 401 unless authenticated?
  9. # ...
  10. end
  11. private
  12. def authenticated?
  13. # ...
  14. end
  15. end
  16. end
  17. end
  18. end

When used, this API interrupts the flow, and returns the control to the framework. Subsequent instructions will be entirely skipped.

When halt is used, the flow is interrupted and the control is passed back to the framework.

That means that halt can be used to skip #call invocation entirely if we use it in a before callback.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. before :authenticate!
  8. def call(params)
  9. # ...
  10. end
  11. private
  12. def authenticate!
  13. halt 401 if current_user.nil?
  14. end
  15. end
  16. end
  17. end
  18. end

#halt accepts an HTTP status code as the first argument. When used like this, the body of the response will be set with the corresponding message (eg. “Unauthorized” for 401).

An optional second argument can be passed to set a custom body.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. def call(params)
  8. halt 404, "These aren't the droids you're looking for"
  9. end
  10. end
  11. end
  12. end
  13. end

When #halt is used, Hanami renders a default status page with the HTTP status and the message.

Hanami default template

To customize the UI for the HTTP 404 error, you can use a custom error page.

HTTP Status

In case you want let the view to handle the error, instead of using #halt, you should use #status=.

The typical case is a failed form submission: we want to return a non-successful HTTP status (422) and let the view to render the form again and show the validation errors.

  1. # apps/web/controllers/books/create.rb
  2. module Web
  3. module Controllers
  4. module Books
  5. class Create
  6. include Web::Action
  7. params do
  8. required(:title).filled(:str?)
  9. end
  10. def call(params)
  11. if params.valid?
  12. # persist
  13. else
  14. self.status = 422
  15. end
  16. end
  17. end
  18. end
  19. end
  20. end
  1. # apps/web/views/books/create.rb
  2. module Web
  3. module Views
  4. module Books
  5. class Create
  6. include Web::View
  7. template 'books/new'
  8. end
  9. end
  10. end
  11. end
  1. # apps/web/templates/books/new.html.erb
  2. <% unless params.valid? %>
  3. <ul>
  4. <% params.error_messages.each do |error| %>
  5. <li><%= error %></li>
  6. <% end %>
  7. </ul>
  8. <% end %>
  9. <!-- form goes here -->

Redirect

A special case of control flow management is relative to HTTP redirect. If we want to reroute a request to another resource we can use redirect_to.

When redirect_to is invoked, control flow is stopped and subsequent code in the action is not executed.

It accepts a string that represents an URI, and an optional :status argument. By default the status is set to 302.

  1. # apps/web/controllers/dashboard/index.rb
  2. module Web
  3. module Controllers
  4. module Dashboard
  5. class Index
  6. include Web::Action
  7. def call(params)
  8. redirect_to routes.root_path
  9. foo('bar') # This line will never be executed
  10. end
  11. end
  12. end
  13. end
  14. end

Back

Sometimes you’ll want to redirect_to back in your browser’s history so the easy way to do it is the following way:

  1. redirect_to request.get_header("Referer") || fallback_url