How to Rescue from ActionDispatch::ParamsParser::ParseError in Rails 4

exception, parameters, rescue, ruby, ruby-on-rails

Solution

Turns out that further up the middleware stack, ActionDispatch::ShowExceptions can be configured with an exceptions app.

module Traphos
  class Application < Rails::Application
    # For the exceptions app
    require "#{config.root}/lib/exceptions/public_exceptions"
    config.exceptions_app = Traphos::PublicExceptions.new(Rails.public_path)
  end
end

Based heavily on the Rails provided one I am now using:

module Traphos
  class PublicExceptions
    attr_accessor :public_path

    def initialize(public_path)
      @public_path = public_path
    end

    def call(env)
      exception    = env["action_dispatch.exception"]
      status       = code_from_exception(env["PATH_INFO"][1..-1], exception)
      request      = ActionDispatch::Request.new(env)
      content_type = request.formats.first
      body         = {:status => { :code => status, :exception => exception.class.name, :message => 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

    def code_from_exception(status, exception)
      case exception
      when ActionDispatch::ParamsParser::ParseError
        "422"
      else
        status
      end
    end
  end
end

To use it in a test environment requires setting config variables (otherwise you get the standard exception handling in development and test). So to test I have (edited to just have the key parts):

describe Photo, :type => :api do
  context 'update' do
    it 'attributes with non-parseable json' do 

      Rails.application.config.consider_all_requests_local = false
      Rails.application.config.action_dispatch.show_exceptions = true

      patch update_url, {:description => description}
      response.status.should eql(422)
      result = JSON.parse(response.body)
      result['status']['exception'].should match(/ParseError/)

      Rails.application.config.consider_all_requests_local = true
      Rails.application.config.action_dispatch.show_exceptions = false
    end
  end
end

Which performs as I need in a public API way and is adaptable for any other exceptions I may choose to customise.

Problem

Rails 4 adds an exception ActionDispatch::ParamsParser::ParseError exception but since its in the middleware stack it appears it can't be rescued in the normal controller environment. In a json API application I want respond with a standard error format. This gist shows a strategy for inserting middleware to intercept and respond. Following this pattern I have: application.rb: ``` module Traphos class Application < Rails::Application .... config.middleware.insert_before ActionDispatch::ParamsParser, "JSONParseError" end end ``` And the middleware is: ``` class JSONParseError def initialize(app) @app = app end def call(env) begin @app.call(env) rescue ActionDispatch::ParamsParser::ParseError => e [422, {}, ['Parse Error']] end end end ``` If I run my test without the middleware I get (spec): ``` Failures: 1) Photo update attributes with non-parseable json Failure/Error: patch update_url, {:description => description}, "CONTENT_TYPE" => content_type, "HTTP_ACCEPT" => accepts, "HTTP_AUTHORIZATION" => @auth ActionDispatch::ParamsParser::ParseError: 399: unexpected token at 'description=Test+New+Description]' ``` Which is exactly what I would expect (ParseError that I can't rescue_from). Now with the only change to add in the middleware above: ``` 2) Photo update attributes with non-parseable json Failure/Error: response.status.should eql(422) expected: 422 got: 200 ``` And the log shows that the standard controller action is being executed and returning a normal response (although since it didn't receive any parameters it didn't update anything). My questions: How can rescue from ParseError and return a custom response. Feels like I'm on the right track but not quite there. I can't work out why, when the exception is raised and rescued, that the controller action still proceeds. Help much appreciated, --Kip

Original source