Edited at

Rails: Missing template エラーの扱い

More than 1 year has passed since last update.

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_fromActionController::UnknownFormatActionView::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 は、getnamespacescope にも付けられます。また、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