V2.1: Control flow


Actions support control flow through callbacks, halting and redirects.

Callbacks

Callbacks allow logic to be executed either before or after an action’s #handle method. They are useful for encapsulating code used for tasks like checking whether a user is signed in, handling 404 responses or tidying up a response.

A callback can be added using the before or after methods. These methods accept either a symbol representing the name of a method to be called, or a proc.

Like the #handle method, callbacks receive the request and response as arguments.

Method callbacks

# app/actions/books/show.rb

module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        before :validate_params

        params do
          required(:id).filled(:integer)
        end

        def handle(request, response)
          # ...
        end

        private

        def validate_params(request, response)
          params = request.params
          halt 422, request.params.errors.to_h unless request.params.valid?
        end
      end
    end
  end
end

If the action above, the validate_params method will be called before the action’s handler. The callback ensures that if the :id parameter cannot be coerced to an integer, the action will be halted and a 422 Unprocessable response returned.

With this callback in place, the #handle method can now proceed knowing that all parameters are valid.

Proc

The example above could also be implemented using a proc. Procs are bound to the instance context of the action.

# app/actions/books/show.rb
module Bookshelf
  module Actions
    module Books
      class Show < Bookshelf::Action
        before { |request, response| halt 422, request.params.errors.to_h unless request.params.valid? }

        params do
          required(:id).filled(:integer)
        end

        def handle(request, response)
          # ...
        end
      end
    end
  end
end

Halt

Halting an action interrupts its flow and returns control to the framework, which then returns a response based on the status code and body passed to the halt call.

halt 401, "You are not authorized"

Internally, Hanami uses Ruby’s throw and catch mechanisms to provide this behaviour, which is a lightweight approach compared to using exceptions.

This action will return a 401 Unauthorized response when the #authenticated? method returns false:

# app/actions/books/index.rb

module Bookshellf
  module Actions
    module Books
      class Index < Action
        def handle(request, response)
          halt 401 unless authenticated?(request)
          # ...
        end

        private

        def authenticated?(request)
          # ...
        end
      end
    end
  end
end

When halt is invoked, subsequent instructions within the action are entirely skipped. That means that halt will skip the #handle invocation entirely when triggered in a before callback.

# app/actions/books/index.rb

module Bookshelf
  module Actions
    module Books
      class Index < Action
        before :authenticate_user!

        def handle(request, response)
          # ...
        end

        private

        def authenticate_user!(request, response)
          halt 401 unless request.session[:user_id]
        end
      end
    end
  end
end

#halt accepts an HTTP status code as the first argument and an optional body as its second argument. If no body is provided, the body will be set to a message corresponding to the status code (for example the body of a 401 response will be "Unauthorized").

# app/actions/books/index.rb

module Bookshelf
  module Actions
    module Books
      class Index < Action
        def handle(request, response)
          halt 404, "These aren't the droids you're looking for"
        end
      end
    end
  end
end

Hanami 404 response

As with Response#status=, you may use a symbolic status name instead of an integer.

halt :not_found

See Status Codes for the complete list.

Redirects

To redirect a request to another location, call the #redirect_to method on the response object.

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

redirect_to accepts a url and an optional HTTP status, which defaults to 302.

response.redirect_to("/sign-in")
response.redirect_to("https://hanamirb.org", status: 301)

This action below shows an example of redirecting unauthenticated users to a sign in page:

# app/actions/books/index.rb

module Bookshelf
  module Actions
    module Books
      class Index < Action
        before :authenticate_user!

        def handle(request, response)
          # ...
        end

        private

        def authenticate_user!(request, response)
          response.redirect_to("/sign-in") unless request.session[:user_id]
        end
      end
    end
  end
end

If you have given the route you wish to redirect to a name, you can also use the routes helper, which is automatically available to actions.

# config/routes.rb
get "/sign-in", to: "sign_in", as: :sign_in
# Within your action
response.redirect_to routes.path(:sign_in)

See the Routing guide for more information on named routes.