class Hanami::Action

An HTTP endpoint

@since 0.1.0

@example

require "hanami/controller"

class Show < Hanami::Action
  def handle(req, res)
    # ...
  end
end

@api public

@api private

Constants

ACTION_INSTANCE

@since 2.2.0 @api private

CACHE_CONTROL

The HTTP header for Cache-Control

@since 2.0.0 @api private

CONTENT_LENGTH

The Content-Length HTTP header

@since 1.0.0 @api private

The key used by Rack to set the cookies as an Hash in the env

@since 2.0.0 @api private

The key used by Rack to set the cookies as a String in the env

@since 2.0.0 @api private

DEFAULT_ACCEPT

The default mime type for an incoming HTTP request

@since 0.1.0 @api private

DEFAULT_CHARSET

@since 2.0.0 @api private

DEFAULT_CONTENT_TYPE

The default mime type that is returned in the response

@since 0.1.0 @api private

DEFAULT_ERROR_CODE

@since 1.0.0 @api private

DEFAULT_ID_LENGTH

@since 2.0.0 @api private

DEFAULT_REQUEST_METHOD

Default HTTP request method for Rack env

@since 2.0.0 @api private

ENTITY_HEADERS

Entity headers allowed in blank body responses, according to RFC 2616 - Section 10 (HTTP 1.1).

“The response MAY include new or updated metainformation in the form

of entity-headers".

@since 0.4.0 @api private

@see www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.2.5 @see www.w3.org/Protocols/rfc2616/rfc2616-sec7.html

ETAG

The HTTP header for ETag

@since 2.0.0 @api private

EXPIRES

The HTTP header for Expires

@since 2.0.0 @api private

GET

GET request

@since 2.0.0 @api private

HEAD request

@since 0.3.2 @api private

HTTP_ACCEPT

The key that returns accepted mime types from the Rack env

@since 0.1.0 @api private

The key that returns raw cookies from the Rack env

@since 2.0.0 @api private

HTTP_STATUSES_WITHOUT_BODY

Status codes that by RFC must not include a message body

@since 0.3.2 @api private

IF_MODIFIED_SINCE

@since 2.0.0 @api private

IF_NONE_MATCH

@since 2.0.0 @api private

LAST_MODIFIED

The HTTP header for Last-Modified

@since 0.3.0 @api private

LOCATION

The HTTP header for redirects

@since 0.2.0 @api private

NOT_FOUND

Not Found

@since 1.0.0 @api private

OPTIONS

OPTIONS request

@since 2.0.0 @api private

PATH_INFO

The request relative path

@since 2.0.0 @api private

RACK_ERRORS

@since 0.2.0 @api private

RACK_EXCEPTION

This isn’t part of Rack SPEC

Exception notifiers use rack.exception instead of rack.errors, so we need to support it.

@since 0.5.0 @api private

@see Hanami::Action::Throwable::RACK_ERRORS @see www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream @see github.com/hanami/controller/issues/133

RACK_INPUT

The key that returns raw input from the Rack env

@since 2.0.0 @api private

RACK_SESSION

The key that returns Rack session params from the Rack env Please note that this is used only when an action is unit tested.

@since 2.0.0 @api private

@example

# action unit test
action.call("rack.session" => { "foo" => "bar" })
action.session[:foo] # => "bar"

@api private

REQUEST_ID

@since 2.0.0 @api private

REQUEST_METHOD

The request method

@since 0.3.2 @api private

RESPONSE_BODY

Rack SPEC response body

@since 1.0.0 @api private

RESPONSE_CODE

Rack SPEC response code

@since 1.0.0 @api private

RESPONSE_HEADERS

Rack SPEC response headers

@since 1.0.0 @api private

ROUTER_PARAMS

The key that returns router params from the Rack env This is a builtin integration for Hanami::Router

@since 2.0.0 @api private

TRACE

TRACE request

@since 2.0.0 @api private

X_CASCADE

The non-standard HTTP header to pass the control over when a resource cannot be found by the current endpoint

@since 1.0.0 @api private

Public Class Methods

after(...)

@since 0.1.0

Alias for: append_after
append_after(...) click to toggle source

@overload self.append_after(*callbacks, &block)

Define a callback for an Action.
The callback will be executed **after** the action is called, in the
order they are added.

@param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
  each of them is representing a name of a method available in the
  context of the Action.

