RailsでJSONのリクエストパラメータがパースできなかった場合の対応
この記事はQiitaに投稿した内容を一部加筆修正し、移行してきたものです。
昨今、フロントエンド(Webやネイティブアプリケーションなど)は巨大化および複雑化しており、HTMLやJavaScript、CSSなどを配信する通常のモノリシックなRailsアプリケーションでは対応が難しくなってきました。 このような状況も相まって、RailsをJSONなどを返すAPIサーバーとして使用するケースが増えてきています。
Rails 5以降、rails-apiがRails本体に取り込まれることになり1、APIアプリケーションの作成が活発になると思われます。
Rails製のAPIサーバーで、クライアントから不正なJSONがリクエストパラメータが送られ、パースできなかった問題に遭遇ました。 今回はその対策と調査の記録を残します。
対応方法
Rails 3系および4系とRails 5以降で対応方法が異なるため別々に解説していきます。
Rails 3系および4系の場合
config/initializers/
ディレクトリのrescue_json_parse_errors.rb
ファイルに以下のようなミドルウェアを定義します。 ファイル名およびクラス名はこれにこだわる必要はなく、任意なものでかまいません。
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
env
はRackのenv
であるため、env['HTTP_ACCEPT']
の内容をもとにレスポンスのContent-Type
を変更したり、原因となったアクセス元であるRemote IPなどをログ出力したりできます。
その後、config/application.rb
ファイルに、先程定義したミドルウェアを差し込む処理を追加します。
module MyApp
class Application < Rails::Application
# パラメータをパースするミドルウェアの前に独自のミドルウェアのクラス名を文字列で差し込む
config.middleware.insert_before ActionDispatch::ParamsParser, 'RescueJsonParseErrors'
end
end
このようにすることで、リクエストのJSONパースができなかった場合、HTTP statusを400
としつつJSONを返却できます。
なぜ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で発生する例外を共通的に処理するのであれば、上記のようにControllerの基底クラスで例外を捕捉すればよいと思われるかもしれません。
しかし、上記のコードで同様の事象を再現しても例外は捕捉されません。 これはControllerで使用しているparams
のパース処理がControllerの処理に到達する前のミドルウェア層で行われており2 3、そのタイミングで例外が発生してしまいます。
ハマったところ
config/application.rb
にミドルウェアの差し込みを行う際、
module MyApp
class Application < Rails::Application
config.middleware.insert_before ActionDispatch::ParamsParser, RescueJsonParseErrors.to_s
end
end
上記のようにクラスを評価しつつ文字列可するとuninitialized constant RescueJsonParseErrors
となってしまいました。
これは、config/application.rb
が読み込まれた後にinitializers
のファイルが読み込まれることが原因です。
そのため、クラス名の文字列にするか、config/application.rb
で RescueJsonParseErrors
クラスを定義してあるファイルを明示的にrequire
する必要があります。
gemでの解決
解決可能なgemを調べたところ、
の2つが見つかりました。 しかし執筆現在、それぞれのgemはメンテナンスが行われていないため、適切に判断して使用することをお勧めします。
Rails 5以降の場合
Rails 5系では、このあたりの処理に修正が入り、Controllerでリクエストパラメータのパース例外を捕捉できるようになりました。
# Rails 5.2.2未満
class ApplicationController < ActionController::Base
def process_action(*args)
super
rescue ActionDispatch::Http::Parameters::ParseError => _e
render status: 400, json: { message: 'There was a problem in the your JSON' }
end
end
# Rails 5.2.2以上
class ApplicationController < ActionController::Base
rescue_from ActionDispatch::Http::Parameters::ParseError do |_e|
render status: 400, json: { message: 'There was a problem in the your JSON' }
end
end
Rails 5.0.0においてActionDispatch::ParamsParser::ParseError
は非推奨となり、5.2.0で削除されました。 そのため、代替となるActionDispatch::Http::Parameters::ParseError
を使用しています。
終わりに
このような処理を追加すると、どのような恩恵が得られるのでしょうか。ログインを行うAPIを提供するケースを考えてみましょう。
何かしらの理由でリクエストのJSONのパラメータがパースできなかった場合、ActionDispatch::Http::FilterParameters
が適用されずメールアドレスやパスワードなどが平分でログに出力される自体が考えられます。 この処理を適用することで、リクエストパラメータの漏洩事故を防ぐことができます。
これに限らず、サーバーはクライアントからのリクエストは信頼すべきではないため、適切にバリデーション処理を入れるべきでしょう。