require 'hanami/utils/class_attribute'
require 'hanami/http/status'
module Hanami
module Action
# Throw API
#
# @since 0.1.0
#
# @see Hanami::Action::Throwable::ClassMethods#handle_exception
# @see Hanami::Action::Throwable#halt
# @see Hanami::Action::Throwable#status
module Throwable
# @since 0.2.0
# @api private
RACK_ERRORS = 'rack.errors'.freeze
# 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 http://www.rubydoc.info/github/rack/rack/file/SPEC#The_Error_Stream
# @see https://github.com/hanami/controller/issues/133
RACK_EXCEPTION = 'rack.exception'.freeze
def self.included(base)
base.extend ClassMethods
end
# Throw API class methods
#
# @since 0.1.0
# @api private
module ClassMethods
private
# Handle the given exception with an HTTP status code.
#
# When the exception is raise during #call execution, it will be
# translated into the associated HTTP status.
#
# This is a fine grained control, for a global configuration see
# Hanami::Action.handled_exceptions
#
# @param exception [Hash] the exception class must be the key and the
# HTTP status the value of the hash
#
# @since 0.1.0
#
# @see Hanami::Action.handled_exceptions
#
# @example
# require 'hanami/controller'
#
# class Show
# include Hanami::Action
# handle_exception RecordNotFound => 404
#
# def call(params)
# # ...
# raise RecordNotFound.new
# end
# end
#
# Show.new.call({id: 1}) # => [404, {}, ['Not Found']]
def handle_exception(exception)
configuration.handle_exception(exception)
end
end
protected
# 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 code [Fixnum] a valid HTTP status code
# @param message [String] the response body
#
# @since 0.2.0
#
# @see Hanami::Controller#handled_exceptions
# @see Hanami::Action::Throwable#handle_exception
# @see Hanami::Http::Status:ALL
#
# @example Basic usage
# require 'hanami/controller'
#
# class Show
# def call(params)
# halt 404
# end
# end
#
# # => [404, {}, ["Not Found"]]
#
# @example Custom message
# require 'hanami/controller'
#
# class Show
# def call(params)
# halt 404, "This is not the droid you're looking for."
# end
# end
#
# # => [404, {}, ["This is not the droid you're looking for."]]
def halt(code, message = nil)
message ||= Http::Status.message_for(code)
status(code, message)
throw :halt
end
# Sets the given code and message for the response
#
# @param code [Fixnum] a valid HTTP status code
# @param message [String] the response body
#
# @since 0.1.0
# @see Hanami::Http::Status:ALL
def status(code, message)
self.status = code
self.body = message
end
private
# @since 0.1.0
# @api private
def _rescue
catch :halt do
begin
yield
rescue => exception
_reference_in_rack_errors(exception)
_handle_exception(exception)
end
end
end
# @since 0.2.0
# @api private
def _reference_in_rack_errors(exception)
return if configuration.handled_exception?(exception)
@_env[RACK_EXCEPTION] = exception
if errors = @_env[RACK_ERRORS]
errors.write(_dump_exception(exception))
errors.flush
end
end
# @since 0.2.0
# @api private
def _dump_exception(exception)
[[exception.class, exception.message].compact.join(": "), *exception.backtrace].join("\n\t")
end
# @since 0.1.0
# @api private
def _handle_exception(exception)
raise unless configuration.handle_exceptions
instance_exec(
exception,
&_exception_handler(exception)
)
end
# @since 0.3.0
# @api private
def _exception_handler(exception)
handler = configuration.exception_handler(exception)
if respond_to?(handler.to_s, true)
method(handler)
else
->(ex) { halt handler }
end
end
end
end
end