@param blk [Proc] an anonymous function to be executed

@return [void]

@since 0.3.2

@see Hanami::Action::Callbacks::ClassMethods#append_before
# File lib/hanami/action.rb, line 219
def self.append_after(...)
  config.after_callbacks.append(...)
end
Also aliased as: after
append_before(...) click to toggle source

@overload self.append_before(*callbacks, &block)

Define a callback for an Action.
The callback will be executed **before** the action is called, in the
order they are added.

@param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
  each of them is representing a name of a method available in the
  context of the Action.

@param blk [Proc] an anonymous function to be executed

@return [void]

@since 0.3.2

@see Hanami::Action::Callbacks::ClassMethods#append_after

@example Method names (symbols)
  require "hanami/controller"

  class Show < Hanami::Action
    before :authenticate, :set_article

    def handle(req, res)
    end

    private
    def authenticate
      # ...
    end

    # `params` in the method signature is optional
    def set_article(params)
      @article = Article.find params[:id]
    end
  end

  # The order of execution will be:
  #
  # 1. #authenticate
  # 2. #set_article
  # 3. #call

@example Anonymous functions (Procs)
  require "hanami/controller"

  class Show < Hanami::Action
    before { ... } # 1 do some authentication stuff
    before {|req, res| @article = Article.find params[:id] } # 2

    def handle(req, res)
    end
  end

  # The order of execution will be:
  #
  # 1. authentication
  # 2. set the article
  # 3. `#handle`
# File lib/hanami/action.rb, line 194
def self.append_before(...)
  config.before_callbacks.append(...)
end
Also aliased as: before
before(...)

@since 0.1.0

Alias for: append_before
contract() click to toggle source

Placeholder for the ‘.contract` method. Raises an error when the hanami-validations gem is not installed.

@raise [NoMethodError]

@api public @since 2.2.0

# File lib/hanami/action.rb, line 130
def self.contract
  message = %(To use `.contract`, please add the "hanami-validations" gem to your Gemfile)
  raise NoMethodError, message
end
format(...) click to toggle source

@see Config#format

@since 2.0.0 @api public

# File lib/hanami/action.rb, line 272
def self.format(...)
  config.format(...)
end
gem_loader() click to toggle source

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 33
def self.gem_loader
  @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
    root = File.expand_path("..", __dir__)
    loader.tag = "hanami-controller"
    loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-controller.rb")
    loader.push_dir(root)
    loader.ignore(
      "#{root}/hanami-controller.rb",
      "#{root}/hanami/controller/version.rb",
      "#{root}/hanami/action/{constants,errors,validatable}.rb"
    )
    loader.inflector.inflect("csrf_protection" => "CSRFProtection")
  end
end
handle_exception(...) click to toggle source

@see Config#handle_exception

@since 2.0.0 @api public

# File lib/hanami/action.rb, line 280
def self.handle_exception(...)
  config.handle_exception(...)
end
inherited(subclass) click to toggle source

Override Ruby’s hook for modules. It includes basic Hanami::Action modules to the given class.

@param subclass [Class] the target action

@since 0.1.0 @api private

Calls superclass method
# File lib/hanami/action.rb, line 101
def self.inherited(subclass)
  super

  if subclass.superclass == Action
    subclass.class_eval do
      include Validatable if defined?(Validatable)
    end
  end
end
new(config: self.class.config, contract: nil) click to toggle source

Returns a new action

@since 2.0.0 @api public

# File lib/hanami/action.rb, line 296
def initialize(config: self.class.config, contract: nil)
  @config = config
  @contract = contract || config.contract_class&.new # TODO: tests showing this overridden by a dep
  freeze
end
params(_klass = nil) click to toggle source

Placeholder for the ‘.params` method. Raises an error when the hanami-validations gem is not installed.

@raise [NoMethodError]

@api public @since 2.0.0

# File lib/hanami/action.rb, line 118
def self.params(_klass = nil)
  message = %(To use `.params`, please add the "hanami-validations" gem to your Gemfile)
  raise NoMethodError, message
end
prepend_after(...) click to toggle source

@overload self.prepend_after(*callbacks, &block)

Define a callback for an Action.
The callback will be executed **after** the action is called.
It will add the callback at the beginning of the callbacks' chain.

@param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
  each of them is representing a name of a method available in the
  context of the Action.

@param blk [Proc] an anonymous function to be executed

