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 JSON to a Rails 3.2 or 4 app, a cryptic and unhelpful error is thrown and they’re left wondering why the request tanked.

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

3.2 throws a 500 error in HTML format (no matter what the client asked for in its Accepts: header), and

format (no matter what the client asked for in its 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.

The middleware that parses parameters ( ActionDispatch::ParamsParser ) runs long before your controller is on the scene, and throws exceptions when invalid JSON is encountered. You can’t capture the parsing exception in your controller, as your controller is never involved in serving the failed request.

Here’s the test where we’re looking for a more informative error message to be thrown. We’re using curb in our JSON 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

Fortunately, it’s easy to write custom middleware that rescue s the errors thrown when JSON 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 JSON by inspecting their HTTP_ACCEPT header, and

by inspecting their header, and If they want JSON , give them back a friendly JSON response with info about where parsing failed.

, give them back a friendly response with info about where parsing failed. If they want something OTHER than JSON , re-raise the error and the default behavior takes over.

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

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

Now when a JSON client submits invalid JSON , 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'"}