fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます
Phoenixのエラーハンドリングは、あまり文献も多く無くて、いざ実際のPJでいじろうとしたとき、どう手を付けていけば良いか、悩む方も多いのでは無いかと思います
そこで、Phoenixのエラー処理について、基礎的なところを解説しておきます
今回は、「phx.gen.html」で構築できる、Web CRUDに集中した基礎編です(他には、API CRUD基礎編やLiveView基礎編があります)
本コラムの検証環境
本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)
- Windows 10
- Elixir 1.9.1 ※最新版のインストール手順はコチラ
- Phoenix 1.4.11 ※最新版のインストール手順はコチラ
- PostgreSQL 12.1 ※最新版のインストール手順はコチラ
Phoenixデフォルト状態のエラー処理
まずは、Phoenixデフォルト状態のエラー処理を確認します
PhoenixプロジェクトをDB利用有で作るところからです
mix phx.new either
Fetch and install dependencies? [Yn] y
cd either
mix ecto.create
mix phx.gen.html Api Post posts title:string body:text
router.exにエンドポイント追加します
defmodule EitherWeb.Router do
…
scope "/", EitherWeb do
pipe_through :browser
get "/", PageController, :index
resources "/posts", PostController # <- add here
end
…
マイグレートし、Phoenixを起動します
mix ecto.migrate
iex -S mix phx.server
マイグレートしたページを、ブラウザからhttp://localhost:4000/posts
で表示します
このページのコード(コントローラ)は下記です
defmodule EitherWeb.PostController do
…
def index(conn, _params) do
posts = Api.list_posts()
render(conn, "index.html", posts: posts)
end
…
このコントローラは、DBアクセスをしているので、テーブルをリネームすると、エラーを起こします
psqlから下記でテーブルをリネームしてみましょう
\c either_dev
alter table posts rename to ppposts;
エラー表示は、dev環境とProd環境で異なります
dev環境でのエラー表示/定義
ブラウザでhttp://localhost:4000/posts
を再度アクセスすると、下記エラーデバッグ画面が表示されます
このエラーデバッグ画面は、下記に定義されています
…
<body>
<div class="top-details">
<%= if @style.logo do %>
<aside class="exception-logo"></aside>
<% end %>
<header class="exception-info">
<% [headline | details] = String.split(@message, "\n\n") %>
<h5 class="struct">
<%= h @title %>
<small>at <%= h method(@conn) %></small>
<small class="path"><%= h @conn.request_path %></small>
</h5>
<h1 class="title"><%= h headline %></h1>
<%= for detail <- details do %>
<p class="detail"><%= h detail %></p>
<% end %>
</header>
<%= if @banner do %>
</div>
<div class="banner"><%= @banner %></div>
<div class="top-details">
<% end %>
…
上記ファイルを変更し、下記コマンドでビルドし直せば、この画面を直接変更することもできます(なお、通常は後述する方法で変更するので、この方法は使いません)
mix deps.compile
デバッグ画面のスタイルのカスタマイズをしたい場合は、@ngmr_mo さんの下記コラムが役立つと思います
Plug.ErrorHandlerとPlug.DebuggerでPhoenixの標準のエラーをカスタマイズする
https://qiita.com/ngmr_mo/items/7d03ac5d616aa8a42488
Prod環境でのエラー表示/定義
Prod環境への切り替えは、Ctrl+Cで抜けた後、以下で行います
set MIX_ENV=Prod
set DATABASE_URL=ecto://postgres:postgres@localhost/either_dev
set SECRET_KEY_BASE=dummy
mix phx.gen.secret
fnVgWnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
set SECRET_KEY_BASE=fnVgWnXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # 上記シークレットを貼り付け
iex -S mix phx.server
このエラー画面は、下記に定義されています
defmodule Plug.Conn.Status do
…
statuses = %{
100 => "Continue",
101 => "Switching Protocols",
…
500 => "Internal Server Error",
…
上記ファイルを変更し、mix deps.compileでビルドし直せば、やはりこの画面を直接変更できますが、通常は、この方法は使いません
dev環境のエラーデバッグを解除する
Ctrl+Cで抜けた後、以下でdev環境に戻します
set MIX_ENV=
iex -S mix phx.server
エラー画面も下記デバッグ表示に戻ったことを、ブラウザをリロードして確認します
ここから、dev.exsのdebug_errorsを無効にします
use Mix.Config
…
config :either, EitherWeb.Endpoint,
http: [port: 4000],
debug_errors: false, # <- change here from true
…
Ctrl+Cで抜けて、Phoenixを再起動します
iex -S mix phx.server
ブラウザをリロードすると、Prod環境と同じエラー画面になります
こうすることで、dev環境とProd環境で同様のエラー処理を共有できます
Phoenixのエラー処理カスタマイズ
Phoenixのエラー処理は、以下2つの方法で、カスタマイズできます
- ErrorViewでエラー画面レンダリング関数を追加
- FallbackControllerの追加
①ErrorViewでエラー画面レンダリング関数を追加
ErrorViewを使えば、ステータスコード毎のエラー画面レンダリングを、関数パターンマッチで追加できます
カスタマイズ前のErrorViewは、以下の通りです
defmodule EitherWeb.ErrorView do
use EitherWeb, :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
これを下記のように書き換えます
defmodule EitherWeb.ErrorView do
use EitherWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
def render( "500.html", assigns ) do
raw( "<h1>こんな感じでカスタマイズできます</h1><h2>raw()を使えば生HTMLも使えます</h2><code>#{ inspect( assigns ) }</code>" )
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
deps配下では無い通常のソースコード変更なので、recompileで反映できます
iex> recompile
ブラウザをリロードすると、下記のように、カスタマイズされたエラー画面が表示されます
エラー詳細は、assigns内に全て入っているので、関数パターンマッチ、もしくは関数内でエラー内容に応じてエラー画面を切り替えることもできます(recompileで反映できます)
defmodule EitherWeb.ErrorView do
use EitherWeb, :view
# If you want to customize a particular status code
# for a certain format, you may uncomment below.
def render( "500.html", %{ kind: :error, reason: %Postgrex.Error{ postgres: %{ code: :undefined_table } } } ) do
"テーブル未存在"
end
def render( "500.html", assigns ) do
raw( "<h1>こんな感じでカスタマイズできます</h1><h2>raw()を使えば生HTMLも使えます</h2><code>#{ inspect( assigns ) }</code>" )
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
②FallbackControllerを追加
FallbackControllerを追加しておくと、コントローラでrender()が行われなかった場合、FallbackController.call()が呼ばれるため、これを使って、エラー処理ができます
まず、FallbackControllerを追加します(エラーを見分けやすくするため、故意に400エラーとしています)
エラー詳細は、ErrorView同様、call()の第2引数にしているassigns内に全て入っています(ここでは、IO.inspectしています)
defmodule EitherWeb.FallbackController do
use EitherWeb, :controller
alias EitherWeb.ErrorView
def call( conn, assigns ) do
IO.puts "=================================="
IO.inspect assigns
IO.puts "=================================="
status = 400
conn
|> put_status( status )
|> put_view( ErrorView )
|> render( "#{ status }.html" )
end
end
それから、コントローラ側にFallbackControllerをaction_fallback()で紐付けます
なお、テーブル未存在時は、Api.list_posts()が例外を吐くため、try/rescueで囲む必要があります
defmodule EitherWeb.PostController do
use EitherWeb, :controller
alias EitherWeb.ErrorView
alias Either.Api
alias Either.Api.Post
action_fallback( EitherWeb.FallbackController ) # <- add here
def index(conn, _params) do
try do
posts = Api.list_posts()
render(conn, "index.html", posts: posts)
rescue
e -> e
end
end
…
recompileして、リロードすると、FallbackControllerで設定した400エラー画面が表示されました
なお、IO.inspectしているエラー詳細は、コンソール側に出ます
iex> ==================================
iex> %Postgrex.Error{
connection_id: 3892,
message: nil,
postgres: %{
code: :undefined_table,
file: "X:\\pginstaller_12.auto\\postgres.windows-x64\\src\\backend\\parser\\parse_relation.c",
line: "1194",
message: "リレーション\"posts\"は存在しません",
pg_code: "42P01",
position: "79",
routine: "parserOpenTable",
severity: "ERROR",
unknown: "ERROR"
},
query: "SELECT p0.\"id\", p0.\"body\", p0.\"title\", p0.\"inserted_at\", p0.\"updated_at\" FROM \"posts\" AS p0"
}
iex> ==================================
FallbackControllerも、エラー内容に応じて、関数パターンマッチ、もしくは関数内でエラー画面を切り替えれるのはErrorView同様です
テーブル未存在時は、昨年末、Go界隈やNode.js界隈でザワザワしていた、ステータスコード418「I’m a teapot」エラー画面を返すようにしてみましょう(あくまで例ですよw)
defmodule EitherWeb.FallbackController do
use EitherWeb, :controller
alias EitherWeb.ErrorView
def call( conn, %Postgrex.Error{ postgres: %{ code: :undefined_table } } ) do
status = 418
conn
|> put_status( status )
|> put_view( ErrorView )
|> render( "#{ status }.html" )
end
def call( conn, assigns ) do
IO.puts "=================================="
IO.inspect assigns
IO.puts "=================================="
status = 400
conn
|> put_status( status )
|> put_view( ErrorView )
|> render( "#{ status }.html" )
end
end
recompileして、リロードすると、「I’m a teapot」エラー画面に切り替わりました
エラーハンドリングの優先順位など
なお、ErrorViewとFallbackControllerが同時に設定されている場合、FallbackControllerが勝ちます
ただし、FallbackControllerでマッチする関数が無かった場合や、エラーを起こした場合は、ErrorView側が呼び出されます
終わり
ここまでが、Phoenixにおけるエラーハンドリングの基本になります
ここから、実際のPJでエラー処理を作り込んでいく場合、以下のようなトピックが出てきます
- エラー画面の高度なカスタマイズ(専用ViewによるHTMLテンプレート化)
- ステータスコードやアプリ固有エラーの網羅
- try/rescueの扱い方(≒アプリ全体のエラー設計)
これらについては、別途コラム化する予定です
p.s.「いいね」よろしくお願いします
ページ左上のや
のクリックを、どうぞよろしくお願いします
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!