6
votes

I am a Phoenix/Elixir beginner and am trying to write an API to allow users to sign up in my application.

The API endpoint works as expected unless I try to set the HTTP status code of the response. When I include lines A, B and C (indicated in the code below), I get a FunctionClauseError with the message no function clause matching in :cowboy_req.status/1.

The complete error message is as follows:

[error] #PID<0.344.0> running App.Endpoint terminated
Server: localhost:4000 (http)
Request: POST /api/user/
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in :cowboy_req.status/1
        (cowboy) src/cowboy_req.erl:1272: :cowboy_req.status(451)
        (cowboy) src/cowboy_req.erl:1202: :cowboy_req.response/6
        (cowboy) src/cowboy_req.erl:933: :cowboy_req.reply_no_compress/8
        (cowboy) src/cowboy_req.erl:888: :cowboy_req.reply/4
        (plug) lib/plug/adapters/cowboy/conn.ex:34: Plug.Adapters.Cowboy.Conn.send_resp/4
        (plug) lib/plug/conn.ex:356: Plug.Conn.send_resp/1
        (app) web/controllers/user_controller.ex:1: App.UserController.action/2
        (app) web/controllers/user_controller.ex:1: App.UserController.phoenix_controller_app/2
        (app) lib/app/endpoint.ex:1: App.Endpoint.instrument/4
        (app) lib/phoenix/router.ex:261: App.Router.dispatch/2
        (app) web/router.ex:1: App.Router.do_call/2
        (app) lib/app/endpoint.ex:1: App.Endpoint.phoenix_app/1
        (app) lib/plug/debugger.ex:122: App.Endpoint."call (overridable 3)"/2
        (app) lib/app/endpoint.ex:1: App.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:15: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

My code is as follows:

defmodule App.UserController do
  use App.Web, :controller

  import Ecto.Changeset

  alias App.User
  alias App.Session

  def create(conn, params) do
    changeset = User.changeset(%User{}, params)

    case Repo.insert(changeset) do
      {:ok, _user} ->
        email = get_field(changeset, :email)
        password = get_field(changeset, :password)

        # Log on user upon sign up
        session_changeset = Session.changeset(%Session{
          email: email,
          password: password
        })
        result = Repo.insert(session_changeset)

        case result do
          {:ok, session} ->
            conn
            |> put_resp_cookie("SID", session.session_id)
            |> put_status(201)  # line A
            |> render("signup.json", data: %{
                 changeset: changeset
               })
          {:error, changeset} ->
            conn
            |> put_status(251)  # line B
            |> render("signup.json", data: %{
                 changeset: changeset
               })
        end
      {:error, changeset} ->
        conn
        |> put_status(451)  # line C
        |> render("signup.json", data: %{
             changeset: changeset
           })
    end
  end

end

Why does this happen and where am I going wrong?

1

1 Answers

10
votes

Edit as of 22nd October 2016 this is now possible on Plug master. Here is the relevant section of the docs for reference:

Custom status codes

Plug allows status codes to be overridden or added in order to allow new codes not directly specified by Plug or its adapters. Adding or overriding a status code is done through the Mix configuration of the :plug application. For example, to override the existing 404 reason phrase for the 404 status code
("Not Found" by default) and add a new 451 status code, the following config can be specified:

  config :plug, :statuses, %{
    404 => "Actually This Was Found",
    451 => "Unavailable For Legal Reasons"
  }

As this configuration is Plug specific, Plug will need to be recompiled for the changes to take place: this will not happen automatically as dependencies are not automatically recompiled when their configuration changes. To recompile Plug:

MIX_ENV=prod mix deps.compile plug

The atoms that can be used in place of the status code in many functions are inflected from the reason phrase of the status code. With the above configuration, the following will all work:

  put_status(conn, :not_found)                     # 404
  put_status(conn, :actually_this_was_found)       # 404
  put_status(conn, :unavailable_for_legal_reasons) # 451

Even though 404 has been overridden, the :not_found atom can still be used to set the status to 404 as well as the new atom :actually_this_was_found inflected from the reason phrase "Actually This Was Found".


Cowboy manually specifies the HTTP response code and matches on the integer specified.

https://github.com/ninenines/cowboy/blob/1.0.x/src/cowboy_req.erl#L1318

A binary is permitted, however doing:

conn
|> put_status("451 Unavailable For Legal Reasons")

Won't work as plug only permits an integer or a known atom.

This should probably be considered a bug. You can try and get a pull request into Cowboy in the file I linked.

If getting a PR merged into Cowboy isn't possible, it can also be performed in Plug for the Cowboy adapter by transforming the status (this is a naive solution):

status = if (status == 451) do
  "451 Unavailable For Legal Reasons"
else
  status
end

In this file https://github.com/elixir-lang/plug/blob/master/lib/plug/adapters/cowboy/conn.ex#L33

See also https://github.com/ninenines/cowboy/issues/965 and https://github.com/elixir-lang/plug/issues/451