RailsでAPIを作るときのエラー処理について
RailsでAPIを雑に書いていたんだけど, コントローラとかをどう書くとエラー処理しやすくなっていいかなーと考えていて, 個人的に考えがまとまったのでブログ書いた.
※9/1に追記書いた.
個人的にAPIを書く上で(API書くに限らない気はするけど)どういうふうにエラー処理を行うと良いかなーと考えてみると
- コントローラ内では基本的に, ある関数の処理が失敗して, 次の処理が行えない場合はすべて例外を投げる
- 例外は各々のコントローラ内で例外のキャッチは行わず, すべてApplicationControllerなど, 親コントローラ内の1メソッドで完結させる
かなーと思う. APIのエラー処理は, Envelopeにステータスコードとエラーメッセージを書いて, APIのフォーマットを統一するほうがクライアントが作りやすそうだし, またこのように処理することで, エラー処理での条件分岐の必要がなくなり, コントローラの可読性の向上にもつながる.
APIつくるんだったら, Grapeサイコーという意見が多い.
確かにGrapeのDSLは直感的に書けるし, バリデーションなど便利メソッドが多いけど, 個人的には素のRailsでAPIを書くほうがセンスが良いと感じる. というのもRackベースなので, ルーティングなど独自のものが多く, せっかくRailsが提供してるRakeのタスクや, ジェネレータがそのまま使えないからである.
SinatraとかでAPI納品するんだったら, Grapeとかいれるのはすごい良さそう.
ただ, そのままのRailsではJSONやXMLをいい感じの構造で返す仕組みが貧弱なので, RABLを導入するのが便利. これはJSONやXMLをいい感じに生成するためのテンプレートエンジンで, DSLを用いて直感的にAPI出力を定義できる.
また, RailsのLayoutsにも対応しており, views/layouts/application.rabl
とかを定義しておくことで, Envelopeみたいなのを簡単に実現できる.
上記に上げたとおり, コントローラ内でモデルのCRUDなどの処理が失敗した場合は例外を投げてApplicationControllerに処理を渡す.
例えばshow
メソッドでは以下のように処理する.
def show
@piyo = Piyo.find_by!(:id, params[:id])
end
以下のようなConcernを定義し, ApplicationControllerから読み込むことでエラー処理を行う.
module Api::ErrorHandlers
extend ActiveSupport::Concern
attr_accessor :status, :message
included do
before_filter :setup
rescue_from StandardError, :with => :rescue_exception
end
private
def rescue_exception(e)
@message = e.message
if rescuable?(e)
re = e.is_a?(Api::Exceptions::RescuableException) ? e : RESCUABLE_EXCEPTIONS[e.to_s.to_sym]
@status = re.status
else
@status = 500
@message = e.message
end
render 'api/errors/base'
end
def rescuable?(e)
return e.is_a?(Api::Exceptions::RescuableException) || RESCUABLE_EXCEPTIONS.has_key?(e.to_s.to_sym)
end
def setup
@status = 200
@message = "OK"
end
end
ポイントはすべての例外処理をrescue_exception
で受け取るところである. このrescue_exception
は投げられた例外によって, 適切なステータスコードとエラーメッセージをビューに渡すメソッドで, それらはEnvelopeとして出力される. 例えばRablのLayoutsで以下のように定義することでエラー出力する.
{
"status": <%= @status.to_json.html_safe %>,
"message": <%= @message.to_json.html_safe %>,
"data": <%= yield %>
}
ここで, 例外に対応するステータスコードを以下のように引く.
- 独自の例外の場合は, その例外クラスにステータスを保持させる
- 組み込みの例外(例えばActiveRecordのNotFoundException)の場合は, 例外に対応するステータスコードの対応表から引く
- それ以外の例外の場合は500を返す
1の場合は, Api::Exceptions::RescuableException
を作成して, それを継承した独自の例外クラスを投げて対応する.
module Api::Exceptions
class RescuableException < StandardError
attr_accessor :status
def initialize(status = 500, message = "Error")
super(message)
@status = status
end
end
class UnAuthenticationException < RescuableException
def initialize(message = "Unauthorized")
super(401, message)
end
end
end
2の場合は, RESCUEABLE_EXCEPTIONS
みたいなハッシュを作って対応する.
RESCUABLE_EXCEPTIONS = {
ActiveRecord::RecordNotFound.to_s.to_sym => Api::Exceptions::RescuableException.new(404, "Record Not Found")
}
3の場合は, 上に2つの条件を満たさない場合に500を返すようにrescue_exception
メソッドを書くことで対応する.
ApplicationControllerでApi::ErrorHandlers
を定義し, rescue_exception
で例外処理することで, 開発速度が上がって良さそうだという個人的なエラー処理のまとめを書いてみた.
@r7kamuraさんに, 以下のリプライを頂いて
http://t.co/mcjKTWmnr7 コントローラで積極的に例外投げるの, わりと自分の中ではしっくりきてるけど, きっと違う局面に遭遇したら違うことしてる気がする
— ゆっち〜 (@yucchiy_) 2014年8月31日
@yucchiy_ 例えばありがちな問題として、RailsにContent-Type: application/jsonを指定しながら誤ったJSONを送ると、パース部分はRack middlewareで実装されているので、例外が発生して500が返ります (400とかにしたい)
— r7kamura (@r7kamura) 2014年8月31日
確かに, Rack middlewareのこととか全く考慮できてなくてダメダメって感じだった.
そして起きたらRailsでAPIをつくるときのエラー処理っていうすごい知見がまとめられていた.