TLDR;

In a pipeline function, call halt() after a redirect().

In a controller action, it is not necessary to halt after a redirect.

Halt - The Confusion

If you’re a Phoenix beginner like me, you may have seen some code where a redirect() is followed by a halt().

The code generated by mix phx.gen.auth includes a module called UserAuth, which has the following function defined:

def redirect_if_user_is_authenticated(conn, _opts) do
  if conn.assigns[:current_user] do
    conn
    |> redirect(to: signed_in_path(conn))
    |> halt()
  else
    conn
  end
end

But you can also find examples where redirect is not followed by halt. UserSessionController has the following function:

defp create(conn, %{"user" => user_params}, info) do
  %{"email" => email, "password" => password} = user_params

  if user = Accounts.get_user_by_email_and_password(email, password) do
    conn
    |> put_flash(:info, info)
    |> UserAuth.log_in_user(user, user_params)
  else
    conn
    |> put_flash(:error, "Invalid email or password")
    |> put_flash(:email, String.slice(email, 0, 160))
    |> redirect(to: ~p"/users/log_in")
  end
end

The Phoenix Controller docs docs illustrate a redirect followed by a halt, and the text below their example states that Plug.Conn.halt/1 is “by default imported into controllers”.

These different examples can make one wonder when and why to use halt().

What does halt do?

The docs for halt(conn) are brief, saying it “Halts the Plug pipeline by preventing further plugs downstream from being invoked.”

Phoenix is built around the concept of transforming some input data through a pipeline (a series of pipe stages), resulting in output data which is then returned to the client. But when circumstances dictate a change in paths (such as with a redirect), then some pipelines need to be stopped.

Looking in the Router module, we can see something like this:

pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, html: {ExampleWeb.Layouts, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers
  plug :fetch_current_user
end

Each of these plugs gets called in sequence, with the output of the previous stage being the input of the next stage. Any one of these stages could decide that the process needed to deviate or stop based on any number of reasons.

As we see earlier, redirect_if_user_is_authenticated() branches based on a condition in the current state (which is usually called conn). In one path, it redirects and halts. In the other path, it simply returns appropriately so the next stage of the pipeline can be executed.

In this case, the halt is important because it tells Phoenix to stop further processing of this pipeline. Meanwhile, the redirect will have resulted in another pipeline running, and that pipeline should be the one to determine and return the final data.

When to use halt

USE

Call halt() when you need to stop the pipeline from performing any following stages. This would be in a plug function after a redirect.

DON’T USE

Since a controller action is the last stage of a pipeline, there is no need to call it within the action (even if you have a redirect). It is harmless if used at the end of the pipeline, however…

Why was that confusing?

Perhaps it’s just me, but I was confused about this probably mostly because of the previously mentioned line in the controller docs which stated that halt “is by default imported into controllers”. Why would it be automatically available to controllers if controller actions are the last step of a pipeline?

The reason is that you may wish to write some function plugs in a controller (similar to Rails’ before filters) which would be run before your actions in that controller. And your custom plug might wish to stop the current process and redirect to a new one. Therefore, your plug function would need to use halt.

I still find the choice to make halt automatically available in controllers curious, because it implies that there would be so many times you would need it that having it automatically included was a significant convenience. The implication is that a controller will have more plug functions (with redirects) than actions (which don’t need to halt even if they redirect). I kind of doubt that’s the reality.

I would expect controllers to have more actions than plug functions which redirect. And in that case, I think it would be clearer to make the developer explicitly import halt/1 when needed.