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
- COOKIE_HASH_KEY
The key used by
Rack
to set the cookies as an Hash in the env@since 2.0.0 @api private
- COOKIE_STRING_KEY
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
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
- HTTP_COOKIE
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
SPECException notifiers use
rack.exception
instead ofrack.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 theRack
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
@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
@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
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
@see Config#format
@since 2.0.0 @api public
# File lib/hanami/action.rb, line 272 def self.format(...) config.format(...) end
@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
@since 2.0.0 @api public
# File lib/hanami/action.rb, line 280 def self.handle_exception(...) config.handle_exception(...) end
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
# 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
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
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
@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
@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
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
@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
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
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
@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
@since 2.0.0 @api private
# File lib/hanami/action.rb, line 575 def _empty_body(res) res.body = Response::EMPTY_BODY end
@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
@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
@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
@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
@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
@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
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
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
@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
@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
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
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
@see Session#session_enabled?
@since 2.0.0 @api private
# File lib/hanami/action.rb, line 433 def session_enabled? false end