エラーページをAsset Pipelineで管理する

はじめに

Railsでエラーページをいかに楽に書いて管理するかを模索した結果、Asset Pipelineを使う方法が良さそうだったのでまとめます。

追記(2015/08/17)

このエントリで提案した手法をgem化し、その使い方を解説したエントリを書きました。
よろしければこちらもご一読ください。

以下、元エントリになります。

背景

Railsproduction環境などでエラーが発生するとpublicディレクトリ下の404.htmlなどを表示するという仕組みを標準で用意しています。
単純でとてもいいと思うのですが、ただのhtmlなので以下のような辛みがあります。

  1. テンプレートエンジンが使えない
  2. public/assets以下のファイルを利用できない
  3. ヘッダーやフッターを共有(部分テンプレートが利用)できない

これらの問題を解決策のひとつとして、config.exceptions_appを使ってごにょごにょすることで、動的にエラーページを生成して返すという方法があります。
詳しくは以下の投稿(とコメント)を参照して下さい。

動的生成の限界

@keenさんが指摘するように、動的にエラーページを生成する手法では、メンテナンス時の503ページなどに対応できないという限界があります。

そこで本エントリでは、共通のヘッダーやフッターを使うのを諦める代わりに、エラーページを静的ファイルとしてAsset Pipelineで管理する方法を提案します。

提案手法

提案手法の流れは次の通りです。

  1. 標準の静的エラーページを削除
  2. 使用するテンプレートエンジンを登録する
  3. app/assets/templatesのようなディレクトリを作り、そこにエラーページを書く
  4. assets:precompileをフックして、実行後にエラーページをpublic配下に移す

以下、順番に説明していきます。

使用するテンプレートエンジンを登録する

例えばslimを使いたい場合、config/initializers/assets.rbに次の行を追加します。

Rails.application.assets.register_engine('.slim', Slim::Template)

app/assets/templatesにエラーページを書く

別にディレクトリ名はなんでもいいんですが、僕はapp/assets/templatesという名前にしました。
このディレクトリに次のようなエラーページを書いておきます。

doctype html
html
  head
    title
      | TestApp
    = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': true
    = javascript_include_tag 'application', 'data-turbolinks-track': true
  body
    = image_tag('404.png')

assets:precompile実行後にエラーページをpublicディレクトリ下に移す

lib/tasks/assets.rakeのようなファイルを作成して、assets:precompileの実行後にエラーページをpublicディレクトリ下に移す処理を書きます。

Rake::Task['assets:precompile'].enhance do
  logger = Logger.new(STDOUT)

  %i(404 422 500 503).each do |status_code|
    pattern = Rails.public_path.join('assets', "#{status_code}-*.html")
    src = Dir.glob(pattern).sort_by { |path| File.mtime(path) }.last
    next if src.nil?

    dest = Rails.public_path.join("#{status_code}.html")
    logger.info("mv #{src} to #{dest}")

    FileUtils.mv(src, dest)
  end
end

使い方

app/assets/templates以下をバージョン管理するようにして、デプロイ時にrake assets:precompileを実行するようにすればOK。

デメリット

上でも述べたように、(renderメソッドが使えないので)ヘッダーやフッターのような共通のテンプレートを読み込むことができないことに加え、修正時に表示を確認するのが面倒くさい*1、というところでしょうか。
後者ですが、エラーページはそこまで頻繁に修正されることはないと思うので、許容範囲なのかなあと思っています。

おわりに

これをgemに組み込んで、各microserviceでエラーページを共通化するというのが本当にやりたかったことだったりする。

参考

*1:railsを開発環境で立ち上げて/assets以下にアクセスするだけなんだけど、それをデザイナーさんにやってもらうときの手間を考えると…って話です。