Halting Plugs in Phoenix

Zachery Moneypenny
Adorable
Published in
3 min readAug 10, 2017

Here at adorable we’re on a journey to introduce more internal folks to writing web services with Elixir and Phoenix. I’m currently working on building a simple analytics platform for a cross-platform product we’re working on and I’ve been learning a ton about the architecture and design of a web service written in Elixir.

Of course, this learning has been punctuated every so often by a lack of understanding of the Elixir Way. There have been plenty of situations where I could force through some Ruby-ish code to get past a problem, but I’m focusing on representing solutions in idiomatic Elixir so I can get a better understanding of the language and framework (Phoenix, in this case.)

One recent stumbling block was a failing manual test that I could not reproduce in a unit test. I wrote a plug to handle Authorization header validation along with tests for the Plug itself as well as for the controller that applied it. All these tests passed both the happy path and edge cases, but the next day at an internal demo things went sideways.

Instead of the 401 I expected when hitting an endpoint with no Authorization header, I received a 500 with the following error in my server console:

[debug] Processing by FooWeb.AccountController.show/2
Parameters: %{"id" => "1"}
Pipelines: [:api]
[debug] FooWeb.AccountController halted in FooWeb.Authentication.call/2
[error] #PID<0.353.0> running FooWeb.Endpoint terminated
Server: localhost:4000 (http)
Request: GET /api/accounts/1
** (exit) an exception was raised:
** (Plug.Conn.NotSentError) a response was neither set nor sent from the connection
(plug) lib/plug/adapters/cowboy/handler.ex:42: Plug.Adapters.Cowboy.Handler.maybe_send/2
(plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
(cowboy) /Users/zmoneype/code/ador/Foo/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Much of the example code I’d seen in my searching for tutorials on Plug included code very much like what I had written to halt the chain in case of an error.

My incorrect implementation was:

defp auth_error!(conn) do
conn |> put_status(:unauthorized) |> halt
end

Critically, this implementation worked for all the tests I’d written because of course my unit tests were merely invoking functions on each of the modules under test, not exercising the full router request processing of the Phoenix framework.

Before fixing the incomplete code in my plug, I was determined to get a failing test bootstrapped so we could test these integration scenarios in the future. This time, after only a little googling I found that Phoenix includes a way to start a server under test specifically so we can hit the endpoints in that environment.

All I had to do was set server: true in config/test.exs:

config :my_app, FooWeb.Endpoint,
http: [port: 4001],
server: true

Now I could create a new class of integration test that would use an HTTP client library to hit the server and exercise the router (and all configured plugs) to verify that the authentication plug returned the correctly formatted HTTP response. The failing test I wrote looked thusly:

defmodule FooWeb.ProductsTest do
use FooWeb.IntegrationCase
test "show, 401 when no auth header" do
resp = HTTPoison.get!("http://localhost:4001/api/accounts/123/products/456")
assert resp.status_code == 401
end
end

This then gave me the failing test that I needed and I could move on to fixing the issue with my authentication plug. The problem was that even though we were setting the status code of the response (via put_status(:unauthorized)) we were never actually sending the response to the client.

The fixed method:

defp auth_error!(conn) do
conn
|> put_status(:unauthorized)
|> Phoenix.Controller.render(FooWeb.ErrorView, "401.json")
|> halt
end

Zachery Moneypenny is a Principal Developer at adorable.io. Rubyist for a long time, with experience in Golang and Javascript as well but moving towards Elixir and loving it.

--

--

Zachery Moneypenny
Adorable

engineering manager at cars.com | opinions on tech interviewing | woodworking | song-and-dance man