Ruby on Rails のバージョン: 5.0, 5.1, 5.2
問題
アプリケーションサーバーに例外を検出して通知する仕組みを入れていると、ときどき余計な拡張子を付けたリクエストが来て、Missing template 関連のエラーが通知されます。重大なバグでもないのに、いちいち通知されるのをなんとかしたい。
とりあえずの結論
対策はあるけど、あまりすっきりしない。
Missing template 関連エラーの種類
0. エラーが起きない場合
viewsの下のテンプレートをまったく使わない場合は、エラーは出ません。
def show
render json: { message: @post.message }
end
1. views の下にフォーマットに対応したテンプレートがない
/posts/1.png
を要求されて、views/posts/show.png
がない場合は、 ActionController::UnknownFormat
( ... is missing a template for this request format and variant
... ) となります。
ただし、/posts/1.aaa
のようにRailsのMIMEリストにない拡張子の場合は、HTMLでレンダリングされます。
2. render メソッドを呼んだときに対応したテンプレートがない
コントローラのアクション内で render
メソッドを使っている場合は、/posts/1.png
を要求されたときに、1.とは違う例外 ActionView::MissingTemplate
が出ます。
def show
render 'another'
end
3. respond_to を使っているときに対応したテンプレートがない
respond_to
のブロック中にないフォーマットのリクエストが来たときは、ActionController::UnknownFormat
が発生します。
respond_to do |format|
format.html
format.json
end
Railsのデフォルトとrescue_from
Railsのproduction環境では、ActionController::UnknownFormat
例外が出た場合は、デフォルトで406 (Not Acceptable)を返します。JSONの場合は、ボディは {"status":406,"error":"Not Acceptable"}
、それ以外はボディが空になります。
しかし、次のように rescue_from
を書いていると、URLに拡張子を付けただけで通知が飛ぶ、ということになります。
if Rails.env.production?
rescue_from Exception, with: :rescue500
rescue_from ActionController::RoutingError, with: :rescue404
rescue_from ActiveRecord::RecordNotFound, with: :rescue404
end
def rescue500(e)
# ここでなんか通知飛ばす...
render file: 'errors/internal_server_error.html',
content_type: 'text/html',
status: :internal_server_error, layout: 'error'
end
対策1. rescue_from で406返す
rescue_from
に ActionController::UnknownFormat
と ActionView::MissingTemplate
を加えれば、Railsのデフォルトと似たようなことができます。
if Rails.env.production?
rescue_from Exception, with: :rescue500
rescue_from ActionController::UnknownFormat, with: :rescue406
rescue_from ActionView::MissingTemplate, with: :rescue406
rescue_from ActionController::RoutingError, with: :rescue404
rescue_from ActiveRecord::RecordNotFound, with: :rescue404
end
def rescue406(e)
respond_to do |format|
format.json {
render json: { status: 406, error: "Not Acceptable" }, status: :not_acceptable
}
format.all {
render body: nil, content_type: 'text/html', status: :not_acceptable
}
end
end
ただし、この方法だとコントローラであれこれ処理をしたあとで406を返すことになるので、何だか無駄な感じがします。即座に404を返したほうがいいような気もします。
対策2. routes.rb で constraints を使う
routes.rb で constraints
オプションを指定すれば、想定外のフォーマットをルーティングエラーにできます。
resources :posts, constraints: { format: 'html' }
constraints
は、get
や namespace
、scope
にも付けられます。また、constraints
メソッドでルーティングの設定を囲むこともできます。
「indexアクションではHTMLとJSONを返し、それ以外はHTMLだけ返す」みたいな場合はどうしましょう。 constraints do
で囲んで resources
を2回書けばいいですかね。
constraints format: 'html' do
resources :posts
resources :notifications
end
constraints format: 'json'
resources :posts, only: %i(index)
end
既存のアプリケーションのroutes.rbに constraints
を加えるのは、かなり面倒くさいです。対策1だけでいいんじゃないかなあ。
おまけ:エラーハンドラを書くときの注意
rescue_from
で指定したメソッド内でもMissing template エラーが出ることがあります。次はよくない例です。HTML以外のフォーマットだとエラーハンドラ内でさらにエラーが出ます。
def rescue404(e)
render 'errors/not_found', status: :not_found, layout: 'error'
end
何が来てもHTMLで404を返すには、次のようにします。
def rescue404(e)
render file: 'errors/not_found.html',
content_type: 'text/html', status: :not_found,
layout: 'error'
end
次の例は、JSONのときはJSONで404を返し、それ以外はHTMLで返します。format.html
ではなく format.all
を使います。
def rescue404(e)
respond_to do |format|
format.json { render json: {}, status: :not_found }
format.all { render file: 'errors/not_found.html',
content_type: 'text/html',
status: :not_found, layout: 'error' }
end
end