28
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails: Missing template エラーの扱い

Last updated at Posted at 2018-11-06

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
28
30
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
30

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?