Chapter 29

カスタムエラーページ

koga1020
koga1020
2021.11.23に更新

カスタムエラーページ

Phoenixには ErrorView というビューがあり、 lib/hello_web/views/error_view.ex にあります。この ErrorView の目的は、一般的な方法で一元的にエラーを処理することです。

ErrorView

新しいアプリケーションの場合、ErrorView は次のようになります。

defmodule HelloWeb.ErrorView do
  use HelloWeb, :view

  # If you want to customize a particular status code
  # for a certain format, you may uncomment below.
  # def render("500.html", _assigns) do
  #   "Internal Server Error"
  # end

  # By default, Phoenix returns the status message from
  # the template name. For example, "404.html" becomes
  # "Not Found".
  def template_not_found(template, _assigns) do
    Phoenix.Controller.status_message_from_template(template)
  end
end

これに飛び込む前に、レンダリングされた 404 Not Found メッセージがブラウザ上でどのように見えるか見てみましょう。開発環境では、Phoenixはデフォルトでエラーをデバッグし、非常に有益なデバッグページを表示します。しかし、ここで私たちが知りたいのは、アプリケーションが本番環境でどのようなページを表示するのかを見ることです。そのためには、config/dev.exsdebug_errors: false を設定する必要があります。

import Config

config :hello, HelloWeb.Endpoint,
  http: [port: 4000],
  debug_errors: false,
  code_reloader: true,
  . . .

設定ファイルを変更した後、この変更を有効にするにはサーバーを再起動する必要があります。サーバーを再起動した後、ローカルアプリケーションのhttp://localhost:4000/such/a/wrong/pathにアクセスして、何が得られるか見てみましょう。

さて、これはあまりエキサイティングではありません。マークアップもスタイリングもせずに、"Not Found" という文字列が表示されます。

最初の質問は、そのエラー文字列はどこから来ているのかということです。答えは ErrorView の中にあります。

def template_not_found(template, _assigns) do
  Phoenix.Controller.status_message_from_template(template)
end

良いですね。template_not_found/2 関数はテンプレートと assigns マップを受け取りますが、assigns は無視します。template_not_found/2 は、Phoenix.View がテンプレートをレンダリングしようとしてもテンプレートが見つからない場合に呼び出されます。

つまり、カスタムエラーページを提供するために、HelloWeb.ErrorView の中に適切な render/2 関数を定義できます。

  def render("404.html", _assigns) do
    "Page Not Found"
  end

しかし、もっと良い方法があります。

Phoenixは ErrorView を生成してくれますが、lib/hello_web/templates/error ディレクトリは与えてくれません。それでは、ディレクトリを作成してみましょう。新しいディレクトリの中に、404.html.heex という名前のテンプレートを追加して、アプリケーションのレイアウトと、ユーザーへのメッセージを書いた新しい <div> を混ぜたマークアップを追加します。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Welcome to Phoenix!</title>
    <link rel="stylesheet" href="/css/app.css"/>
    <script defer type="text/javascript" src="/js/app.js"></script>
  </head>
  <body>
    <header>
      <section class="container">
        <nav>
          <ul>
            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
          </ul>
        </nav>
        <a href="https://phoenixframework.org/" class="phx-logo">
          <img src="/images/phoenix.png" alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <main class="container">
      <section class="phx-hero">
        <p>Sorry, the page you are looking for does not exist.</p>
      </section>
    </main>
  </body>
</html>

テンプレートファイルを定義した後は、そのテンプレートに相当する render/2 を削除することを忘れないでください。先に lib/hello_web/views/error_view.ex で導入した "404.html" のマッチを削除しましょう。

- def render("404.html", _assigns) do
-  "Page Not Found"
- end

これでhttp://localhost:4000/such/a/wrong/pathに戻ってみると、ずっときれいなエラーページが表示されているはずです。ここで注目すべきは、エラーページをサイトの他の部分と同じルックアンドフィールにしたいにもかかわらず、アプリケーションのレイアウトを通して 404.html.heex テンプレートをレンダリングしていないことです。これは循環したエラーを避けるためです。たとえば、レイアウトのエラーが原因でアプリケーションが失敗した場合はどうなるでしょうか。再度レイアウトをレンダリングしようとすると、別のエラーが発生してしまいます。そのため、エラーテンプレートの依存関係やロジックの量を最小限にし、必要なものだけを共有するのが理想的です。

カスタムの例外

Elixirには、カスタム例外を定義するための defexception/1 というマクロがあります。例外は構造体として表現され、構造体はモジュール内で定義する必要があります。

カスタム例外を作成するためには、新しいモジュールを定義する必要があります。通常、このモジュールの名前には "Error" が付けられます。そのモジュールの中で、新しい例外を defexception/1 で定義する必要があり、ファイル lib/hello_web.ex がその場所として適しているようです。

defmodule HelloWeb.SomethingNotFoundError do
  defexception [:message]
end

このように新しい例外を発生させることができます。

raise HelloWeb.SomethingNotFoundError, "oops"

デフォルトでは、PlugとPhoenixはすべての例外を500のエラーとして扱います。しかし、プラグは Plug.Exception というプロトコルを提供しています。このプロトコルでは、ステータスをカスタマイズしたり、例外構造体がデバッグエラーページに返すアクションを追加したりできます。

もし、HelloWeb.SomethingNotFound のエラーに対して404のステータスを提供したい場合は、lib/hello_web.ex に対して次のような Plug.Exception プロトコルの実装を定義することで可能になります。

defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
  def status(_exception), do: 404
  def actions(_exception), do: []
end

あるいは、例外構造体の中で plug_status フィールドを直接定義することもできます。

defmodule HelloWeb.SomethingNotFoundError do
  defexception [:message, plug_status: 404]
end

しかし、アクション可能なエラーを提供する場合など、Plug.Exception プロトコルを手作業で実装しておくと便利な場合があります。

アクション可能なエラー

例外アクションとは、エラーページでトリガーできる機能のことで、基本的には、実行される labelhandler を定義したマップのリストです。

これらはボタンの集合体としてエラーページに表示され、以下のようなフォーマットに従います。

[
  %{
    label: String.t(),
    handler: {module(), function :: atom(), args :: []}
  }
]

HelloWeb.SomethingNotFoundError に対して何らかのアクションを返したい場合は、次のように Plug.Exception を実装します。

defimpl Plug.Exception, for: HelloWeb.SomethingNotFoundError do
  def status(_exception), do: 404

  def actions(_exception) do
    [
      %{
        label: "Run seeds",
        handler: {Code, :eval_file, "priv/repo/seeds.exs"}
      }
    ]
  end
end