pockestrap

Web Programmer's memo

Railsで、Layoutを適用した静的なエラーページを生成してみた

こんにちは。私は現在 bearfruits という GitHub と連携した就活支援Webサービスを作成、運営しています。

bearfruits はRuby on Railsで開発しています。 エラーページがRailsデフォルトのままだったのを改善したので、手順をまとめます。

環境

目標

  • 静的なエラーページを作成する
    • public/404.html など
  • 静的だけど layout は適用する。
    • app/views/layouts/application.html.erb
  • DRY にする
    • application.html.erbpublic/下での layout の二重管理はやらない

目標に至るまで

(急いでいたら次の「生成方法」まで読み飛ばしても構いません)

前述した通り、bearfruitsでは今までエラーページがRailsデフォルトのままでした。

f:id:Pocke:20160112111145p:plain

「これはまずい!」と言うことで、エラーページをきちんと作ることにしました。

第一段階 Rack の exceptions_app を設定する

上記Qiitaの記事のコメントが参考になります。

  • エラーハンドリング用のControllerを作成し
  • その中でエラー処理を書いて
  • そのControllerをexceptions_appに登録する

と言うことをしています。

まずこの方針で実装を進めたのですが、どう考えてもめんどくさいです。
めんどくさいのは嫌いなので解決策を調べていたところ、次のGemを見つけました。

第二段階 Rambulance Gem を使う

Rambulance は、エラーハンドリングをいい感じにやってくれるGemです(多分(結局使わなかったので断言は出来ない…))。

  • 例外のクラスと、HTTPのステータスコードを指定しておけばいい感じにエラーページが出せる
  • エラーページにlayoutもつけられる

これはよさそうだと思ったのですが、調べていくうちに「エラーページを動的に生成すること」には問題点があることが分かってきました。

特に大きいと感じたのは、「Railsサーバーが止まっているとエラーページすら返せない」ということです。
Railsサーバーが死んでいても手前のNginxなどが生きていれば適切にエラーページを返したいですね。

第三段階 静的にエラーページを作成する

上記の問題を解決するには、public/下に存在する404.htmlなどを直接編集すればよいです。
public/下をNginxなどでドキュメントルートに指定しておけば、Railsサーバーが落ちていてもエラーページを表示することが出来ます。

ですが、静的ファイルを使用するということは、layoutを適用できないと言うことです。
layout を適用するには二重管理が必要になってしまいます。

そのため、「静的なエラーページを生成する」という手段を取ることにしました。

生成方法

以下の2つのファイルを用意します。

  • lib/tasks/error_page.rake
namespace :error_page do
  desc 'generate error pages'
  task gen: :environment do
    re = /^error_(\d+)$/
    ErrorsController.instance_methods.map(&:to_s).select{|m|m =~ re}.each do |action|
      code = action[re, 1]
      fpath = Rails.root.join('public', "#{code}.html")
      html  = ErrorsController.render(action).gsub(/^\s*<meta name="csrf-token".+$/, "")

      File.write(fpath, html)
    end
  end
end
  • app/controllers/errors_controller.rb
class ErrorsController < ApplicationController
  # Devise を使っている場合、current_userを上書きしないと怒られた
  def current_user
    return nil
  end

  def error_404;end
  def error_500;end
  def error_422;end
end

そして、app/views/errors/下に、以下の3つのファイルを用意します。
内容はそれぞれのエラーの際に表示したいページのHTMLです。

また、backport_new_renderer gem をインストールします。

Rails 5からrenderメソッドをControllerの外で使えるようになりました。
このGemは、その機能のRails 4バックポート用です。

gem 'backport_new_renderer'

そして、以下のコマンドを実行することでpublic/*.htmlが生成されます。

$ bundle exec rake error_page:gen

また、production環境用のエラーページを生成したい場合はRAILS_ENV=productionをつけて下さい。

しくみ

ErrorsControllerを作成し、各ステータスコードに対応するアクションを作成します。
そして、そのアクションをRake task内から実行し、その結果をpublic/下に書き出しています。

また、ErrorsControllerに対するroutingは書いていないため、アプリケーション側からこのコントローラーが使われることはありません。

他やったこと

gitignore

public/*.htmlは、RAILS_ENV毎に生成されるものが変わる(はず)なので、gitの管理から外しました。

  • .gitignore
/public/404.html
/public/422.html
/public/500.html

そして、上記のRake task実行をデプロイ手順に組み込むようにしました。

Controllerから404を返す

通常のコントローラーから404エラーを返したい時の為に、以下のようなメソッドを用意しました。

  • app/controllers/application_controller.rb
def render_404
  render file: Rails.root.join('public', '404.html'), status: 404, layout: nil
end

render_404メソッドを404を返したいところから呼び出すことで、作成した404ページを返すことが出来ます。
また、他のステータスも同様に返すことが出来ます。

error layout の作成

エラーページはそれぞれ似たようなものになっていました。
そのため、実際にはapplication layout をそのまま使うのではなく、それを継承したerrors layout を作成し、それを使用するようにしました。

上記記事を参考に、入れ子レイアウトを作成しました。

課題

他の assets に依存している

現状では、layout にスタイルシートや画像のsrcを書いていた場合、それをそのまま使用しています。
つまり、スタイルシート等をNginxがサーブできない状態の場合、エラーページでもそれらを表示できません。

解決策としてはスタイルシートや画像をエラーページにインラインで埋め込んでしまう方法が考えられます。
この方法が取れるか調べて試してみようと思っています。

まとめ

静的にエラーページをサーブする場合の一つの手段として有用ではないでしょうか。
また、やろうと思えば Rambulance などの Gem との連携も可能だと考えています(多分…)。