@return [void]

@since 0.3.2

@see Hanami::Action::Callbacks::ClassMethods#prepend_before
# File lib/hanami/action.rb, line 264
def self.prepend_after(...)
  config.after_callbacks.prepend(...)
end
prepend_before(...) click to toggle source

@overload self.prepend_before(*callbacks, &block)

Define a callback for an Action.
The callback will be executed **before** the action is called.
It will add the callback at the beginning of the callbacks' chain.

@param callbacks [Symbol, Array<Symbol>] a single or multiple symbol(s)
  each of them is representing a name of a method available in the
  context of the Action.

@param blk [Proc] an anonymous function to be executed

@return [void]

@since 0.3.2

@see Hanami::Action::Callbacks::ClassMethods#prepend_after
# File lib/hanami/action.rb, line 244
def self.prepend_before(...)
  config.before_callbacks.prepend(...)
end

Public Instance Methods

call(env) click to toggle source

Implements the Rack/Hanami::Action protocol

@since 0.1.0 @api private

# File lib/hanami/action.rb, line 306
def call(env)
  request  = nil
  response = nil

  halted = catch :halt do
    params = Params.new(env: env, contract: contract)
    request  = build_request(
      env: env,
      params: params,
      session_enabled: session_enabled?
    )
    response = build_response(
      request: request,
      config: config,
      content_type: Mime.response_content_type_with_charset(request, config),
      env: env,
      headers: config.default_headers,
      session_enabled: session_enabled?
    )

    enforce_accepted_mime_types(request)

    _run_before_callbacks(request, response)
    handle(request, response)
    _run_after_callbacks(request, response)
  rescue StandardError => exception
    _handle_exception(request, response, exception)
  end

  # Before finishing, put ourself into the Rack env for third-party instrumentation tools to
  # integrate with actions
  env[ACTION_INSTANCE] = self

  finish(request, response, halted)
end

Protected Instance Methods

_requires_empty_headers?(res)

@since 2.0.0 @api private

Alias for: _requires_no_body?
_requires_no_body?(res) click to toggle source

@since 0.3.2 @api private

# File lib/hanami/action.rb, line 401
def _requires_no_body?(res)
  HTTP_STATUSES_WITHOUT_BODY.include?(res.status)
end
Also aliased as: _requires_empty_headers?
halt(status, body = nil) click to toggle source

Halt the action execution with the given HTTP status code and message.

When used, the execution of a callback or of an action is interrupted and the control returns to the framework, that decides how to handle the event.

