はじめに
Rails で、Web API でJSONを返すものと、HTMLのページを返すものを混在させたアプリケーションを rails new
で作成していました。
routing で、Web API は、 /api
以下にマウントしていました。
/api/xxx
で Web API が呼ばれて想定外のエラーが発生した時、JSON でレスポンスを返すようにしたいと考えたのですが、意外に手間取ったので自分用にメモしておきます。
なお、動作確認は、Rails7.1 で実施しています。
500エラーを発生させる
意図的に 500 エラーを発生させるために、Controller で以下のようなコードを書きます。
class Api::UsersController < ApplicationController
def index
raise StandardError, 'error'
end
end
何もしない場合
何もしない場合、Rails は、/500.html
を返します。JSON ではなくて、HTML が返ってきます。
ただし、リクエストするときに、HEADER で、Accept: application/json
を指定している場合は、JSON が返ってきます。
curl -H "Accept: application/json" http://localhost:3000/api/users # => JSON
curl http://localhost:3000/api/users # => HTML
今回は、/api
以下のリクエストに対しては JSON を返すようにしたいです。
rescue_from StandardError は推奨されない
そこで、/api
の場合だけ、 rescue_from
を使って、StandardError
をキャッチして JSON を返すようにしようと考えましたが、rescue_from
で、 StandardError
をキャッチするのは推奨されないということが Rails Guide に記載されていました(Rails Guide)。
config.exceptions_app を使う
別の方法を色々探していたのですが、config.exceptions_app
を使う方法がいくつかみつかりました。
ちなみに、調べている途中で見つかったRambulance gem も config.exceptions_app
を使っているようでした。
config.exceptions_app を試してみる
config/initializers/exceptions_app.rb
を作成して以下のように記述します。
Rails.application.configure do
config.exceptions_app = -> (env){ ErrorController.action(:show).call(env) }
end
app/controllers/error_controller.rb
を作成して以下のように記述します。
class ErrorController < ApplicationController
def show
exception = request.env["action_dispatch.exception"]
original_path = request.env["action_dispatch.original_path"]
Rails.logger.error "Rendering 500 with exception in #{original_path}: #{exception.class.name}, message: (#{exception.message}), backtrace: #{exception.backtrace}"
# /api 以下のリクエストの場合は、JSON を返す
if original_path.start_with?("/api/")
render_error_json(exception) and return
end
respond_to do |format|
format.json { render_error_json(exception) }
format.html { render file: Rails.root.join("public", "500.html"), status: :internal_server_error, layout: false }
end
end
private
def render_error_json(exception)
render json: { result: "ng", message: exception.message }, status: :internal_server_error
end
end
config.consider_all_requests_local を false にする
Development 環境では、config.consider_all_requests_local
が true
になっていて、エラーが発生したときに、ErrorController が呼ばれないので、config/environments/development.rb
に以下のように記述します。(これに気づくのに時間がかかりました。)
config.consider_all_requests_local = false
試してみる
/api
以下のリクエストに対して、500エラーが発生した場合、JSON が返ってくることを確認します。
curl http://localhost:3000/api/users
{"result":"ng","message":"error"}
--api
だとどうなるか?
ちなみに rails new --api
でアプリケーションを作成した場合だと500エラーでもJSON が返ってきます。
まとめ
今回は、config.exceptions_app
を使って、/api
以下のリクエストに対して、500エラーが発生した場合に JSON を返すようにしました。
他にもっといい方法があれば教えていただきたいです。