Simple Api where create function send me 400 "Bad Request" but I can't figure out why

Hello,
I’m struggling to figure out why my api is sending me a 400 error on create route. Show and index route are working fine.
I tried different playload, I double check there was no error in json playload and

Here is my controller (auto generated with phoenix generator):

defmodule ColdDataApiWeb.ProjectController do
  use ColdDataApiWeb, :controller

  alias ColdDataApi.Record
  alias ColdDataApi.Record.Project

  action_fallback ColdDataApiWeb.FallbackController

  def index(conn, _params) do
    projects = Record.list_projects()
    render(conn, "index.json", projects: projects)
  end

  def create(conn, %{"project" => project_params}) do
    with {:ok, %Project{} = project} <- Record.create_project(project_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", project_path(conn, :show, project))
      |> render("show.json", project: project)
    end
  end

  def show(conn, %{"id" => id}) do
    project = Record.get_project!(id)
    render(conn, "show.json", project: project)
  end

  def update(conn, %{"id" => id, "project" => project_params}) do
    project = Record.get_project!(id)

    with {:ok, %Project{} = project} <- Record.update_project(project, project_params) do
      render(conn, "show.json", project: project)
    end
  end

  def delete(conn, %{"id" => id}) do
    project = Record.get_project!(id)
    with {:ok, %Project{}} <- Record.delete_project(project) do
      send_resp(conn, :no_content, "")
    end
  end
end

My schema:

defmodule ColdDataApi.Record.Project do
  use Ecto.Schema
  import Ecto.Changeset

  schema "projects" do
    field :name, :string
    field :short_description, :string
    field :description, :string
    field :human_name, :string

  end

  @allowed_fields [:name, :short_description, :description, :human_name]
  @required_fields [:name, :short_description, :human_name]

  @doc false
  def changeset(project, attrs) do
    project
    |> cast(attrs, @allowed_fields)
    |> validate_required(@required_fields)
    |> validate_length(:name, max: 40, count: :codepoints)
    |> validate_length(:short_description, max: 600, count: :codepoints)
    |> validate_length(:human_name, max: 40, count: :codepoints)
    |> validate_length(:description, max: 65_535, count: :codepoints)
    |> unique_constraint(:name)
  end
end

My FallbackController:

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

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

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(ColdDataApiWeb.ChangesetView, "error.json", changeset: changeset)
  end

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

I add the routes like this:

  # Other scopes may use custom stacks.
  scope "/api/v1", ColdDataApiWeb do
    pipe_through :api

    resources "/projects", ProjectController, except: [:new, :edit]
  end

I tested in iex console, Record.create_project works. I suppose the error come from the controller.
Does anyone could give me an hint on this error ?

Ps: english is not my mother tongue, so do not hesitate to ask me question if something is unclear.

1 Like

Can you provide the output of mix phx.routes?

And how do you send the request?

Try putting a few IO.inspect/1 calls into the create action:

  def create(conn, %{"project" => project_params}) do
    IO.inspect(project_params, label: "project_params")
    with {:ok, %Project{} = project} <- Record.create_project(project_params) do
      IO.inspect(project, label: "project")
      conn
      |> put_status(:created)
      |> put_resp_header("location", project_path(conn, :show, project))
      |> render("show.json", project: project)
      |> IO.inspect(label: "conn")
    end
  end

and making the request again. These inspect calls might help reveal the problem.

The output of mix phx.routes is:

project_path  GET     /api/v1/projects      ColdDataApiWeb.ProjectController :index
project_path  GET     /api/v1/projects/:id  ColdDataApiWeb.ProjectController :show
project_path  POST    /api/v1/projects      ColdDataApiWeb.ProjectController :create
project_path  PATCH   /api/v1/projects/:id  ColdDataApiWeb.ProjectController :update
              PUT     /api/v1/projects/:id  ColdDataApiWeb.ProjectController :update
project_path  DELETE  /api/v1/projects/:id  ColdDataApiWeb.ProjectController :delete

To send the request is tried both postman and curl with the same payload:

> curl -H "Content-Type: application/json" -X POST http://localhost:4000/api/v1/projects -d '{"name": "name ", "human_name":"h name", "short_description":"sd", "description":"des"}'

About putting the IO.inspect, I did it but I’m running the api into a docker container, and I don’t get outputs yet for this command (I get other logs through). So, once I figure out how to get thos output I’ll show you the results

There is no "project" key in your body. Thats the first thing I do see with your snippet.

Perhaps try a catch-all header for now (def create(conn, params) do...) and as the very first line do Logger..debug(params).

So I added the "project" key in the request body:

> curl -H "Content-Type: application/json" -X POST http://localhost:4000/api/v1/projects -d '{"project":{"short_description":"sd","name":"name","human_name":"h name","description":"des"}}'

{"errors":{"detail":"Internal Server Error"}}%

And With the logger I get the following error:

web    | Request: POST /api/v1/projects
web    | ** (exit) an exception was raised:
web    |     ** (Protocol.UndefinedError) protocol String.Chars not implemented for %{"description" => "technicals specifications of the project...", "human_name" => "project name", "name" => "TEST", "short_description" => "yolo"}. This protocol is implemented for: Atom, BitString, Date, DateTime, Decimal, Ecto.Date, Ecto.DateTime, Ecto.Time, Float, Integer, List, Mariaex.Query, NaiveDateTime, Time, URI, Version, Version.Requirement
web    |         (elixir) /usr/local/src/elixir/lib/elixir/lib/string/chars.ex:3: String.Chars.impl_for!/1
web    |         (elixir) /usr/local/src/elixir/lib/elixir/lib/string/chars.ex:22: String.Chars.to_string/1
web    |         (logger) lib/logger.ex:776: Logger.truncate/2
web    |         (logger) lib/logger.ex:626: Logger.bare_log/3
web    |         (cold_data_api) lib/cold_data_api_web/controllers/project_controller.ex:16: ColdDataApiWeb.ProjectController.create/2
web    |         (cold_data_api) lib/cold_data_api_web/controllers/project_controller.ex:1: ColdDataApiWeb.ProjectController.action/2
web    |         (cold_data_api) lib/cold_data_api_web/controllers/project_controller.ex:1: ColdDataApiWeb.ProjectController.phoenix_controller_pipeline/2
web    |         (cold_data_api) lib/cold_data_api_web/endpoint.ex:1: ColdDataApiWeb.Endpoint.instrument/4
web    |         (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
web    |         (cold_data_api) lib/cold_data_api_web/endpoint.ex:1: ColdDataApiWeb.Endpoint.plug_builder_call/2
web    |         (cold_data_api) lib/cold_data_api_web/endpoint.ex:1: ColdDataApiWeb.Endpoint.call/2
web    |         (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
web    |         (cowboy) /cold_data_api/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Now you used a "project" key in the body and you obviously didn’t use a catch all clause. Just remove the log-call and you should be good to go.

Thanks a lot ! It’s working.