1. kenchan0130

    Posted

    kenchan0130
Changes in title
+ActionDispatch::ParamsParser::ParseError で JSON がパース出来なかった場合の対応
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,107 @@
+Rails 5 で [rails-api](https://github.com/rails-api/rails-api) が[公式に取り込まれる](https://github.com/rails/rails/pull/19832)ことになり、ますます Rails で API アプリケーションを書くケースが増えていくことでしょう。
+ほとんどのケースでデータのやり取りは JSON を介して行われると思いますが、
+Rails はその辺り(主にリクエスト周り)をよろしくやってくれるのでとても助かっています。
+
+さて、このエントリでは `ActionDispatch::ParamsParser::ParseError` で JSON がパース出来なかった事案に遭遇したので、対応方法を書いていこうと思います。
+後述していますが、 Rails 5 系ではこの対応は必要なくなる予定です。
+
+## 対応方法
+
+```rb:config/initializers/rescue_json_parse_errors.rb
+# Rails 4 系の場合
+class RescueJsonParseErrors
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ begin
+ @app.call(env)
+ rescue ActionDispatch::ParamsParser::ParseError => _e
+ return [
+ 400, { 'Content-Type' => 'application/json' },
+ [{ error: 'There was a problem in the your JSON' }.to_json]
+ ]
+ end
+ end
+end
+
+# Rails 3 系の場合
+class RescueJsonParseErrors
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ begin
+ @app.call(env)
+ rescue MultiJson::LoadError => _e
+ return [
+ 400, { 'Content-Type' => 'application/json' },
+ [{ error: 'There was a problem in the your JSON' }.to_json]
+ ]
+ end
+ end
+end
+
+```
+
+```rb:config/application.rb
+module MyApp
+ class Application < Rails::Application
+ config.middleware.insert_before ActionDispatch::ParamsParser, 'RescueJsonParseErrors'
+ end
+end
+```
+
+今回は `initializers` にファイルを設置していますが、 application が立ち上がる際に `require` されていれば良いので、ご自身の構成に合わせて適宜変更していただいて構いません。
+また、必ずしもファイル名やクラス名がこの名前である必要はありません。
+
+上記のようにすると JSON パースが出来なかった場合、
+HTTP status を 400 として json レスポンスを返却することができます。
+
+`env` は rack の `env` なので、
+`env['HTTP_ACCEPT']` の内容で レスポンスの `Content-Type` を変更するもよし、 remote IP などをログに吐くもよしです。
+
+
+## そもそもなぜこんなことをやる必要があるのか
+これって
+
+```rb
+# grape の場合
+class API < Grape::API
+ rescue_from ActionDispatch::ParamsParser::ParseError do
+ error!('There was a problem in the your JSON', 400)
+ end
+end
+
+# action controller の場合
+class ApplicationController < ActionController::Base
+ rescue_from ActionDispatch::ParamsParser::ParseError do
+ render json: { message: 'There was a problem in the your JSON' }, status: 400
+ end
+end
+```
+
+みたいにすればいいんじゃないの?
+と思われる方もいるかもしれませんが、これでは `ActionDispatch::ParamsParser::ParseError` を捕捉することができません。
+
+なぜなら、(まぁ対応している通りですが) controller などで使用できる `params` の parse が controller などに到達する前に行われているためです。
+具体的には Rails 3 系なら[ここ](https://github.com/rails/rails/blob/3-2-stable/actionpack/lib/action_dispatch/middleware/params_parser.rb#L46-L50)、 Rails 4 系なら[ここ](https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/params_parser.rb#L43-L46)です。
+
+
+## ハマったところ
+```rb:config/application.rb
+config.middleware.insert_before ActionDispatch::ParamsParser, RescueJsonParseErrors.to_s
+```
+
+としたら `uninitialized constant RescueJsonParseErrors` で怒られました。
+それもそのはずで、 `config/application.rb` が読み込まれた後に `initializers` のファイルが読み込まれるからです。
+素直に初めから文字列にするか、 `application.rb` で `RescueJsonParseErrors` を定義してあるファイルを `require` すればよかったのでした。
+
+
+## 余談
+あくまで個人的な意見ですが、サーバー側は強固に越したことはないので、 Rails で API アプリケーションを作成する場合は、この対応は入れた方が良いと思っています。
+
+また、Rails 5 系ではこの辺り修正が入るようです。
+[ここ](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/http/request.rb#L372-L374)を見る限り、 `ParamsParser::ParseError` が raise されてくるので、 controller などで `ParamsParser::ParseError` が捕捉できるようになりますね。