AJAX Post request error: "can't be blank", [validation: :required] When sending JSON object in POST body

463 views Asked by At

I'm getting this error when a request hits my router at OPTIONS "/products"

error

14:45:33.433 [error] #PID<0.339.0> running Api.Router terminated
Server: 192.168.20.3:4000 (http)
Request: OPTIONS /products
** (exit) an exception was raised:
    ** (Poison.EncodeError) unable to encode value: {:brand, {"can't be blank", [validation: :required]}}
        (poison) lib/poison/encoder.ex:383: Poison.Encoder.Any.encode/2
        (poison) lib/poison/encoder.ex:259: anonymous fn/3 in Poison.Encoder.List.encode/3
        (poison) lib/poison/encoder.ex:260: Poison.Encoder.List."-encode/3-lists^foldr/2-1-"/3
        (poison) lib/poison/encoder.ex:260: Poison.Encoder.List.encode/3
        (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3
        (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map."-encode/3-lists^foldl/2-0-"/3
        (poison) lib/poison/encoder.ex:228: Poison.Encoder.Map.encode/3
        (poison) lib/poison/encoder.ex:227: anonymous fn/4 in Poison.Encoder.Map.encode/3

Image showing the error caught on the frontend that occurred during the network request:

enter image description here

Judging by the image above it looks like the product JSON object does get sent so for some reason it isn't correctly mapping the http post body to the elixir changeset.

log of conn:

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> %{}
%Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{},
 before_send: [], body_params: %{},
 cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false,
 host: "192.168.20.3", method: "OPTIONS", owner: #PID<0.339.0>, params: %{},
 path_info: ["products"], path_params: %{}, peer: {{192, 168, 20, 3}, 63793},
 port: 4000,
 private: %{plug_route: #Function<1.14347947/1 in Api.Router.do_match/4>},
 query_params: %{}, query_string: "", remote_ip: {192, 168, 20, 3},
 req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
 req_headers: [{"host", "192.168.20.3:4000"}, {"connection", "keep-alive"},
  {"access-control-request-method", "POST"}, {"origin", "http://evil.com/"},
  {"user-agent",
   "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/
537.36"},
  {"access-control-request-headers", "content-type,x-requested-with"},
  {"accept", "*/*"}, {"referer", "http://localhost:8081/debugger-ui"},
  {"accept-encoding", "gzip, deflate, sdch"},
  {"accept-language", "en-GB,en-US;q=0.8,en;q=0.6"}], request_path: "/products",
 resp_body: nil, resp_cookies: %{},
 resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
 scheme: :http, script_name: [], secret_key_base: nil, state: :unset,
 status: nil}
%Api.Product{__meta__: #Ecto.Schema.Metadata<:built, "products">, brand: nil,
 description: nil, id: nil, image: nil, name: nil, numberOfVotes: nil,
 rating: nil}

router.ex

defmodule Api.Router do
  use Plug.Router

  if Mix.env == :dev do
    use Plug.Debugger
  end
  plug :match
  plug Plug.Parsers, parsers: [:json],
                   pass:  ["application/json"],
                   json_decoder: Poison
  plug :dispatch

  get "/favicon.ico" do
    # Api.Repo.getCategories(conn)
  end

  get "/categories/" do
    Api.Repo.getCategories(conn)
  end

  options "/categories/" do
    Api.Repo.getCategories(conn)
  end

  post "/products" do
    Api.Repo.insertProduct(conn, conn.body_params)
  end

  options "/products" do
    IO.puts inspect conn.body_params
    Api.Repo.insertProduct(conn, conn.body_params)
  end

  get "/products" do
    Api.Repo.insertProduct(conn, conn.body_params)
  end
end

In repo.ex

  def insertProduct(conn, product) do
    IO.inspect(conn)
    changeset = Api.Product.changeset(%Api.Product{}, product)
    errors = changeset.errors
    valid = changeset.valid?
    case insert(changeset) do
      {:ok, product} ->
        conn
          |> put_resp_content_type("application/json")
          |> send_resp(200, Poison.encode!(%{
              successs: "success"
          }))
      {:error, changeset} ->

        conn
          |> put_resp_content_type("application/json")
          |> send_resp(500, Poison.encode!(%{
              failure: changeset
          }))
    end
  end

product.ex

defmodule Api.Product do
  use Ecto.Schema

  @derive {Poison.Encoder, only: [:name, :brand, :description, :image, :rating, :numberOfVotes]}
  schema "products" do
    field :name, :string
    field :brand, :string
    field :description, :string
    field :image, :string
    field :rating, :integer
    field :numberOfVotes, :integer
  end

  def changeset(product, params \\ %{}) do
    product
    |> Ecto.Changeset.cast(params, [:name, :brand, :description, :image, :rating, :numberOfVotes])
    |> Ecto.Changeset.validate_required([:name, :description, :brand])
  end
end

By the way - origin is evil.com because of a browser plugin I use to enable CORS

1

There are 1 answers

0
BeniaminoBaggins On

Some javascript global constants used in react native to allow network requests to show in the chrome developer console:

GLOBAL.XMLHttpRequest = GLOBAL.originalXMLHttpRequest || GLOBAL.XMLHttpRequest

Seem to also mess with the network requests themselves. Once I got rid of that code it inserted the POSTed product into the database.

Thanks for the comments which got me most of the way.