Rails で JSON のリクエストパラメータがパース出来なかった場合の対応

More than 3 years have passed since last update.

Rails 5 で rails-api公式に取り込まれることになり、ますます Rails で API アプリケーションを書くケースが増えていくことでしょう。

ほとんどのケースでデータのやり取りは JSON を介して行われると思いますが、

Rails はその辺り(主にリクエスト周り)をよろしくやってくれるのでとても助かっています。

さて、このエントリでは JSON のリクエストパラメータがパース出来なかった事案に遭遇したので、対応方法を書いていこうと思います。

後述していますが、 Rails 5 系ではこの対応は必要なくなる予定です。


対応方法


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



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 などをログに吐くもよしです。


そもそもなぜこんなことをやる必要があるのか

これって

# Rails 4 系

## 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

# Rails 3 系
## grape の場合
class API < Grape::API
rescue_from MultiJson::LoadError do
error!('There was a problem in the your JSON', 400)
end
end

## action controller の場合
class ApplicationController < ActionController::Base
rescue_from MultiJson::LoadError do
render json: { message: 'There was a problem in the your JSON' }, status: 400
end
end

みたいにすればいいんじゃないの?

と思われる方もいるかもしれませんが、これでは MultiJson::LoadErrorActionDispatch::ParamsParser::ParseError を捕捉することができません。

なぜなら、(まぁ対応している通りですが) controller などで使用できる params のパースが controller などに到達する前に行われているためです。

具体的には Rails 3 系ならここ、 Rails 4 系ならここです。


ハマったところ


config/application.rb

config.middleware.insert_before ActionDispatch::ParamsParser, RescueJsonParseErrors.to_s


としたら uninitialized constant RescueJsonParseErrors で怒られました。

それもそのはずで、 config/application.rb が読み込まれた後に initializers のファイルが読み込まれるからです。

素直に初めから文字列にするか、 application.rbRescueJsonParseErrors を定義してあるファイルを require すればよかったのでした。


Rails 5

Rails 5.0.0.rc1 現在ではこの辺り修正が入るようです。

ここを見る限り、 ParamsParser::ParseError が raise されてくるので、 controller などで ParamsParser::ParseError が捕捉できるようになりますね。


余談

例えばどういうケースで恩恵が得られるのでしょうか。

ログインを行う API を提供するケースを考えてみましょう。

何かしらの理由で JSON がパースできなかった際、

ActionDispatch::Http::FilterParameters が適用されずメールアドレスやパスワードなどがログに出力される自体が考えられます。

また、あくまで個人的な意見ですが、サーバー側は強固に越したことはないので、 Rails 3 系 および 4系 で API アプリケーションを作成する場合は、この対応は入れた方が良いと思っています。