18
8

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 1 year has passed since last update.

Phoenixエラー処理①:Web CRUD基礎編(Prod環境構築付き)

Last updated at Posted at 2020-01-04

fukuoka.ex/kokura.exのpiacereです
ご覧いただいて、ありがとうございます :bow:

Phoenixのエラーハンドリングは、あまり文献も多く無くて、いざ実際のPJでいじろうとしたとき、どう手を付けていけば良いか、悩む方も多いのでは無いかと思います

そこで、Phoenixのエラー処理について、基礎的なところを解説しておきます

今回は、「phx.gen.html」で構築できる、Web CRUDに集中した基礎編です(他には、API CRUD基礎編やLiveView基礎編があります)

本コラムの検証環境

本コラムは、以下環境で検証しています(Windowsで実施していますが、Linuxやmacでも動作する想定です)

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にエンドポイント追加します

lib/either_web/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で表示します
image.png

このページのコード(コントローラ)は下記です

lib/either_web/controllers/post_controller.ex
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を再度アクセスすると、下記エラーデバッグ画面が表示されます
image.png

このエラーデバッグ画面は、下記に定義されています

deps/plug/lib/plug/templates/debugger.html.eex
<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

Prod環境では、下記エラーが表示されます
image.png

このエラー画面は、下記に定義されています

deps/plug/lib/plug/conn/status.ex
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

エラー画面も下記デバッグ表示に戻ったことを、ブラウザをリロードして確認します
image.png

ここから、dev.exsのdebug_errorsを無効にします

config/dev.exs
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環境と同じエラー画面になります
image.png

こうすることで、dev環境とProd環境で同様のエラー処理を共有できます

Phoenixのエラー処理カスタマイズ

Phoenixのエラー処理は、以下2つの方法で、カスタマイズできます

  • ErrorViewでエラー画面レンダリング関数を追加
  • FallbackControllerの追加

①ErrorViewでエラー画面レンダリング関数を追加

ErrorViewを使えば、ステータスコード毎のエラー画面レンダリングを、関数パターンマッチで追加できます

カスタマイズ前のErrorViewは、以下の通りです

lib/either_web/views/error_view.ex ※カスタマイズ前
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

これを下記のように書き換えます

lib/either_web/views/error_view.ex ※カスタマイズ後
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

ブラウザをリロードすると、下記のように、カスタマイズされたエラー画面が表示されます
image.png

エラー詳細は、assigns内に全て入っているので、関数パターンマッチ、もしくは関数内でエラー内容に応じてエラー画面を切り替えることもできます(recompileで反映できます)

lib/either_web/views/error_view.ex ※テーブル未存在エラーは関数パターンマッチで切り替え
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しています)

lib/either_web/controllers/fallback_controller.ex
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で囲む必要があります

lib/either_web/controllers/post_controller.ex
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エラー画面が表示されました
image.png

なお、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)

lib/either_web/controllers/fallback_controller.ex
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」エラー画面に切り替わりました
image.png

エラーハンドリングの優先順位など

なお、ErrorViewとFallbackControllerが同時に設定されている場合、FallbackControllerが勝ちます

ただし、FallbackControllerでマッチする関数が無かった場合や、エラーを起こした場合は、ErrorView側が呼び出されます

終わり

ここまでが、Phoenixにおけるエラーハンドリングの基本になります

ここから、実際のPJでエラー処理を作り込んでいく場合、以下のようなトピックが出てきます

  • エラー画面の高度なカスタマイズ(専用ViewによるHTMLテンプレート化)
  • ステータスコードやアプリ固有エラーの網羅
  • try/rescueの扱い方(≒アプリ全体のエラー設計)

これらについては、別途コラム化する予定です

p.s.「いいね」よろしくお願いします

ページ左上のimage.pngimage.png のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください!:tada:

18
8
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
18
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?