Problem with phoenix and ErrorHelpers

Hi everyone,

I am trying to use Ecto.insert, and i have the following error :

[error] #PID<0.416.0> running GORprojectWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: POST /api/rooms
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in GORprojectWeb.ErrorHelpers.translate_error/1
        (gor_project) lib/gor_project_web/views/error_helpers.ex:9: GORprojectWeb.ErrorHelpers.translate_error("is invalid")
        (ecto) lib/ecto/changeset.ex:2345: anonymous fn/3 in Ecto.Changeset.merge_error_keys/3
        (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ecto) lib/ecto/changeset.ex:2339: Ecto.Changeset.traverse_errors/2
        (gor_project) lib/gor_project_web/views/changeset_view.ex:17: GORprojectWeb.ChangesetView.render/2
        (phoenix) lib/phoenix/view.ex:332: Phoenix.View.render_to_iodata/3
        (phoenix) lib/phoenix/controller.ex:740: Phoenix.Controller.do_render/4
        (gor_project) lib/gor_project_web/controllers/room_controller.ex:1: GORprojectWeb.RoomController.action/2
        (gor_project) lib/gor_project_web/controllers/room_controller.ex:1: GORprojectWeb.RoomController.phoenix_controller_pipeline/2
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.plug_builder_call/2
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/giom/Documents/develop/web/GORProject-API/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

After some investigations, it seems that the error comes from an association not loaded, but the errors kind of triggers me because i have in my errors.pot the strings to handle this error :

## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""

So i should be handled.

Here are my schemas, CRUD and controller :

Schema :

 defmodule GORproject.Rooms.Room do
  use Ecto.Schema
  import Ecto.Changeset

  alias GORproject.Rooms.Room
  alias GORproject.Auth.User

  schema "rooms" do
    field(:depth, :integer)
    field(:name, :string)
    field(:description, :string)
    field(:public, :boolean)
    many_to_many(:users, User, join_through: "users_rooms")
    has_many(:children, Room)
    belongs_to(:father, Room, foreign_key: :father_id)

    timestamps()
  end

  @doc false
  def changeset(room, attrs) do
    room
    |> cast(attrs, [:name, :depth, :description, :public, :father_id])
    |> foreign_key_constraint(:father_id)
    |> validate_required([:name, :depth, :public])
  end
end

CRUD :

  def create_room(attrs \\ %{}) do
    %Room{children: [], father: {}, users: []}
    |> Room.changeset(attrs)
    |> Repo.insert()
  end

