diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index dcd3ee0644..74ddf2954a 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -53,6 +53,7 @@ module ActionDispatch autoload :ExceptionWrapper autoload :Flash autoload :ParamsParser + autoload :ApiPublicExceptions autoload :PublicExceptions autoload :Reloader autoload :RemoteIp diff --git a/actionpack/lib/action_dispatch/middleware/api_public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/api_public_exceptions.rb new file mode 100644 index 0000000000..126842672d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/api_public_exceptions.rb @@ -0,0 +1,47 @@ +module ActionDispatch + class ApiPublicExceptions + attr_accessor :public_path + + def initialize(public_path) + @public_path = public_path + end + + def call(env) + exception = env["action_dispatch.exception"] + status = env["PATH_INFO"][1..-1] + request = ActionDispatch::Request.new(env) + content_type = request.formats.first + body = { :status => status, :error => exception.message } + + render(status, content_type, body) + end + + private + + def render(status, content_type, body) + format = content_type && "to_#{content_type.to_sym}" + if format && body.respond_to?(format) + render_format(status, content_type, body.public_send(format)) + else + render_html(status) + end + end + + def render_format(status, content_type, body) + [status, {'Content-Type' => "#{content_type}; charset=#{ActionDispatch::Response.default_charset}", + 'Content-Length' => body.bytesize.to_s}, [body]] + end + + def render_html(status) + found = false + path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale + path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path)) + + if found || File.exist?(path) + render_format(status, 'text/html', File.read(path)) + else + [404, { "X-Cascade" => "pass" }, []] + end + end + end +end diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb index 72eaa916bc..c70bc5f95e 100644 --- a/actionpack/test/dispatch/show_exceptions_test.rb +++ b/actionpack/test/dispatch/show_exceptions_test.rb @@ -11,7 +11,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest when "/bad_params" raise ActionDispatch::ParamsParser::ParseError.new("", StandardError.new) when "/method_not_allowed" - raise ActionController::MethodNotAllowed + raise ActionController::MethodNotAllowed, 'PUT' when "/unknown_http_method" raise ActionController::UnknownHttpMethod when "/not_found_original_exception" @@ -22,7 +22,8 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest end end - ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")) + ProductionApp = ActionDispatch::ShowExceptions.new(Boomer.new, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")) + ProductionApiApp = ActionDispatch::ShowExceptions.new(Boomer.new, ActionDispatch::ApiPublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")) test "skip exceptions app if not showing exceptions" do @app = ProductionApp @@ -55,6 +56,42 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest assert_equal "", body end + test "rescue api apps with json response" do + @app = ProductionApiApp + + get "/", headers: { 'HTTP_ACCEPT' => 'application/json', 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal({ :status => '500', :error => 'puke!' }.to_json, body) + + get "/method_not_allowed", headers: { 'HTTP_ACCEPT' => 'application/json', 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_equal({ :status => '405', :error => 'Only PUT requests are allowed.' }.to_json, body) + + get "/unknown_http_method", headers: { 'HTTP_ACCEPT' => 'application/json', 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_equal({ :status => '405', :error => 'ActionController::UnknownHttpMethod' }.to_json, body) + end + + test "rescue api apps unknown content-type requests with html response" do + @app = ProductionApiApp + + get "/", headers: { 'HTTP_ACCEPT' => 'application/x-custom', 'action_dispatch.show_exceptions' => true } + assert_response 500 + assert_equal "500 error fixture\n", body + + get "/bad_params", headers: { 'HTTP_ACCEPT' => 'application/x-custom', 'action_dispatch.show_exceptions' => true } + assert_response 400 + assert_equal "400 error fixture\n", body + + get "/not_found", headers: { 'HTTP_ACCEPT' => 'application/x-custom', 'action_dispatch.show_exceptions' => true } + assert_response 404 + assert_equal "404 error fixture\n", body + + get "/unknown_http_method", headers: { 'HTTP_ACCEPT' => 'application/x-custom', 'action_dispatch.show_exceptions' => true } + assert_response 405 + assert_equal("", body) + end + test "localize rescue error page" do old_locale, I18n.locale = I18n.locale, :da