LoginSignup
7
11

More than 5 years have passed since last update.

Rails5.2でJSONパースエラーをJSON形式で返す

Last updated at Posted at 2019-02-16

なんの話?

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ミドルウェアです。

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 5.2では期待通りに動きません。

原因

Rails 5.2の変更に対応する必要があります。

Rails 5.2の変更

  1. ActionDispatch::ParamsParser がRackミドルウェアでなくなった
  2. config.middleware.insert_before にミドルウェアを文字列で指定できなくなった
  3. JSONパースエラーの例外がActionDispatch::ParamsParser::ParseError でなくなった

以上の差分に対応すると、Rails 5.2でも、JSONパースエラーをJSON形式で返せます。

対応方法

最終型

最初に最終型を示します。次の2つのファイルを追加します。

config/initializers/rescue_json_parse_errors.rb
Rails.application.config.middleware.insert_before Rack::Head, Middleware::RescueJsonParseErrors
app/lib/middleware/rescue_json_parse_errors.rb
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を指定します。

config/apprication.rb
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というミドルウェアを指定すると上手く動きます。

config/apprication.rb
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

かと言って

config/apprication.rb
module MyApp
  class Application < Rails::Application
    config.middleware.insert_before Rack::Head, RescueJsonParseErrors
  end
end

とすると、今度は次のエラーが起きます。

rails aborted!
NameError: uninitialized constant MyApp::Application::RescueJsonParseErrors

なぜでしょうか?

config/environment.rb
# 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を参考にしましょう。

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ミドルウェアが読み込めています。真似をして、次のファイルを用意します。

config/initializers/rescue_json_parse_errors.rb
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が推奨されています。これに置き換えます。

config/initializers/rescue_json_parse_errors.rb
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に対応していません。

その他の参考ページ

7
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
11