0
votes

From an elixir genserver process I am executing the post_metdata method to send a JSON HTTP Patch request to a Phoenix endpoint (on another server). The server continues to error indicating no matching clause showing the JSON link structure is not being included in the params based to the method. The id is included but not the passed data payload. Any suggestions on what appears to be the problem?

Client code

defp post_metadata(metadata, webhook) do
    case HTTPoison.patch webhook, encode(metadata), [{"content-type", "application/json"}] do
      {:ok, %HTTPoison.Response{status_code: 200} = response} ->
        # Logger.debug response.body
        Logger.debug "Successfully extracted and posted metadata for #{webhook}"
      {:error, %HTTPoison.Error{reason: reason}} ->
        Logger.warn "Unable to extract and post metadata for #{webhook}"
    end

  end

  defp encode(metadata) do
    %{
      "link":
      %{
        "title": metadata.title,
        "description": metadata.description
      }
    }
    |> Poison.encode!
  end

Phoenix Controller method expected, but not being matched

  def update(conn, %{"id" => id, "link" => link_params}) do
    link = Repo.get!(Link, id)
    changeset = Link.changeset(link, link_params)

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

router.ex

defmodule Mini.Router do
  use Mini.Web, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
    plug Plug.Logger, log: :debug
  end

  scope "/", Mini do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
  end

  scope "/api", Mini do
    pipe_through :api
    resources "/links", LinkController, except: [:new, :edit]
  end
end

Error logged to the console

[debug] ** (Phoenix.ActionClauseError) bad request to Mini.LinkController.update, no matching action clause to process request
    (mini) web/controllers/link_controller.ex:39: Mini.LinkController.update(%Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{}, before_send: [#Function<1.42492691/1 in Plug.Logger.call/2>, #Function<1.42492691/1 in Plug.Logger.call/2>, #Function<0.111727833/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "PATCH", owner: #PID<0.438.0>, params: %{"id" => "12"}, path_info: ["api", "links", "12"], path_params: %{}, peer: {{127, 0, 0, 1}, 55369}, port: 4000, private: %{Mini.Router => {[], %{}}, :phoenix_action => :update, :phoenix_controller => Mini.LinkController, :phoenix_endpoint => Mini.Endpoint, :phoenix_format => "json", :phoenix_layout => {Mini.LayoutView, :app}, :phoenix_pipelines => [:api], :phoenix_route => #Function<4.107513407/1 in Mini.Router.match_route/4>, :phoenix_router => Mini.Router, :phoenix_view => Mini.LinkView, :plug_session_fetch => #Function<1.61377594/1 in Plug.Session.fetch_session/1>}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"content-type", "application/json, application/json"}, {"user-agent", "hackney/1.6.6"}, {"host", "localhost:4000"}, {"content-length", "58"}], request_path: "/api/links/12", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "5bggaasurlj1oe027nvmv5aiek0hq3k8"}], scheme: :http, script_name: [], secret_key_base: "YfnqjmBhsSJMF/TmhK6qpMnJl7mS0tIYHk1tZ/dZUA6d7KOdv2g/AOJUfWo8sulb", state: :unset, status: nil}, %{"id" => "12"})
    (mini) web/controllers/link_controller.ex:1: Mini.LinkController.action/2
    (mini) web/controllers/link_controller.ex:1: Mini.LinkController.phoenix_controller_pipeline/2
    (mini) lib/mini/endpoint.ex:1: Mini.Endpoint.instrument/4
    (mini) lib/phoenix/router.ex:261: Mini.Router.dispatch/2
    (mini) web/router.ex:1: Mini.Router.do_call/2
    (mini) lib/mini/endpoint.ex:1: Mini.Endpoint.phoenix_pipeline/1
    (mini) lib/plug/debugger.ex:123: Mini.Endpoint."call (overridable 3)"/2
    (mini) lib/mini/endpoint.ex:1: Mini.Endpoint.call/2
    (plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Handler.upgrade/4
    (cowboy) /Users/billc/dev/mini/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

I've executed a request mocking the JSON data structure using Insomnia with success. I sent both requests to Httpbin and compared. The only difference I can find is the Content-type has duplicate application/json entries for the request sent by HTTPoison. But, I can find no reason or option to prevention the duplicate value. Nor any reason why Phoenix would choke on it.

1
I think the invalid content-type is causing Plug to not parse the body but I don't know why the header is being sent like that.Dogbert
I am equally suspicious of the content-type.Bill Christian

1 Answers

0
votes

For an unexplainable reason, HTTPoison is adding duplicate 'application/json' values in the content-type header. Plug is choking on having multiple content-type values. I worked around the issue by adding a semi-colon to the end of the passed in header.

case HTTPoison.patch webhook, encode(metadata), [{"content-type", "application/json;"}] do

The ending semi-colon terminates the content-type value. HTTPoison only inserts a single 'application/json' value and Plug behaves normally.

Strange.