Skip to content

Commit

Permalink
Exception handling for controllers using ActionController::Live
Browse files Browse the repository at this point in the history
Any exceptions that occured at the view or controller level for a
controller using ActionController::Live would cause the server to either
hang with an open socket indefinitely, or immediately crash (depending
on whether the server was launched with rails s or directly). Changed
the behavior of exceptions to act the same as streaming templates for
html requests, and allow for an on_error callback if needed.
  • Loading branch information
sgrif committed Mar 18, 2013
1 parent eb32b36 commit c01d080
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 1 deletion.
28 changes: 28 additions & 0 deletions actionpack/lib/action_controller/metal/live.rb
Expand Up @@ -56,6 +56,14 @@ def close
super super
@buf.push nil @buf.push nil
end end

def on_error(&block)
@error_callback = block
end

def call_on_error
@error_callback.call
end
end end


class Response < ActionDispatch::Response #:nodoc: all class Response < ActionDispatch::Response #:nodoc: all
Expand Down Expand Up @@ -121,6 +129,16 @@ def process(name)


begin begin
super(name) super(name)
rescue => e
begin
@_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
@_response.stream.call_on_error
rescue => exceptionception
log_error(exceptionception)
ensure
log_error(e)
@_response.stream.close
end
ensure ensure
@_response.commit! @_response.commit!
end end
Expand All @@ -129,6 +147,16 @@ def process(name)
@_response.await_commit @_response.await_commit
end end


def log_error(exception)
logger = ActionController::Base.logger
return unless logger

message = "\n#{exception.class} (#{exception.message}):\n"
message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
message << " " << exception.backtrace.join("\n ")
logger.fatal("#{message}\n\n")
end

def response_body=(body) def response_body=(body)
super super
response.stream.close if response response.stream.close if response
Expand Down
76 changes: 75 additions & 1 deletion actionpack/test/controller/live_stream_test.rb
Expand Up @@ -52,6 +52,29 @@ def thread_locals
def with_stale def with_stale
render :text => 'stale' if stale?(:etag => "123") render :text => 'stale' if stale?(:etag => "123")
end end

def exception_in_view
render 'doesntexist'
end

def exception_with_callback
response.headers['Content-Type'] = 'text/event-stream'

response.stream.on_error do
response.stream.write %(data: "500 Internal Server Error"\n\n)
response.stream.close
end

raise 'An exception occurred...'
end

def exception_in_exception_callback
response.headers['Content-Type'] = 'text/event-stream'
response.stream.on_error do
raise 'We need to go deeper.'
end
response.stream.write params[:widget][:didnt_check_for_nil]
end
end end


tests TestController tests TestController
Expand All @@ -66,6 +89,21 @@ def build_response
TestResponse.new TestResponse.new
end end


def assert_stream_closed
assert response.stream.closed?, 'stream should be closed'
end

def capture_log_output
output = StringIO.new
old_logger, ActionController::Base.logger = ActionController::Base.logger, ActiveSupport::Logger.new(output)

begin
yield output
ensure
ActionController::Base.logger = old_logger
end
end

def test_set_response! def test_set_response!
@controller.set_response!(@request) @controller.set_response!(@request)
assert_kind_of(Live::Response, @controller.response) assert_kind_of(Live::Response, @controller.response)
Expand Down Expand Up @@ -119,7 +157,43 @@ def test_live_stream_default_header
def test_render_text def test_render_text
get :render_text get :render_text
assert_equal 'zomg', response.body assert_equal 'zomg', response.body
assert response.stream.closed?, 'stream should be closed' assert_stream_closed
end

def test_exception_handling_html
capture_log_output do |output|
get :exception_in_view
assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body
assert_match 'Missing template test/doesntexist', output.rewind && output.read
assert_stream_closed
end
end

def test_exception_handling_plain_text
capture_log_output do |output|
get :exception_in_view, format: :json
assert_equal '', response.body
assert_match 'Missing template test/doesntexist', output.rewind && output.read
assert_stream_closed
end
end

def test_exception_callback
capture_log_output do |output|
get :exception_with_callback, format: 'text/event-stream'
assert_equal %(data: "500 Internal Server Error"\n\n), response.body
assert_match 'An exception occurred...', output.rewind && output.read
assert_stream_closed
end
end

def test_exceptions_raised_handling_exceptions
capture_log_output do |output|
get :exception_in_exception_callback, format: 'text/event-stream'
assert_equal '', response.body
assert_match 'We need to go deeper', output.rewind && output.read
assert_stream_closed
end
end end


def test_stale_without_etag def test_stale_without_etag
Expand Down

0 comments on commit c01d080

Please sign in to comment.