Controller :

  def create(conn, %{"room" => room_params}) do
    with {:ok, %Room{} = room} <- Rooms.create_room(room_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", room_path(conn, :create, room))
      |> render("show.json", room: room)
    end
  end

Thank you :slight_smile:

How you handling in create method if changeset is not valid?

error is sent to my fallbackController :

defmodule GORprojectWeb.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.

  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use GORprojectWeb, :controller

  def call(conn, {:error, :bad_request}) do
    conn
    |> put_status(:bad_request)
    |> render(GORprojectWeb.ErrorView, :"400")
  end

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> render(GORprojectWeb.ErrorView, :"404")
  end

  def call(conn, {:error, :bad_params}) do
    conn
    |> put_status(:bad_request)
    |> render(GORprojectWeb.ErrorView, :custom, message: "Bad request parameters")
  end

  def call(conn, {:error, :bad_uuid}) do
    conn
    |> put_status(:bad_request)
    |> render(GORprojectWeb.ErrorView, :custom, message: "Bad uuid provided")
  end

  def call(conn, {:error, :bad_id}) do
    conn
    |> put_status(:bad_request)
    |> render(GORprojectWeb.ErrorView, :custom, message: "Bad id provided")
  end

  def call(conn, {:error, changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(GORprojectWeb.ChangesetView, "error.json", changeset: changeset)
  end
end

Fall back controller is okay but its better if you use the error_view file for displaying changeset errors.

 def render("errors.json", %{code: code, message: message, changeset: changeset}) do
    %{error: %{code: code, message: message, errors: translate_errors(changeset)}}
  end

 def translate_errors(changeset) do
   Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
end

you can pass your invalid changeset to this and it will display an error from that changeset .

2 Likes

I added your code in my error_view.ex, and it seems like the error is not handled there. From the stacktrace it seems that it goes from room_controller directly to changeset_view, in which there is :

  def translate_errors(changeset) do
    Ecto.Changeset.traverse_errors(changeset, &translate_error/1)
  end

  def render("error.json", %{changeset: changeset}) do
    # When encoded, the changeset returns its errors
    # as a JSON object. So we just pass it forward.
    %{errors: translate_errors(changeset)}
  end

From stacktrace it seems like in error helpers the translate_error function is not working for your is invalid message. You are passing one argument but it accepts 2 arguments.

I totally agree with you, and this is where i do not understand. In my errors.pot, i have the following code :

## From Ecto.Changeset.put_change/3
msgid "is invalid"
msgstr ""

According to this thread, it should be working

what is your goal? What you trying to implement?

I am trying to understand why this error is raised, how to handle it properly, and of course i am trying to fix the Repo.insert error

note that the error is not raised anymore if i delete father: {} inside

  def create_room(attrs \\ %{}) do
    %Room{children: [], father: {}, users: []}
    |> Room.changeset(attrs)
    |> Repo.insert()
  end

But i then have the following error :

error] #PID<0.594.0> running GORprojectWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: POST /api/rooms
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Enumerable not implemented for %GORproject.Rooms.Room{__meta__: #Ecto.Schema.Metadata<:loaded, "rooms">, children: [], depth: 1, description: nil, father: #Ecto.Association.NotLoaded<association :father is not loaded>, father_id: nil, id: 25, inserted_at: ~N[2018-10-01 09:52:22.729960], name: "Accueillir", public: false, updated_at: ~N[2018-10-01 09:52:22.733056], users: []}. This protocol is implemented for: DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, List, Map, MapSet, Postgrex.Stream, Range, Stream
        (elixir) /private/tmp/elixir-20180825-48782-1ofrgzr/elixir-1.7.3/lib/elixir/lib/enum.ex:1: Enumerable.impl_for!/1
        (elixir) /private/tmp/elixir-20180825-48782-1ofrgzr/elixir-1.7.3/lib/elixir/lib/enum.ex:141: Enumerable.reduce/3
        (elixir) lib/enum.ex:2979: Enum.reduce/3
        (gor_project) lib/gor_project_web/router.ex:1: GORprojectWeb.Router.Helpers.segments/3
        (gor_project) lib/gor_project_web/router.ex:1: GORprojectWeb.Router.Helpers.room_path/3
        (gor_project) lib/gor_project_web/controllers/room_controller.ex:22: GORprojectWeb.RoomController.create/2
        (gor_project) lib/gor_project_web/controllers/room_controller.ex:1: GORprojectWeb.RoomController.action/2
        (gor_project) lib/gor_project_web/controllers/room_controller.ex:1: GORprojectWeb.RoomController.phoenix_controller_pipeline/2
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.plug_builder_call/2
        (gor_project) lib/gor_project_web/endpoint.ex:1: GORprojectWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/giom/Documents/develop/web/GORProject-API/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Add IO.inspect after |> Room.changeset(attrs)

      def create_room(attrs \\ %{}) do
       %Room{children: [], father: {}, users: []}
       |> Room.changeset(attrs)
       |> IO.inspect()
       |> Repo.insert()
      end

And see what it gives you in the console. After you run this method

i’ve put two IO.inspect :

  def create_room(attrs \\ %{}) do
    %Room{children: [], father: {}, users: []}
    |> Room.changeset(attrs)
    |> IO.inspect()
    |> Repo.insert()
    |> IO.inspect()
  end

and i have :

[info] POST /api/rooms
[debug] Processing with GORprojectWeb.RoomController.create/2
  Parameters: %{"room" => %{"depth" => 1, "name" => "Accueillir", "public" => false}}
  Pipelines: [:api, :auth_api]
#Ecto.Changeset<
  action: nil,
  changes: %{depth: 1, name: "Accueillir", public: false},
  errors: [],
  data: #GORproject.Rooms.Room<>,
  valid?: true
>
[debug] QUERY OK db=0.3ms
begin []
[debug] QUERY OK db=0.2ms
rollback []
{:error,
 #Ecto.Changeset<
   action: :insert,
   changes: %{depth: 1, name: "Accueillir", public: false},
   errors: [father: "is invalid"],
   data: #GORproject.Rooms.Room<>,
   valid?: false
 >}

followed by the error stacktrace

errors: [father: "is invalid"]

I think this is the culprit, there errors should be in the form:

errors: [field: {message, metadata}]

so in this case:

errors: [father: {"is invalid", []}]

Are you setting the is invalid error yourself, or is Ecto setting it (via cast_assoc etc)? If the latter, it seems it’s an Ecto bug. What’s your Ecto version?

1 Like

Thats why ecto throws an error. transalate error function expects a tuple.

1 Like

btw, you’re defaulting father to tuple {}, you probably meant to write %{}, however for embeds/associations nil is probably better default. In any case, it’s still curious why you’re getting the error.

I got it working by puttin nil insteand of {}. Coming from javascript it’s still hard thinking in maps.
The error is sent by Ecto though, so i assume it’s an Ecto bug

Anyway, thank you both for your help :slight_smile:

ps : my ecto version is {:phoenix_ecto, "~> 3.2"}

glad it’s fixed!

my ecto version is {:phoenix_ecto, "~> 3.2"}

could you paste your ecto version from mix.lock? (or run: mix deps | grep ecto :)) If you happen to run on a recent version there’s likely still a bug in ecto.

I’ve reproduced this in ecto test suite, I’ll report this upstream!

2 Likes

Aaaand… it’s fixed! See https://github.com/elixir-ecto/ecto/pull/2725 by @qcam.

3 Likes

The speed at which you’ve done this amazes me !
Thank you :slight_smile:

2 Likes