Phoenix视图渲染报错:Protocol.UndefinedError(Enumerable未实现)
问题:Phoenix API渲染响应时遇到Enumerable未实现错误
本人是Elixir、Phoenix/Ecto及Erlang新手,目前正通过TDD方式搭建全新API。参照现有可用示例定义Model、View和Controller,但创建Conversation并尝试渲染响应时,持续遇到Protocol.UndefinedError: protocol Enumerable not implemented错误。
以下是相关代码片段及错误输出:
路由代码
scope "/conversations" do post "/", ConversationController, :create end
控制器代码
defmodule ThingyWeb.ConversationController do use ThingyWeb, :controller alias Thingy.Conversations.Conversation alias Thingy.Repo def create(conn, conversation_params) do case Thingy.Auth.validate_session(conn) do {:ok, _} -> result = %Conversation{} |> Conversation.changeset(conversation_params) |> Repo.insert() case result do {:ok, conversation} -> IO.puts("store to db went well, now trying to render response") IO.inspect(conversation) conn |> put_status(201) |> render("created.json", conversation) # 问题所在行 {:error, _} -> conn |> put_status(400) |> render("error.json", %{message: "Unable to process conversation as provided"}) end {:error, "No token"} -> conn |> put_status(401) |> render("error.json", %{message: "Authentication failed (no token)"}) {:error, "Not found"} -> conn |> put_status(404) |> render("error.json", %{message: "Authentication failed (not found)"}) end end end
模型代码
defmodule Thingy.Conversations.Conversation do use Ecto.Schema import Ecto.Changeset schema "conversations" do field :title, :string field :start_date_time, :utc_datetime timestamps() end @doc false def changeset(conversation, attrs) do conversation |> cast(attrs, [:title, :start_date_time]) |> validate_required([:title, :start_date_time]) end end
视图代码
defmodule ThingyWeb.ConversationView do use ThingyWeb, :view def render("error.json", %{message: message}) do %{ errors: [message] } end def render("created.json", %{conversation: conversation}) do render_one(conversation, ConversationView, "conversation.json") end def render("conversation.json", %{conversation: conversation}) do %{ id: conversation.id, title: conversation.title } end end
测试代码
describe "Conversation operations" do setup [:login_user] test "able to provide details of a new conversation, and receive the assigned id in response", %{ conn: conn, authentication: authentication } do IO.puts("starting problem test") conn = conn |> put_req_header("authorization", "Bearer " <> authentication.meallogger_token) |> put_req_header("content-type", "application/json") |> post( Routes.conversation_path(conn, :create), %{ title: "new conversation", start_date_time: "2024-06-14T15:30:00Z", } ) resp = json_response(conn, 201) assert %{ "id" => _id, } = resp case validate_return_properties(resp, @expected_create_response_properties) do {:error, extra_keys} -> assert false, "there were extraneous keys in the json response: #{extra_keys}" end end end
错误输出
starting problem test store to db went well, now trying to render response %Metabite.Conversations.Conversation{ __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, id: 50, title: "new conversation", start_date_time: ~U[2024-06-14 15:30:00Z], inserted_at: ~N[2024-06-07 06:30:42], updated_at: ~N[2024-06-07 06:30:42] } Mix task exited with reason normal returning code 0 1) test Conversation operations able to provide details of a new conversation, and receive the assigned id in response (ThingyWeb.ConversationControllerTest) test/thingy_web/controllers/conversation_controller_test.exs:50 ** (Protocol.UndefinedError) protocol Enumerable not implemented for %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], conn: %Plug.Conn{adapter: {Plug.Adapters.Test.Conn, :...}, assigns: %{id: 49, title: "new conversation", __struct__: Thingy.Conversations.Conversation, layout: false, inserted_at: ~N[2024-06-07 06:30:40], __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1}, body_params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, cookies: %{}, halted: false, host: "www.example.com", method: "POST", owner: #PID<0.587.0>, params: %{"start_date_time" => "2024-06-14T15:30:00Z", "title" => "new conversation"}, path_info: ["api", "v1", "conversations"], path_params: %{}, port: 80, private: %{ThingyWeb.Router => {[], %{PhoenixSwagger.Plug.SwaggerUI => []}}, :phoenix_view => ThingyWeb.ConversationView, :phoenix_template => "created.json", :phoenix_router => ThingyWeb.Router, :phoenix_endpoint => ThingyWeb.Endpoint, :phoenix_action => :create, :phoenix_controller => ThingyWeb.ConversationController, :before_send => [#Function<0.54455629/1 in Plug.Telemetry.call/2>], :plug_session_fetch => #Function<1.76384852/1 in Plug.Session.fetch_session/1>, :plug_skip_csrf_protection => true, :phoenix_recycled => true, :phoenix_request_logger => {"request_logger", "request_logger"}, :phoenix_format => "json", :phoenix_layout => {ThingyWeb.LayoutView, :app}}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{}, req_headers: [{"accept", "application/json"}, {"authorization", "Bearer auth-token-123"}, {"content-type", "application/json"}], request_path: "/api/v1/conversations", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "F9alGnAy2l0PbroAAAbB"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: 201}, __meta__: #Ecto.Schema.Metadata<:loaded, "conversations">, start_date_time: ~U[2024-06-14 15:30:00Z], updated_at: ~N[2024-06-07 06:30:40], current_user: 1} of type Thingy.Conversations.Conversation (a struct). This protocol is implemented for the following type(s): DBConnection.PrepareStream, DBConnection.Stream, Date.Range, Ecto.Adapters.SQL.Stream, File.Stream, Function, GenEvent.Stream, HashDict, HashSet, IO.Stream, Jason.OrderedObject, JasonV.OrderedObject, List, Map, MapSet, Phoenix.LiveView.LiveStream, Postgrex.Stream, Range, Stream code: |> post( stacktrace: (elixir 1.16.3) lib/enum.ex:1: Enumerable.impl_for!/1 (elixir 1.16.3) lib/enum.ex:166: Enumerable.reduce/3 (elixir 1.16.3) lib/enum.ex:4396: Enum.reverse/1 (elixir 1.16.3) lib/enum.ex:3726: Enum.to_list/1 (elixir 1.16.3) lib/map.ex:224: Map.new_from_enum/1 (phoenix_view 2.0.2) lib/phoenix_view.ex:370: Phoenix.View.render/3 (phoenix_view 2.0.2) lib/phoenix_view.ex:557: Phoenix.View.render_to_iodata/3 (phoenix 1.6.15) lib/phoenix/controller.ex:772: Phoenix.Controller.render_and_send/4 (thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.action/2 (thingy 0.1.1) lib/thingy_web/controllers/conversation_controller.ex:1: ThingyWeb.ConversationController.phoenix_controller_pipeline/2 (phoenix 1.6.15) lib/phoenix/router.ex:354: Phoenix.Router.__call__/2 (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.plug_builder_call/2 (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint."call (overridable 3)"/2 (thingy 0.1.1) deps/plug/lib/plug/debugger.ex:136: ThingyWeb.Endpoint."call (overridable 4)"/2 (thingy 0.1.1) lib/thingy_web/endpoint.ex:1: ThingyWeb.Endpoint.call/2 (phoenix 1.6.15) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5 test/thingy_web/controllers/conversation_controller_test.exs:60: (test)
解决方案
问题出在控制器的render调用逻辑上:
render("created.json", conversation)
Phoenix的render/3函数要求第二个参数是包含模板所需变量的映射(map),而非直接传入struct。当你直接传递conversation这个Ecto struct时,Phoenix会尝试将其转换为映射,但Ecto struct本身并非Enumerable类型,因此抛出Protocol.UndefinedError。
修改控制器中的这一行代码,将struct包装成键值对形式的映射:
render("created.json", %{conversation: conversation})
修改后,视图中的render("created.json", %{conversation: conversation})就能正确接收conversation变量,调用render_one渲染单个资源,问题即可解决。
内容的提问来源于stack exchange,提问作者Timo Newton
相关产品推荐
相关产品推荐

