はじめに
- RailsのAPIでエラー処理ってどうすればいいんだろう?
- どんなJSONを返せばいいんだろう?
筆者が関わっている企業でよくこのように悩んでる人を見かけるので、自分の経験を基に記事にまとめてみました。
返すべきJSONの形
ぶっちゃけ、企業によってまちまちですし、「これが絶対正解だ!」と言えるものはありません。
いろんな企業のAPIがどのような形のレスポンスを返しているか知りたければ、WebAPIでエラーをどう表現すべき?15のサービスを調査してみたという記事が非常に参考になります。
ただ、上記の記事にも書いてありますが、2016年に出たRFC7807が、「こういう形でいいんじゃね!?」っていう標準となるようなJSONの形を提案してくれています。
RFC7807が提案する形
詳しくはRFCの方を参照していただければと思いますが、具体例を書くとこんな感じです。
{
"type": "https://your-api-url.com/problems/article-not-found",
"title": "お探しの記事は見つかりませんでした",
"status": 404,
"detail": "IDが999の記事は見つかりませんでした。URLが正しいかご確認ください。",
"instance": "/articles/999"
}
フィールド名 | 必須 | 簡単な説明(詳しくはRFCを参照) |
---|---|---|
type | ✔︎ | エラーの種類を識別するURI。URIに遷移すると、エラーのドキュメンテーションが返されることが望ましい。 |
title | エラーを説明する短い文章。typeと1:1の関係(i18nは除く)。 | |
status | httpステータスコード | |
detail | titleの長いバージョン。より詳細な説明のための文章。 | |
instance | エラーの原因となったリソースのインスタンスを識別するURI。 |
また、上記以外にも必要に応じてフィールドを追加することも可能。
RFC7807に準拠した最低限の形
では、実際にtype
,title
,status
,detail
,instance
は全て必要かと言われたら、そんなことはありません。
以下は、筆者の個人的な意見になりますが、各フィールドの必要性を判断したものです。
-
type
: 必須だしエラーの種類を識別するエラーコード的な役割も果たしてくれるので必要 -
title
: ユーザーに表示するエラーメッセージとして使えるので必要 -
status
: レスポンスのボディになくてもヘッダーを見ればわかるので不要 -
detail
,instance
: あっても使う機会は少なそう(規模にもよるかもしれない)ので不要
まとめると「とりあえずtype
とtitle
さえあれば困らないかな」という感じです。
なので、ミニマムな形としては以下で十分だと思います。
{
"type": "https://your-api-url.com/problems/article-not-found",
"title": "お探しの記事は見つかりませんでした"
}
type
は相対ぱすでも良いので、以下のような形でも大丈夫です。
{
"type": "/problems/article-not-found",
"title": "お探しの記事は見つかりませんでした"
}
さらに、「内部でしか使わないからAPIのエラーコードのドキュメンテーションをわざわざURLでアクセスするようにしないよ!」っていう方も多いと思うので、その場合は(RFC7807の仕様からは外れてしまいますが)以下のような形でも良いと思います。
{
"type": "article-not-found",
"title": "お探しの記事は見つかりませんでした"
}
異論は認めます。ぜひコメントを投げてください!
Railsにおける実装方法
さて、JSONの形が決まったところで、次はRailsで実装する方法を考えます。
実装方法1. 返すJSONベタがき
これはスケールしないのであまりおすすめしませんが、すごく小規模なアプリケーション、使い捨てのアプリケーション、なんらかの制約でとりあえず早くリリースしなければいけない場合などはありかなと思います。
class HogesController
before_action :authenticate!
def show
# ログインしてないと見れない情報を返す
end
private
# こういう共通で使いそうなメソッドは実際には親コントローラーやconcernに定義されてそうだが、
# 今回は簡単にするためにprivate methodとして定義する
def authenticate!
@user = # 認証するコード
return if @user
render json: { type: '/problems/authentication_required', title: 'ログインしてください' },
status: 401,
content_type: 'application/problem+json'
end
end
全てのコントローラーに継承される親コントローラーに、このようにJSONをとりあえずベタがきで書いて返します。
親コントローラーがない場合は、各コントローラーにinclude
されるモジュールでも良いと思います。
実装方法2. エラーごとにクラスを定義し、SerializerでJSONを組み立てる
1. エラーの親クラスを作成
まず、ApiExceptions::BaseException
と言う、エラークラスの親を作成します。
# app/models/api_exceptions/base_exception.rb
module ApiExceptions
class BaseException < StandardError
attr_reader :status_code, :type, :title
def initialize
raise NotImplementedError
end
end
end
2. 個別のエラークラスを作成
次に、個別のエラークラスを作成します。
# 例) 認証エラー
# app/models/api_exceptions/authentication_required.rb
module ApiExceptions
class AuthenticationRequired < BaseException
def initialize
@status_code = 401 # Unauthorized
@type = '/problems/authentication_required'
@title = 'ログインしたください'
end
end
end
3. 例外のSerializerを作成
エラークラスをJSONに変換するApiExceptionSerializer
を作成します。
今回の例では、ActiveModel::Serializerを使っていますが、他のSerializerでも同じことを実現できるはずです。
# app/serializers/api_exception_serializer.rb
class ApiExceptionSerializer < ActiveModel::Serializer
attributes :type, :title
end
4. 親コントローラーでBaseException
をrescue
する
# app/controllers/base_controller.rb
class BaseController < ActionController::API
rescue_from ApiExceptions::BaseException, with: :render_api_exception
private
def render_api_exception(exception)
render json: exception,
serializer: ApiExceptionSerializer,
status: exception.status_code,
content_type: 'application/problem+json'
end
end
5. 各コントローラーでraise
する
class HogesController < BaseController
before_action :authenticate!
def show
# ログインしてないと見れない情報を返す
end
private
def authenticate!
@user = # 認証するコード
raise ApiExceptions::AuthenticationRequired unless @user
# 認証なしでアクセスすると以下のJSONが返される
# {
# "type": "/problems/authentication_required",
# "title": "ログインしてください"
# }
end
end
このアプローチの何が嬉しいか
実装方法1.と比べると、
- 各コントローラーで書くのはエラーを
raise
するコードのみなので、コントローラーをスリムに保てる - エラーの種類に対応するクラスを
app/models/api_exceptions
配下に必ず作成しなければいけないので、どんなエラーの種類があるのかわかりやすい=ドキュメンテーションになる。 - JSONを生成する責務をコントローラーからSerializerに移せるので責務がいい感じに別れる
-
content_type: 'application/problem+json'
のような共通で返す必要があるJSONは一箇所に定義すれば済む
まとめ
- エラーで返すJSONはRFC7807に準拠するのが無難
- 最低限、エラーの種類を表す
type
と、エラー内容の説明をするtitle
は入れよう - その他は必要に応じて追加すれば良い
- 最低限、エラーの種類を表す
- エラーの種類ごとにクラスを定義するといい感じに書ける