---
title: Catching Invalid JSON Parse Errors with Rack Middleware
teaser:
tags: web,rails
author: Dan Collis-Puro
published_on: 2013-11-04
---

There is a world where developers need never worry about poorly formatted JSON. This is not that world.

If a client submits invalid / poorly formatted <abbr title="JavaScript Object Notation">JSON</abbr> to a Rails 3.2 or 4 app, a cryptic and unhelpful error is thrown and they're left wondering why the request tanked.

### Example errors

The error thrown by the parameter parsing middleware behaves differently depending on your version of Rails:

* 3.2 throws a 500 error in <abbr title="HyperText Markup Language">HTML</abbr> format (no matter what the client asked for in its `Accepts:` header), and
* 4.0 throws a 400 "Bad Request" error in the format the client specifies.

Here's the default rails 3.2 error - not great.

    > curl  -H "Accept: application/json" -H "Content-type: application/json" 'http://localhost:3000/posts' -d '{ i am broken'

    <!DOCTYPE html>
    <html>
      <!-- default 500 error page omitted for brevity -->
    </html>

Here's the default Rails 4 error. Not bad, but it could be better.

    > curl -H "Accept: application/json" -H "Content-type: application/json" 'http://localhost:3000/posts' -d '{ i am broken'

    {"status":"400","error":"Bad Request"}%

Neither message tells the client directly about the actual problem - that invalid
JSON was submitted.

## Why

The middleware that parses parameters (`ActionDispatch::ParamsParser`) runs long before your controller is on the scene, and throws exceptions when invalid <abbr title="JavaScript Object Notation">JSON</abbr> is encountered. You can't capture the parsing exception in your controller, as your controller is never involved in serving the failed request.

## TDD All Day, Every Day

Here's the test where we're looking for a more informative error message to be thrown. We're using [curb](http://rubygems.org/gems/curb) in our <abbr title="JavaScript Object Notation">JSON</abbr> client integration tests to simulate a real-world client as closely as possible.

    feature "A client submits JSON" do
      scenario "submitting invalid JSON", js: true do
        invalid_tokens = ', , '
        broken_json = %Q|{"notice":{"title":"A sweet title"#{invalid_tokens}}}|

        curb = post_broken_json_to_api('/notices', broken_json)

        expect(curb.response_code).to eq 400
        expect(curb.content_type).to match(/application\/json/)
        expect(curb.body_str).to match("There was a problem in the JSON you submitted:")
      end

      def post_broken_json_to_api(path, broken_json)
        Curl.post("http://#{host}:#{port}#{path}", broken_json) do |curl|
          set_default_headers(curl)
        end
      end

      def host
        Capybara.current_session.server.host
      end

      def port
        Capybara.current_session.server.port
      end

      def set_default_headers(curl)
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
      end
    end

## Middleware to the Rescue

Fortunately, it's easy to write custom middleware that `rescue`s the
errors thrown when <abbr title="JavaScript Object Notation">JSON</abbr> can't be parsed. To wit, the version for rails 3.2:

    # in app/middleware/catch_json_parse_errors.rb
    class CatchJsonParseErrors
      def initialize(app)
        @app = app
      end

      def call(env)
        begin
          @app.call(env)
        rescue MultiJson::LoadError => error
          if env['HTTP_ACCEPT'] =~ /application\/json/
            error_output = "There was a problem in the JSON you submitted: #{error}"
            return [
              400, { "Content-Type" => "application/json" },
              [ { status: 400, error: error_output }.to_json ]
            ]
          else
            raise error
          end
        end
      end
    end

And the Rails 4.0 version:

    # in app/middleware/catch_json_parse_errors.rb
    class CatchJsonParseErrors
      def initialize(app)
        @app = app
      end

      def call(env)
        begin
          @app.call(env)
        rescue ActionDispatch::ParamsParser::ParseError => error
          if env['HTTP_ACCEPT'] =~ /application\/json/
            error_output = "There was a problem in the JSON you submitted: #{error}"
            return [
              400, { "Content-Type" => "application/json" },
              [ { status: 400, error: error_output }.to_json ]
            ]
          else
            raise error
          end
        end
      end
    end

The only difference is what errors we're looking to rescue -
`MultiJson::LoadError` under rails 3.2, and the more generic
`ActionDispatch::ParamsParser::ParseError` under 4.0.

What this does is:

* Rescue the relevant parser error,
* Look to see if the client wanted <abbr title="JavaScript Object Notation">JSON</abbr> by inspecting their `HTTP_ACCEPT` header, and
* If they want <abbr title="JavaScript Object Notation">JSON</abbr>, give them back a friendly <abbr title="JavaScript Object Notation">JSON</abbr> response with info about where parsing failed.
* If they want something OTHER than <abbr title="JavaScript Object Notation">JSON</abbr>, re-raise the error and the default behavior takes over.

You need to insert the middleware before `ActionDispatch::ParamsParser`, thusly:

```ruby
# in config/application.rb
module YourApp
  class Application < Rails::Application
    # ...
    config.middleware.insert_before ActionDispatch::ParamsParser, "CatchJsonParseErrors"
    # ...
  end
end
```

## The results

Now when a <abbr title="JavaScript Object Notation">JSON</abbr> client submits invalid <abbr title="JavaScript Object Notation">JSON</abbr>, they get back something like:

    > curl  -H "Accept: application/json" -H "Content-type: application/json" 'http://localhost:3000/posts' -d '{ i am broken'

    {"status":400,"error":"There was a problem in the JSON you submitted: 795: unexpected token at '{ i am broken'"}

Excellent. Now we're telling our clients why we rejected their request and
where their <abbr title="JavaScript Object Notation">JSON</abbr> went wrong, instead of leaving them to wonder.