If a message is provided, it sets the response body with the message. Otherwise, it sets the response body with the default message associated to the code (eg 404 will set ‘“Not Found”`).

@param status [Fixnum] a valid HTTP status code @param body [String] the response body

@raise [StandardError] if the code isn’t valid

@since 0.2.0

@see Hanami::Action::Throwable#handle_exception @see Hanami::Http::Status:ALL

@example Basic usage

require "hanami/controller"

class Show < Hanami::Action
  def handle(*)
    halt 404
  end
end

# => [404, {}, ["Not Found"]]

@example Custom message

require "hanami/controller"

class Show < Hanami::Action
  def handle(*)
    halt 404, "This is not the droid you're looking for."
  end
end

# => [404, {}, ["This is not the droid you're looking for."]]
# File lib/hanami/action.rb, line 395
def halt(status, body = nil)
  Halt.call(status, body)
end
handle(request, response) click to toggle source

Hook for subclasses to apply behavior as part of action invocation

@param request [Hanami::Action::Request] @param response [Hanami::Action::Response]

@since 2.0.0 @api public

# File lib/hanami/action.rb, line 351
def handle(request, response)
end

Private Instance Methods

_dump_exception(exception) click to toggle source

@since 0.2.0 @api private

# File lib/hanami/action.rb, line 466
def _dump_exception(exception)
  [[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
end
_empty_body(res) click to toggle source

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 575
def _empty_body(res)
  res.body = Response::EMPTY_BODY
end
_empty_headers(res) click to toggle source

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 569
def _empty_headers(res)
  res.headers.select! { |header, _| keep_response_header?(header) }
end
_exception_handler(handler) click to toggle source

@since 0.3.0 @api private

# File lib/hanami/action.rb, line 492
def _exception_handler(handler)
  if respond_to?(handler.to_s, true)
    method(handler)
  else
    ->(*) { halt handler }
  end
end
_handle_exception(req, res, exception) click to toggle source

@since 0.1.0 @api private

# File lib/hanami/action.rb, line 472
def _handle_exception(req, res, exception)
  handler = exception_handler(exception)

  if handler.nil?
    _reference_in_rack_errors(req, exception)
    raise exception
  end

  instance_exec(
    req,
    res,
    exception,
    &_exception_handler(handler)
  )

  nil
end
_reference_in_rack_errors(req, exception) click to toggle source

@since 0.2.0 @api private

# File lib/hanami/action.rb, line 455
def _reference_in_rack_errors(req, exception)
  req.env[RACK_EXCEPTION] = exception

  if errors = req.env[RACK_ERRORS]
    errors.write(_dump_exception(exception))
    errors.flush
  end
end
_run_after_callbacks(*args) click to toggle source

@since 0.1.0 @api private

# File lib/hanami/action.rb, line 509
def _run_after_callbacks(*args)
  config.after_callbacks.run(self, *args)
  nil
end
_run_before_callbacks(*args) click to toggle source

@since 0.1.0 @api private

# File lib/hanami/action.rb, line 502
def _run_before_callbacks(*args)
  config.before_callbacks.run(self, *args)
  nil
end
build_request(**options) click to toggle source

Hook to be overridden by ‘Hanami::Extensions::Action` for integrated actions

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 441
def build_request(**options)
  Request.new(**options)
end
build_response(**options) click to toggle source

Hook to be overridden by ‘Hanami::Extensions::Action` for integrated actions

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 449
def build_response(**options)
  Response.new(**options)
end
enforce_accepted_mime_types(request) click to toggle source

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 413
def enforce_accepted_mime_types(request)
  return if config.formats.empty?

  Mime.enforce_accept(request, config) { return halt 406 }
  Mime.enforce_content_type(request, config) { return halt 415 }
end
exception_handler(exception) click to toggle source

@since 2.0.0 @api private

# File lib/hanami/action.rb, line 422
def exception_handler(exception)
  config.handled_exceptions.each do |exception_class, handler|
    return handler if exception.is_a?(exception_class)
  end

  nil
end
finish(req, res, halted) click to toggle source

Finalize the response

Prepare the data before the response will be returned to the webserver

@since 0.1.0 @api private @abstract

@see Hanami::Action::Session#finish @see Hanami::Action::Cookies#finish @see Hanami::Action::Cache#finish

# File lib/hanami/action.rb, line 590
def finish(req, res, halted)
  res.status, res.body = *halted unless halted.nil?

  _empty_headers(res) if _requires_empty_headers?(res)
  _empty_body(res) if res.head?

  res.set_format(Action::Mime.detect_format(res.content_type, config))
  res[:params] = req.params
  res[:format] = res.format
  res
end
keep_response_header?(header) click to toggle source

According to RFC 2616, when a response MUST have an empty body, it only allows Entity Headers.

For instance, a 204 doesn’t allow Content-Type or any other custom header.

This restriction is enforced by Hanami::Action#_requires_no_body?.

However, there are cases that demand to bypass this rule to set meta informations via headers.

An example is a DELETE request for a JSON API application. It returns a 204 but still wants to specify the rate limit quota via X-Rate-Limit.

@since 0.5.0

@see Hanami::Action#_requires_no_body?

@example

require "hanami/controller"

module Books
  class Destroy < Hanami::Action
    def handle(*, res)
      # ...
      res.headers.merge!(
        "Last-Modified" => "Fri, 27 Nov 2015 13:32:36 GMT",
        "X-Rate-Limit"  => "4000",
        "Content-Type"  => "application/json",
        "X-No-Pass"     => "true"
      )

      res.status = 204
    end

    private

    def keep_response_header?(header)
      super || header == "X-Rate-Limit"
    end
  end
end

# Only the following headers will be sent:
#  * Last-Modified - because we used `super' in the method that respects the HTTP RFC
#  * X-Rate-Limit  - because we explicitely allow it

# Both Content-Type and X-No-Pass are removed because they're not allowed
# File lib/hanami/action.rb, line 563
def keep_response_header?(header)
  ENTITY_HEADERS.include?(header)
end
session_enabled?() click to toggle source

@see Session#session_enabled? @since 2.0.0 @api private

# File lib/hanami/action.rb, line 433
def session_enabled?
  false
end