Phoenixで作成したAPIへのAjaxリクエストにエラーを返す方法


PhoenixFrameworkとフロントエンドフレームワークを併用している場合、
`mix phoenix.gen.json`でAPI作成してフロントからAjaxリクエストしてJSONを取得するという使い方をするのが主だと思います。
本記事はそんなAjaxリクエストに対して「ある条件の時にエラーを返したい・・・」となった時のTipsになります。

## やり方

Ajaxリクエストでは特定のHTTPステータスコードが返ってきた場合にエラーになります。
どのHTTPステータスコードがエラーになるかは以下が参考になります。
[jQueryのajaxが，HTTPステータスコードの何番がsuccessで，何番がerrorなのか調べました](http://blog.naru.se/2012/11/jqueryajaxhttpsuccesserror.html)

ということはPhoenix側でエラーになるHTTPステータスコードを送ってあげればよいのです。

それではまずは`mix phoenix.gen.json`で生成されたコントローラを見ていきましょう。

````elixir:user_controller.ex
defmodule Test.UserController do
  use Test.Web, :controller

  alias Test.User

  plug :scrub_params, "user" when action in [:create, :update]

  def index(conn, _params) do
    users = Repo.all(User)
    render(conn, "index.json", users: users)
  end 

  def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)

    case Repo.insert(changeset) do
      {:ok, user} ->
        conn
        |> put_status(:created)
        |> put_resp_header("location", user_path(conn, :show, user))
        |> render("show.json", user: user)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Test.ChangesetView, "error.json", changeset: changeset)
    end 
  end 

  def show(conn, %{"id" => id}) do
    user = Repo.get!(User, id) 
    render conn, "show.json", user: user
  end 

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id) 
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        render(conn, "show.json", user: user)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Test.ChangesetView, "error.json", changeset: changeset)
    end 
  end 

  def delete(conn, %{"id" => id}) do
    user = Repo.get!(User, id) 

    # Here we use delete! (with a bang) because we expect
    # it to always work (and if it does not, it will raise).
    Repo.delete!(user)

    send_resp(conn, :no_content, "") 
  end 
end
````

- `index`
- `create`
- `show`
- `update`
- `delete`

５つの関数が存在しますね。
今回は`show`に対してAjaxリクエストを行った場合に、問答無用でエラーになるようにしましょう。

````elixir:user_controller.ex
  def show(conn, %{"id" => id}) do
    user = Repo.get!(User, id) 
    render conn, "show.json", user: user
  end 
````

この`show`の中身は２つの要素で出来ています。

- 引数の`id`に合致するユーザの取得
- ユーザ情報を`show.json`のテンプレートに当てはめて返す

それではこれをHTTPステータスコードの500を返すようにしてみましょう。

````elixir:user_controller.ex
  def show(conn, %{"id" => id}) do
  	conn
  	|> put_status(500)
    # user = Repo.get!(User, id) 
    # render conn, "show.json", user: user
  end 
````

これで一旦500が返るようになりました。
ただこれだけでは不十分で大抵の場合、「何故エラーになったのか？」が必要なはずです。
次にJSON形式でエラーになった原因を送るようにします。

この時にキモとなるのが`render conn, "show.json", user: user`です。
この一文は「`show.json`という形式に`user: user`を引数とした結果をレンダリングする」という意味になります。

ではこの`show.json`はどこで定義されているのでしょうか？
答えは`views`ディレクトリ内のviewファイルにありました。

````elixir:user_view.ex
defmodule Test.UserView do
  use Test.Web, :view

  def render("index.json", %{users: users}) do
    %{data: render_many(users, Test.UserView, "user.json")}
  end 

  # Showで使っているのはコレ！
  def render("show.json", %{user: user}) do
    %{data: render_one(user, Test.UserView, "user.json")}
  end 

  def render("user.json", %{user: user}) do
    %{id: user.id,
      email: user.email,
      name: user.name,
      age: user.age,
      info: user.info}
  end 
end
````

見て分かるように`show.json`の定義がありますね。
今回はUser情報ではなくエラー情報を送りたいので、新しく`error.json`を定義しましょう。

````elixir:user_view.ex
defmodule Test.UserView do
  use Test.Web, :view

  def render("index.json", %{users: users}) do
    %{data: render_many(users, Test.UserView, "user.json")}
  end 

  # Showで使っているのはコレ！
  #def render("show.json", %{user: user}) do
  #  %{data: render_one(user, Test.UserView, "user.json")}
  #end 
  
  # 追加
  def render("error.json", %{error: error}) do
    %{error: error}
  end

  def render("user.json", %{user: user}) do
    %{id: user.id,
      email: user.email,
      name: user.name,
      age: user.age,
      info: user.info}
  end 
end
````

これで引数に設定された`%{error: error}`をそのまま返すような定義が出来ました。
次にコントローラーから呼び出してみましょう。

````elixir:user_controller.ex
  def show(conn, %{"id" => id}) do
  	conn
  	|> put_status(500)
  	|> render("error.json", error: "TEST ERROR")
    # user = Repo.get!(User, id) 
    # render conn, "show.json", user: user
  end 
````

これでAjaxリクエストにエラーで返り、さらに`error`の中身を見ればエラー内容が記述されているはずです。
このように

- `put_status`でHTTPステータスコードを設定する
- レンダリングするJSONを定義し、エラー内容を引数として渡す

といった方法でエラーにすることが出来ます。