なんの話?
Ruby on Railsはエラーが起きたときに、HTMLの人間向きの親切な形式でレスポンスを返してくれます。人間がブラウザを使って開発しているときは、エラーの内容が理解しやすい便利な機能です。プログラム向けのAPIのレスポンスとしてはHTMLは扱いにくい不便な形式です。
APIサーバーを作っているときは、レスポンスをJSON形式で返したいです。JSONであれば大抵の言語で簡単にパースできます。Ruby on Railsにおいて、通常のレスポンスの形式は、コントローラーでJSON形式を指定すればJSON形式になります。例えば、次のようにです。
def show
render :json => {:name => "mya name"}
end
しかし、エラーレスポンス、特にJSONパースに失敗したときのエラーレスポンスはHTMLのままで、JSON形式で返せません。これは前述のRailsのHTMLで親切なエラーレスポンスを返す機能です。人間向きではありますが、API向きではありません。
既知の知見
古くはCatching Invalid JSON Parse Errors with Rack Middlewareで、日本語の情報ではRails で JSON のリクエストパラメータがパース出来なかった場合の対応 - Qiitaで、対応方法が知られています。
要約すると、JSONパースエラーはRackミドルウェアの階層で処理されているので、Railsアプリケーションでは変更できません。適切なレスポンスを返すRackミドルウェアを追加して対応します。次のようなRackミドルウェアです。
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 5.2では期待通りに動きません。
原因
Rails 5.2の変更に対応する必要があります。
Rails 5.2の変更
- ActionDispatch::ParamsParser がRackミドルウェアでなくなった
- config.middleware.insert_before にミドルウェアを文字列で指定できなくなった
- JSONパースエラーの例外がActionDispatch::ParamsParser::ParseError でなくなった
以上の差分に対応すると、Rails 5.2でも、JSONパースエラーをJSON形式で返せます。
対応方法
最終型
最初に最終型を示します。次の2つのファイルを追加します。
Rails.application.config.middleware.insert_before Rack::Head, Middleware::RescueJsonParseErrors
module Middleware
class RescueJsonParseErrors
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
rescue ActionDispatch::Http::Parameters::ParseError
raise unless %r{application/json}.match?(env['HTTP_ACCEPT'])
[
400, { 'Content-Type' => 'application/json' },
[{ status: 400, error: 'You submitted a malformed JSON.' }.to_json]
]
end
end
end
ミドルウェアの配置場所はapp/lib/middleware
でなくても構いません。自動読込されるパスで、Rackミドルウェアであることがわかりやすいディレクトリを選択しました。
対応 1. ActionDispatch::ParamsParser がRackミドルウェアでなくなった
結論: ActionDispatch::ParamsParser
の代わりにRack::Head
を指定します。
module MyApp
class Application < Rails::Application
config.middleware.insert_before ActionDispatch::ParamsParser, 'RescueJsonParseErrors'
end
end
の部分です。このまま実行すると次のエラーが発生します。
rails aborted!
NameError: uninitialized constant ActionDispatch::ParamsParser
Rails 5.1からActionDispatch::ParamsParserはActionDispatch::Http::Parametersに変わっています。またミドルウェアでもなくなっています。
rails middleware
コマンドで実行すると
~ rails middleware
use Rack::Cors
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Sprockets::Rails::QuietAssets
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
それらしいミドルウェアは存在しません。
ruby on rails - Catching Invalid JSON Parse Errors with Rack Middleware - Stack OverflowにあるようにRack::Head
というミドルウェアを指定すると上手く動きます。
module MyApp
class Application < Rails::Application
config.middleware.insert_before Rack::Head, 'RescueJsonParseErrors'
end
end
対応 2. config.middleware.insert_before にミドルウェアを文字列で指定できなくなった
結論: config/initializers/rescue_json_parse_errors.rb
でRackミドルウェアを組み込む
ミドルウェアを文字列で指定すると、次のエラーが発生します。
rails aborted!
NoMethodError: undefined method `new' for "RescueJsonParseErrors":String
かと言って
module MyApp
class Application < Rails::Application
config.middleware.insert_before Rack::Head, RescueJsonParseErrors
end
end
とすると、今度は次のエラーが起きます。
rails aborted!
NameError: uninitialized constant MyApp::Application::RescueJsonParseErrors
なぜでしょうか?
# Load the Rails application.
require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
を見ればわかるように、config/apprication.rb
が読み込まれるのは、Rails.application.initialize!
より前です。RescueJsonParseErrors
クラスが読み込まれる前に、参照するため定数RescueJsonParseErrors
を見つけられません。
同じようにRackミドルウェアを読み込んでいるをconfig/initializers/cors.rb
を参考にしましょう。
# Be sure to restart your server when you modify this file.
# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.
# Read more: https://github.com/cyu/rack-cors
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: 'Content-Type,X-Requested-With,Accept,Origin',
methods: [:get, :post, :put, :delete, :options]
end
end
config/initializers
での初期化ではRackミドルウェアが読み込めています。真似をして、次のファイルを用意します。
Rails.application.config.middleware.insert_before Rack::Head, Middleware::RescueJsonParseErrors
ここまでで、ミドルウェアの読み込み成功します。しかし実際に動作させると期待通りには動きません。
対応 3. JSONパースエラーの例外がActionDispatch::ParamsParser::ParseError でなくなった
結論: ActionDispatch::Http::Parameters::ParseError
をハンドリングする
Ruby on Rails 5.2 リリースノート - Rails ガイド 5 Action Pack 5.1 削除されたもの
非推奨のActionController::ParamsParser::ParseErrorを削除。
ActionDispatch::ParamsParser::ParseError
は、Rails 5.2では削除されました。
rails/CHANGELOG.md at 5-1-stable · rails/rails Rails 5.1.0 (April 27, 2017)
Deprecate ActionDispatch::ParamsParser::ParseError in favor of ActionDispatch::Http::Parameters::ParseError.
Rails 5.1の時点で、代替えとしてActionDispatch::Http::Parameters::ParseError
が推奨されています。これに置き換えます。
module Middleware
class RescueJsonParseErrors
def initialize(app)
@app = app
end
def call(env)
@app.call(env)
rescue ActionDispatch::Http::Parameters::ParseError
raise unless %r{application/json}.match?(env['HTTP_ACCEPT'])
[
400, { 'Content-Type' => 'application/json' },
[{ status: 400, error: 'You submitted a malformed JSON.' }.to_json]
]
end
end
end
参考
Gemによる対応
Rails 4用のgemは2つあります。
いまのところ、どちらもRails 5.2に対応していません。
- https://github.com/andrhamm/catch_json_parse_errors/blob/master/lib/catch_json_parse_errors/middleware.rb
- https://github.com/andys/rack_middleware_json_error_msg/blob/master/lib/rack/middleware/json-error-msg.rb